1. 项目概述:基于Vue与Spring Boot的博客打赏系统
去年在开发个人技术博客时,我发现很多读者在阅读优质内容后都有打赏意愿,但现有平台要么抽成过高,要么功能简陋。于是决定自己开发一套完整的打赏系统,经过三个版本的迭代,目前这套系统已稳定运行在我的博客上,日均处理打赏订单50+笔。本文将分享从技术选型到部署上线的完整实现方案。
这个系统最核心的价值在于:
- 为博主提供零抽成的打赏渠道
- 读者可以自由选择支付金额(5/10/20元三档)
- 实时展示打赏排行榜增强互动性
- 完整的交易记录查询功能
技术栈选择Vue.js + Spring Boot的组合,主要考虑到:
- 前端需要快速响应和良好的用户体验
- 后端需要稳定处理支付回调等敏感操作
- 整套系统需要易于扩展和维护
2. 技术架构设计
2.1 整体架构图
系统采用经典的前后端分离架构:
code复制[浏览器]
↔ [Vue前端]
↔ [Spring Boot API]
↔ [MySQL数据库]
↔ [支付宝/微信支付接口]
2.2 技术选型决策
前端技术栈:
- Vue 2.x:考虑到生态成熟度和团队熟悉程度
- Element UI:比Ant Design更适合内容型网站的风格
- Axios:处理API请求,内置拦截器实现JWT自动刷新
- Vue Router:实现前端路由和权限控制
后端技术栈:
- Spring Boot 2.7:提供完整的RESTful API支持
- Spring Security:处理认证和授权
- JPA:简化数据库操作,快速开发
- Lombok:减少样板代码
- MapStruct:处理DTO转换
数据库:
- MySQL 8.0:事务支持完善,社区资源丰富
- Redis:缓存热点数据如打赏排行榜
提示:初期考虑过MongoDB,但支付系统对事务要求较高,最终选择关系型数据库
3. 核心功能实现
3.1 前端关键组件开发
打赏按钮组件实现
vue复制<template>
<div class="reward-wrapper">
<el-popover
placement="top"
width="300"
trigger="click"
@show="loadRecentRewards">
<template #reference>
<el-button
type="warning"
icon="el-icon-goods"
class="reward-btn">
支持作者
</el-button>
</template>
<div v-loading="loading">
<h4>选择打赏金额</h4>
<el-radio-group
v-model="amount"
size="medium"
@change="handleAmountChange">
<el-radio-button :label="5">5元</el-radio-button>
<el-radio-button :label="10">10元</el-radio-button>
<el-radio-button :label="20">20元</el-radio-button>
</el-radio-group>
<div class="recent-rewards">
<h5>最近打赏</h5>
<ul>
<li v-for="item in recentRewards" :key="item.id">
{{ item.nickname }} 打赏了 {{ item.amount }}元
</li>
</ul>
</div>
<el-button
type="primary"
@click="handlePayment"
:disabled="!amount">
立即支付
</el-button>
</div>
</el-popover>
</div>
</template>
<script>
export default {
props: {
articleId: {
type: String,
required: true
}
},
data() {
return {
amount: null,
recentRewards: [],
loading: false
}
},
methods: {
async loadRecentRewards() {
this.loading = true
try {
const res = await this.$api.reward.getRecent({
articleId: this.articleId,
limit: 5
})
this.recentRewards = res.data
} finally {
this.loading = false
}
},
handlePayment() {
this.$emit('payment-start', this.amount)
this.$api.payment.createOrder({
amount: this.amount,
articleId: this.articleId
}).then(res => {
// 调用支付SDK
this.invokePaymentSDK(res.data.paymentParams)
}).catch(err => {
this.$message.error(err.message)
})
},
invokePaymentSDK(params) {
// 微信/支付宝SDK调用逻辑
}
}
}
</script>
<style scoped>
.reward-wrapper {
margin: 20px 0;
}
.reward-btn {
font-weight: bold;
}
.recent-rewards {
margin: 15px 0;
max-height: 200px;
overflow-y: auto;
}
</style>
关键实现细节
-
金额选择交互:
- 使用Element UI的RadioButtonGroup实现金额选择
- 禁用未选择金额时的支付按钮防止误操作
-
最近打赏列表:
- 在弹出层显示时异步加载数据
- 设置最大高度和滚动条防止内容过多
-
支付流程:
- 通过$emit通知父组件支付开始
- 统一错误处理显示友好提示
3.2 后端接口设计
支付订单创建接口
java复制@RestController
@RequestMapping("/api/payment")
@RequiredArgsConstructor
public class PaymentController {
private final PaymentService paymentService;
private final RewardService rewardService;
@PostMapping("/create")
public ResponseEntity<PaymentResponse> createOrder(
@Valid @RequestBody PaymentRequest request,
@AuthenticationPrincipal User user) {
// 验证文章是否存在
Article article = rewardService.validateArticle(request.getArticleId());
// 创建支付订单
PaymentResponse response = paymentService.createOrder(
user.getId(),
article.getId(),
request.getAmount(),
request.getChannel()
);
return ResponseEntity.ok(response);
}
@GetMapping("/records")
public Page<RewardRecordVO> getRewardRecords(
@RequestParam String articleId,
Pageable pageable) {
return rewardService.getRecordsByArticle(articleId, pageable)
.map(this::convertToVO);
}
@PostMapping("/callback/{channel}")
public String handlePaymentCallback(
@PathVariable String channel,
@RequestBody String callbackData) {
return paymentService.processCallback(channel, callbackData);
}
private RewardRecordVO convertToVO(RewardRecord record) {
// 使用MapStruct实现DTO转换
}
}
关键业务逻辑
-
订单创建流程:
- 验证用户身份(Spring Security自动处理)
- 校验文章有效性
- 调用支付服务生成订单
-
分页查询优化:
- 使用Spring Data JPA的Pageable实现分页
- 添加缓存减少数据库压力
-
支付回调处理:
- 支持多支付渠道
- 幂等设计防止重复处理
4. 支付系统集成
4.1 微信支付配置
yaml复制# application.yml
payment:
wechat:
app-id: ${WECHAT_APP_ID}
mch-id: ${WECHAT_MCH_ID}
key: ${WECHAT_API_KEY}
cert-path: classpath:certs/wechat/apiclient_cert.p12
notify-url: https://yourdomain.com/api/payment/callback/wechat
refund-notify-url: https://yourdomain.com/api/payment/callback/wechat-refund
4.2 支付宝支付实现
java复制@Service
@RequiredArgsConstructor
public class AlipayServiceImpl implements PaymentService {
private final AlipayProperties properties;
private final OrderRepository orderRepository;
@Override
public PaymentResponse createOrder(Long userId, Long articleId,
BigDecimal amount, String channel) {
// 创建本地订单记录
PaymentOrder order = createLocalOrder(userId, articleId, amount, channel);
// 构造支付宝请求
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setNotifyUrl(properties.getNotifyUrl());
request.setReturnUrl(properties.getReturnUrl());
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo(order.getOrderNo());
model.setTotalAmount(amount.setScale(2).toString());
model.setSubject("文章打赏:" + articleId);
model.setProductCode("FAST_INSTANT_TRADE_PAY");
request.setBizModel(model);
try {
// 调用SDK生成支付表单
String form = AlipayClientFactory.getClient()
.pageExecute(request).getBody();
return PaymentResponse.builder()
.orderNo(order.getOrderNo())
.paymentForm(form)
.build();
} catch (AlipayApiException e) {
throw new PaymentException("支付宝下单失败", e);
}
}
@Override
public String processCallback(String callbackData) {
// 验证签名和处理回调逻辑
}
}
4.3 支付安全措施
-
签名验证:
- 所有支付请求必须携带有效签名
- 使用支付平台提供的SDK验证签名有效性
-
金额校验:
- 前端传递的金额必须与后端配置的档位一致
- 防止篡改金额攻击
-
防重复支付:
- 订单状态机设计确保不会重复处理
- 数据库唯一索引防止订单号重复
5. 数据持久层设计
5.1 实体关系模型
java复制@Entity
@Table(name = "reward_order")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RewardOrder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 32)
private String orderNo;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal amount;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private PaymentStatus status;
@Column(nullable = false, length = 20)
private String paymentChannel;
@Column(nullable = false)
private LocalDateTime createTime;
@Column
private LocalDateTime completeTime;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "article_id", nullable = false)
private Article article;
@Version
private Integer version;
}
5.2 查询优化策略
-
N+1问题解决:
- 使用@EntityGraph标注常用查询路径
- 复杂查询使用JOIN FETCH
-
分页性能优化:
- 添加适当的索引
- 使用COUNT OVER()窗口函数减少查询次数
-
缓存策略:
- 热点数据如打赏排行榜使用Redis缓存
- 设置合理的过期时间
6. 系统安全防护
6.1 JWT认证实现
java复制@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationFilter jwtFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/payment/callback/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
6.2 敏感数据保护
-
支付信息加密:
- 数据库中的敏感字段使用AES加密
- 密钥通过KMS系统管理
-
日志脱敏:
- 使用Logback的PatternLayout实现自动脱敏
- 过滤身份证、银行卡等敏感信息
-
接口防刷:
- 使用Guava RateLimiter实现简单限流
- 关键操作添加验证码校验
7. 部署与运维
7.1 Docker Compose配置
yaml复制version: '3.8'
services:
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "80:80"
environment:
- VUE_APP_API_BASE_URL=https://api.yourdomain.com
networks:
- app-network
depends_on:
- backend
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
- DB_URL=jdbc:mysql://db:3306/reward?useSSL=false
- DB_USER=reward_user
- DB_PASSWORD=${DB_PASSWORD}
networks:
- app-network
depends_on:
- db
- redis
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: reward
MYSQL_USER: reward_user
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- db_data:/var/lib/mysql
networks:
- app-network
redis:
image: redis:6.2
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- app-network
nginx:
image: nginx:1.21
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./nginx/ssl:/etc/nginx/ssl
networks:
- app-network
depends_on:
- frontend
- backend
networks:
app-network:
driver: bridge
volumes:
db_data:
redis_data:
7.2 性能优化建议
-
前端优化:
- 使用CDN加速静态资源
- 开启Gzip压缩
- 实现组件级懒加载
-
后端优化:
- JVM参数调优
- 数据库连接池配置
- 启用HTTP/2协议
-
监控方案:
- Prometheus + Grafana监控系统指标
- ELK收集分析日志
- Sentry捕获前端异常
8. 踩坑经验分享
8.1 支付回调处理
问题现象:
- 支付宝回调验签失败
- 微信回调重复通知
解决方案:
- 确保服务器时间与支付平台同步
- 实现幂等处理逻辑
- 添加手动补单功能
8.2 跨域问题
问题现象:
- 开发环境接口调用失败
- 生产环境某些浏览器无法请求
解决方案:
- 统一使用Nginx反向代理
- 配置详细的CORS策略
- 前端axios设置withCredentials
8.3 移动端适配
问题现象:
- iOS系统支付页面显示异常
- 某些Android机型无法调起支付
解决方案:
- 添加专门的移动端检测逻辑
- 针对不同平台使用不同的支付方式
- 实现H5支付兜底方案
这套系统经过半年多的线上运行,处理了超过1万笔打赏订单,最关键的体会是:支付系统无小事,必须做到每一笔交易都可追溯、可审计。特别是在处理回调通知时,一定要做好日志记录和异常处理,否则很容易出现账务不一致的情况。