支付回调处理,咱得整得 "幂等可靠" 不翻车
作为一个写了 8 年 Java 的老兵,说实话,支付回调这玩意,属实不能马虎。一不小心就 "翻车"。本文就从实战角度聊聊,如何用 幂等可靠的姿势,优雅处理支付回调。
💥 先说问题:支付回调为啥这么敏感?
支付回调,简单说,就是用户付款后,第三方支付平台(比如支付宝、微信)会把"付款成功"的消息回调给我们系统。
BUT! 回调可能:
- 被多次触发(支付平台重试机制)
- 被恶意伪造(安全问题)
- 到达顺序不一致(并发问题)
如果我们系统里对同一订单处理多次,就可能出现:
- 重复发货
- 多次发放优惠券
- 错误业务状态
这就尴尬了,业务也翻车,客户也不满,老板也发火。
🎯 核心目标:幂等 + 安全 + 高可用
我们要的不是"处理一次",而是:
- 无论多少次回调,业务只处理一次
- 防止伪造
- 保证高并发下不出错
🧠 思路拆解
✅ 1. 校验签名,验证来源
确保是支付平台真的发的请求,不是别人伪造的。
✅ 2. 校验业务参数
比如校验订单是否存在、支付金额是否正确等。
✅ 3. 幂等处理核心:状态机 + 乐观锁 / 分布式锁
订单状态从 "待支付" → "已支付",只允许状态从前往后流转,任何重复请求都不再处理。
✅ 4. 异常友好,日志记录
任何异常、非法请求都要记录日志,方便排查。
🧪 实战代码
下面直接上关键代码,基于 Spring Boot + MyBatis Plus 实现。
🚪 控制器入口
kotlin
@PostMapping("/api/pay/callback")
public String handlePayCallback(@RequestBody PayCallbackRequest request) {
log.info("收到支付回调:{}", request);
try {
// 1. 验签
if (!payService.verifySignature(request)) {
log.warn("验签失败!");
return "FAIL";
}
// 2. 幂等处理
payService.handleCallback(request);
return "SUCCESS";
} catch (Exception e) {
log.error("处理支付回调异常", e);
return "FAIL";
}
}
🧰 核心处理逻辑
scss
@Transactional
public void handleCallback(PayCallbackRequest request) {
String orderNo = request.getOrderNo();
// 1. 查询订单
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
throw new BizException("订单不存在");
}
// 2. 校验金额
if (!request.getAmount().equals(order.getAmount())) {
throw new BizException("金额不一致");
}
// 3. 幂等判断 - 已处理直接返回
if (OrderStatus.PAID.equals(order.getStatus())) {
log.info("订单已处理,无需重复处理: {}", orderNo);
return;
}
// 4. 更新订单状态(乐观锁)
int rows = orderMapper.updateStatus(
orderNo,
OrderStatus.WAIT_PAY,
OrderStatus.PAID
);
if (rows == 0) {
log.warn("订单状态更新失败,可能是并发导致:{}", orderNo);
return; // 并发请求中,其他线程已更新
}
// 5. 处理业务逻辑(比如发货、发放优惠券等)
bizService.doAfterPayment(order);
}
✅ 乐观锁实现(MyBatis Plus)
数据库表字段 version 开启乐观锁支持:
java
@Version
private Integer version;
更新 SQL 使用 WHERE status = ? AND version = ? 方式,确保并发下只有一次能成功。
🔐 验签逻辑(伪代码)
ini
public boolean verifySignature(PayCallbackRequest request) {
String data = request.toSignString(); // 转换为按顺序拼接的字符串
String signature = request.getSign();
String expected = SignUtil.sign(data, PAYMENT_SECRET_KEY);
return expected.equals(signature);
}
🧱 数据库设计建议
订单表字段:
order_no:订单号(唯一索引)status:订单状态(枚举)amount:订单金额version:乐观锁字段
🧠 最佳实践 Tips:
- 使用本地事务统一管理状态修改和业务处理
- 并发处理用乐观锁优于分布式锁
- 重要日志打全,后期排查容易
- 支付回调要记录原始报文,方便追溯
✅ 总结
支付回调这件事,最忌讳的就是 "想当然"。你以为只会来一次,但现实是 ------ 可能来十次,还不一定是平台发的。
所以:
- 幂等性 是底线
- 安全性 是前提
- 高可用 是保障
咱写代码的风格就一句话:不翻车、不掉坑、能复用、能扩展。
🚀 如果你也在做支付系统
欢迎留言交流,或者直接在评论区贴出你们的回调处理姿势,让我们一起把"幂等"整明白!