秋雨De blog

  • 首页
  • 留言板
  • 关于
  • rss
秋雨De blog
一个技术小白的个人博客
  1. 首页
  2. 未分类
  3. 正文

支付与业务解耦 —— 支付模块的设计与实现

2025年11月1日 1024点热度 0人点赞 0条评论

做业务系统绕不开支付。最开始我的做法很直接——在订单 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 接口,接入成本很低。

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:2026年6月10日

fallrain

种一棵树最好的时间是十年前,其次是现在。

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

fallrain

种一棵树最好的时间是十年前,其次是现在。

文章目录
  • 一、为什么支付要单独成模块
  • 二、整体架构
  • 三、数据模型设计
    • 3.1 PayOrder:统一支付订单表
    • 3.2 RefundOrder:退款订单表
    • 3.3 PayNotifyRecord:回调日志表
  • 四、创建支付订单
  • 五、支付回调处理
    • 5.1 Controller 层:只做解析和记录
    • 5.2 Service 层:幂等处理 + 回调业务层
  • 六、PayHandler:支付层和业务层的唯一接口
  • 七、退款流程
  • 八、整体数据流
  • 九、小结
友情连接
猫饭范文泉博客迎風别葉CODING手艺人ScarSu博友圈
归档
  • 2026 年 6 月
  • 2026 年 3 月
  • 2025 年 11 月
  • 2025 年 5 月
  • 2025 年 4 月
  • 2025 年 3 月
  • 2024 年 12 月
  • 2024 年 10 月
  • 2024 年 5 月
  • 2023 年 2 月
  • 2022 年 11 月
  • 2022 年 3 月
  • 2021 年 12 月
  • 2021 年 8 月
  • 2021 年 5 月
  • 2021 年 4 月
  • 2021 年 3 月
  • 2020 年 12 月
  • 2020 年 11 月
  • 2020 年 8 月
  • 2020 年 5 月
  • 2019 年 12 月
  • 2019 年 3 月

吉ICP备18007356号

吉公网安备22020302000184号

Theme Kratos Made By Seaton Jiang

COPYRIGHT © 2026 秋雨De blog ALL RIGHTS RESERVED