做业务系统绕不开支付。最开始我的做法很直接——在订单 Service 里直接调微信支付的 SDK,回调接口里直接写订单状态更新的逻辑,能跑,但代码乱成一团,支付逻辑和业务逻辑死死绑在一起。后来系统里的支付场景越来越多,除了普通订单,还有 VIP、拼团、提现……每种业务都有自己的支付回调处理逻辑,这种写法完全撑不住
于是重新设计了这套支付模块,核心目标只有一个:支付层只负责和微信打交道,业务层只负责处理自己的逻辑,两边互不感知。
一、为什么支付要单独成模块
先说问题。如果不做解耦,支付回调通常长这样:
// 伪代码,反面例子
@PostMapping("/payNotify")
public void notify(String body) {
// 解析微信回调
// 更新订单状态
// 如果是VIP,开通会员
// 如果是拼团,检查成团
// 如果是普通订单,发货...
}
所有业务类型的处理逻辑全堆在一个方法里,每次新增一种支付场景都要改这里,时间长了没人敢动。
更大的问题是:支付和业务的关注点完全不同。支付层关心的是验签、防重放、订单幂等、重复支付退款;业务层关心的是支付成功之后我要做什么。这两件事混在一起,哪件事都做不好。
二、整体架构
拆分之后,整个模块的分层是这样的:
业务层(订单/VIP/拼团...)
│ 调用
▼
PayService(支付服务接口)
│ 实现
▼
WechatPayService(微信支付具体实现)
│
├── 创建支付订单 → 调微信统一下单 API
├── 支付回调处理 → 验签、幂等、更新 PayOrder
│ └── 通过 PayHandler 回调业务层
├── 退款 → 调微信退款 API
├── 退款回调 → 更新 RefundOrder
│ └── 通过 PayHandler 回调业务层
└── 转账/转账回调 → TransferHandler
PayController(接收微信回调,只做解析和分发)
支付层对业务层的感知,全部收敛到两个 Handler 接口:PayHandler 和 TransferHandler。各业务模块自己实现这个接口,支付层通过 Spring 的 ApplicationContext 动态找到对应的实现,不需要任何 if-else。
三、数据模型设计
3.1 PayOrder:统一支付订单表
支付订单和业务订单是两张独立的表,用 bizOrderNo 和 bizType 关联:
public class PayOrder {
private String id; // 支付订单号(也是微信的 outTradeNo)
private String bizOrderNo; // 业务订单号(如商城订单号)
private String bizType; // 业务类型(order/vip/group 等)
private String payChannel; // 支付渠道(JSAPI/NATIVE 等)
private BigDecimal totalAmount;
private Integer status; // 0未支付 1已支付 2失败 3已退款
private String prepayId; // 微信预支付 ID
private String transactionId;// 微信交易单号
private String payerId; // 付款人 openid
private LocalDateTime payTime;
private Integer mainPay; // 是否主支付订单(1是 0否,用于防重复支付)
// ...
}
这里有个关键字段:mainPay。同一个业务订单可能因为网络问题被用户多次点击支付,产生多条 PayOrder 记录,mainPay=1 标记哪一条是"有效"的主支付。这是后面处理重复支付的核心。
3.2 RefundOrder:退款订单表
public class RefundOrder {
private String id; // 退款单号(也是微信的 outRefundNo)
private String outTradeNo; // 关联的支付订单号
private String bizOrderNo;
private String bizType;
private Integer refundAmount;
private Integer status; // 0退款中 1已退款 -1退款失败
private String wxRefundId; // 微信退款单号
private LocalDateTime notifyTime;
// ...
}
3.3 PayNotifyRecord:回调日志表
微信的每一次回调通知都原样记录下来,无论处理成功还是失败:
public class PayNotifyRecord {
private String outTradeNo;
private String rawBody; // 原始报文(未解密)
private Boolean processed; // 是否处理成功
private String errorMsg; // 失败原因
private LocalDateTime receivedTime;
}
这张表的作用是排查问题。线上出了支付问题,第一步就是看这张表,确认微信有没有回调过来、回调内容是什么、有没有处理成功。
四、创建支付订单
业务层调用 PayService.createOrder(),传入业务订单号、业务类型、金额、付款人 openid 等参数:
@Data
@Accessors(chain = true)
public class CreateOrderRequest {
private BigDecimal total; // 金额(单位:分)
private String description; // 商品描述
private String payerId; // 付款人 openid
private String bizOrderNo; // 业务订单号
private String bizType; // 业务类型
private String attach; // 附加数据
}
WechatPayService.createOrder() 的处理流程:
@Transactional(rollbackFor = Exception.class)
public AjaxResult createOrder(CreateOrderRequest req) throws WxPayException {
// 1. 检查该业务订单是否已有成功支付记录,防止重复下单
PayOrder paid = payOrderService.getOne(Wrappers.<PayOrder>lambdaQuery()
.eq(PayOrder::getBizOrderNo, req.getBizOrderNo())
.eq(PayOrder::getBizType, req.getBizType())
.eq(PayOrder::getMainPay, 1)
.eq(PayOrder::getStatus, PayOrderStatus.PAID.getCode()));
if (Objects.nonNull(paid)) {
return AjaxResult.error("该订单已支付");
}
// 2. 把之前该业务订单的 PayOrder 全部标记为非主支付
// (用户可能之前发起过支付但没完成,这里把旧的作废)
payOrderService.update(Wrappers.<PayOrder>lambdaUpdate()
.set(PayOrder::getMainPay, 0)
.eq(PayOrder::getBizOrderNo, req.getBizOrderNo())
.eq(PayOrder::getBizType, req.getBizType()));
// 3. 调微信统一下单
String payOrderId = IdWorker.getIdStr();
WxPayUnifiedOrderV3Result result = wxPayService.unifiedOrderV3(TradeTypeEnum.JSAPI,
new WxPayUnifiedOrderV3Request()
.setOutTradeNo(payOrderId)
.setAmount(new WxPayUnifiedOrderV3Request.Amount()
.setCurrency("CNY").setTotal(req.getTotal().intValue()))
.setAttach(JSONObject.toJSONString(
Map.of("bizOrderNo", req.getBizOrderNo(), "bizType", req.getBizType())))
.setNotifyUrl(...)
.setDescription(req.getDescription())
.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(req.getPayerId())));
// 4. 保存 PayOrder,状态为未支付,标记为主支付
payOrderService.save(new PayOrder()
.setId(payOrderId)
.setMainPay(1)
.setBizOrderNo(req.getBizOrderNo())
.setBizType(req.getBizType())
.setStatus(PayOrderStatus.UNPAID.getCode())
...);
// 5. 返回前端需要的支付参数(timeStamp/nonceStr/package 等)
return AjaxResult.success(result.getPayInfo(TradeTypeEnum.JSAPI, ...));
}
第 2 步把旧的 PayOrder 标记为 mainPay=0 是关键——用户可能多次发起支付,只有最新这次是有效的。
五、支付回调处理
5.1 Controller 层:只做解析和记录
PayController 接收微信回调,不做任何业务处理,只负责三件事:验签、解析报文、记录原始日志。
@PostMapping("payNotify")
public ResponseEntity payNotify(@RequestBody String body) {
// 先建好日志记录,无论后续成功失败都要存
PayNotifyRecord record = new PayNotifyRecord()
.setRawBody(body)
.setProcessed(false)
.setReceivedTime(LocalDateTime.now());
try {
// 从 Header 取签名信息
String signature = request.getHeader("Wechatpay-Signature");
String nonce = request.getHeader("Wechatpay-Nonce");
String serial = request.getHeader("Wechatpay-Serial");
String timestamp = request.getHeader("Wechatpay-Timestamp");
// 验签 + 解密报文,交给 WxJava SDK 处理
WxPayNotifyV3Result notifyResult = wxPayService.parseOrderNotifyV3Result(
body, new SignatureHeader(timestamp, nonce, signature, serial));
WxPayNotifyV3Result.DecryptNotifyResult decryptResult = notifyResult.getResult();
record.setOutTradeNo(decryptResult.getOutTradeNo());
// 根据交易状态分发,只有 SUCCESS 才触发业务处理
switch (decryptResult.getTradeState()) {
case WxPayConstants.WxpayTradeStatus.SUCCESS:
record.setProcessed(true);
payService.payNotify(decryptResult); // 转给 Service 层
break;
case WxPayConstants.WxpayTradeStatus.PAY_ERROR:
log.warn("订单:{} 支付失败", decryptResult.getOutTradeNo());
break;
// 其他状态只记日志...
}
} catch (WxSignTestException e) {
return ResponseEntity.status(500).body(e.getMessage());
} catch (Exception e) {
log.error("支付回调失败:", e);
record.setProcessed(false).setErrorMsg(e.getMessage());
}
// 无论成功失败,都存日志
payNotifyRecordService.save(record);
return record.getProcessed()
? ResponseEntity.ok("success")
: ResponseEntity.status(500).body("fail");
}
注意 @Anonymous 注解——支付回调接口必须对外放开鉴权,微信服务器调过来不会带登录 token。
5.2 Service 层:幂等处理 + 回调业务层
payNotify() 是整个支付回调的核心,要处理几个复杂场景:
@Transactional(rollbackFor = Exception.class)
public String payNotify(WxPayNotifyV3Result.DecryptNotifyResult data) {
PayOrder payOrder = payOrderService.getById(data.getOutTradeNo());
// 场景1:该 PayOrder 已经处理过(幂等保护)
// 微信会在回调失败时重试,必须防止重复处理
if (Objects.equals(payOrder.getStatus(), PayOrderStatus.PAID.getCode())) {
return "成功";
}
// 场景2:查询该业务订单是否已经有另一条成功的主支付记录
boolean exists = payOrderService.exists(Wrappers.<PayOrder>lambdaQuery()
.eq(PayOrder::getBizOrderNo, payOrder.getBizOrderNo())
.eq(PayOrder::getBizType, payOrder.getBizType())
.eq(PayOrder::getMainPay, 1)
.eq(PayOrder::getStatus, PayOrderStatus.PAID.getCode()));
// 场景3:存在主支付且当前是非主支付 → 重复支付,自动退款
if (exists && Objects.equals(payOrder.getMainPay(), 0)) {
refund(new RefundRequest()
.setPayOrderId(payOrder.getId())
.setReason("订单重复支付自动回退"));
return "订单重复支付自动回退";
}
// 场景4:存在主支付且当前也是主支付 → 回调已处理
if (exists && Objects.equals(payOrder.getMainPay(), 1)) {
return "成功";
}
// 正常流程:更新 PayOrder 状态为已支付
payOrderService.update(Wrappers.<PayOrder>lambdaUpdate()
.eq(PayOrder::getId, payOrder.getId())
.set(PayOrder::getStatus, PayOrderStatus.PAID.getCode())
.set(PayOrder::getMainPay, 1)
.set(PayOrder::getTransactionId, data.getTransactionId())
.set(PayOrder::getPayTime,
OffsetDateTime.parse(data.getSuccessTime()).toLocalDateTime()));
// 通过 PayHandler 回调对应业务层,支付层到此结束
Map<String, PayHandler> handlers = applicationContext.getBeansOfType(PayHandler.class);
PayHandler handler = handlers.values().stream()
.filter(h -> h.supports(payOrder.getBizType()))
.findFirst()
.orElseThrow(() -> new RuntimeException(
"没有找到支持的支付回调处理器 bizType=" + payOrder.getBizType()));
handler.payNotify(payOrder.getBizOrderNo(), payOrder.getId());
return "成功";
}
重复支付的处理值得单独说一下。用户在支付过程中,可能因为网络问题或者反复操作,同一个业务订单对应了多个微信支付单。mainPay 字段就是解决这个问题的——先到的那笔标记为主支付,后到的如果发现已经有成功的主支付了,直接触发退款,钱原路退回。
六、PayHandler:支付层和业务层的唯一接口
这是整个解耦设计的核心。支付层对业务层的所有感知,就只有这一个接口:
public interface PayHandler {
// 支持哪种业务类型
boolean supports(String bizType);
// 支付成功回调
void payNotify(String bizOrderNo, String payOrderId);
// 退款成功回调
void refundNotify(String bizOrderNo, String refundOrderId);
}
各业务模块实现这个接口,声明自己负责哪种 bizType,实现支付成功和退款成功之后的业务逻辑:
// 订单业务模块的实现
@Service
public class OrderPayHandler implements PayHandler {
@Override
public boolean supports(String bizType) {
return "order".equals(bizType);
}
@Override
public void payNotify(String bizOrderNo, String payOrderId) {
// 更新订单状态为待发货
// 通知仓库
// 发短信给用户...
}
@Override
public void refundNotify(String bizOrderNo, String refundOrderId) {
// 更新订单状态为已退款
// 通知用户...
}
}
// VIP 业务模块的实现
@Service
public class VipPayHandler implements PayHandler {
@Override
public boolean supports(String bizType) {
return "vip".equals(bizType);
}
@Override
public void payNotify(String bizOrderNo, String payOrderId) {
// 开通会员
// 赠送积分...
}
// ...
}
支付层的代码里没有任何 if bizType == "order" 这样的判断。通过 applicationContext.getBeansOfType(PayHandler.class) 拿到所有实现,用 supports() 筛选对应的处理器,新增一种业务类型只需要新加一个实现类,支付层代码一行不动。
TransferHandler 同理,用于处理微信零钱提现的回调:
public interface TransferHandler {
void transferNotify(String outBillNo);
}
七、退款流程
退款支持两种入口:传 payOrderId 直接退,或者传 bizOrderNo + bizType 查出主支付再退:
@Transactional(rollbackFor = Exception.class)
public AjaxResult refund(RefundRequest req) throws WxPayException {
PayOrder payOrder;
if (Objects.nonNull(req.getPayOrderId())) {
// 直接用支付订单号退(重复支付自动退款走这里)
payOrder = payOrderService.getById(req.getPayOrderId());
req.setRefund(payOrder.getTotalAmount());
req.setBizType(payOrder.getBizType());
req.setBizOrderNo(payOrder.getBizOrderNo());
} else {
// 用业务订单号找主支付
payOrder = payOrderService.getOne(Wrappers.<PayOrder>lambdaQuery()
.eq(PayOrder::getBizOrderNo, req.getBizOrderNo())
.eq(PayOrder::getBizType, req.getBizType())
.eq(PayOrder::getMainPay, 1)
.eq(PayOrder::getStatus, PayOrderStatus.PAID.getCode()));
}
if (Objects.isNull(payOrder)) {
return AjaxResult.error("找不到该订单或订单已退款");
}
// 创建退款单
RefundOrder refundOrder = new RefundOrder()
.setId(IdWorker.getIdStr())
.setOutTradeNo(payOrder.getId())
.setBizOrderNo(req.getBizOrderNo())
.setBizType(req.getBizType())
.setRefundAmount(req.getRefund().intValue())
.setReason(req.getReason())
.setStatus(0); // 退款中
// 调微信退款 API
WxPayRefundV3Result result = wxPayService.refundV3(new WxPayPartnerRefundV3Request()
.setOutTradeNo(payOrder.getId())
.setOutRefundNo(refundOrder.getId())
.setReason(req.getReason())
.setAmount(new WxPayPartnerRefundV3Request.Amount()
.setCurrency("CNY")
.setTotal(payOrder.getTotalAmount().intValue())
.setRefund(req.getRefund().intValue()))
.setNotifyUrl(...));
refundOrder.setWxRefundId(result.getRefundId());
refundOrderService.save(refundOrder);
return AjaxResult.success("退款完成");
}
退款是异步的,调完微信 API 只是"发起退款",真正退款完成是微信回调 /pay/refundNotify 之后才确认。退款回调里同样走 PayHandler.refundNotify() 通知业务层。
八、整体数据流
把完整的支付和退款流程串起来看:
支付流程:
用户点击支付
│
▼
业务层调 PayService.createOrder()
│
▼
WechatPayService 调微信统一下单,保存 PayOrder(status=0, mainPay=1)
│
▼
返回前端支付参数,用户在微信完成支付
│
▼
微信回调 /pay/payNotify
│
▼
Controller 验签解析,记录 PayNotifyRecord,调 PayService.payNotify()
│
▼
幂等检查 → 重复支付检查 → 更新 PayOrder(status=1)
│
▼
PayHandler.payNotify() → 业务层处理(发货/开会员/...)
退款流程:
业务层调 PayService.refund()
│
▼
查找主支付 PayOrder → 调微信退款 API → 保存 RefundOrder(status=0)
│
▼
微信回调 /pay/refundNotify
│
▼
更新 RefundOrder(status=1)→ 更新 PayOrder(status=3)
│
▼
PayHandler.refundNotify() → 业务层处理
九、小结
这套设计解决了几个实际问题:
解耦:支付层和业务层只通过 PayHandler 接口通信,新增业务类型不需要动支付层任何代码。
幂等:微信回调可能重复发送,通过 PayOrder.status 和 mainPay 双重保护,确保同一笔支付只处理一次。
重复支付兜底:用户因为网络问题重复支付时,自动识别并触发退款,钱不会丢在中间状态。
可观测:PayNotifyRecord 记录每一次回调的原始报文和处理结果,线上问题排查有据可查。
这套模块目前在项目里支撑了订单、VIP、拼团等多种支付场景,新增一种业务只需要实现 PayHandler 接口,接入成本很低。
文章评论