秋雨De blog

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

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

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

一、为什么要做“支付模块解耦”

在传统的业务系统中,支付逻辑往往与业务逻辑高度耦合。例如商城下单、拼团支付、会员充值等模块都在传统的业务系统中,支付逻辑往往与具体业务强绑定。
以电商系统为例,订单模块通常直接负责下单、发起支付、更新支付状态、处理回调等流程。
甚至在一些项目中,开发者会把“支付状态”字段直接写入 t_order 表,把“支付成功”事件直接在订单代码中触发。

这种设计在早期阶段看似方便,但在系统逐渐复杂后,会暴露出以下典型问题:

1. 可维护性差,改一处动全身

由于支付流程深度耦合在订单代码中,一旦订单结构或流程发生调整(如引入优惠券、拼团、退款逻辑),就可能导致支付相关逻辑需要整体修改。
这种修改成本极高,也容易引发连锁问题,增加测试与上线风险。

2. 重复开发,难以复用

不同业务线往往都有支付需求,比如:

  • 商城的商品订单;
  • 会员充值;
  • 押金支付;
  • 内容付费等。

如果每个业务都单独实现支付逻辑,就会出现大量重复代码——每个模块都要重新写“下单、回调、退款”流程。
不仅开发效率低,还可能因为不同模块逻辑不一致导致财务对账混乱。

3. 无法灵活支持多支付渠道

当系统需要新增支付方式(如从微信支付扩展到支付宝、银联、余额支付等),如果支付逻辑紧耦合在业务中,将面临大规模代码重构。
相反,若支付是独立模块,只需在模块内部扩展“渠道适配层”即可对外透明。

4. 对账与监控复杂

分散在各业务系统中的支付数据,往往格式不统一、状态不一致,
导致财务统计、对账系统难以统一汇总,也无法形成清晰的支付日志与风控链路。

二、支付模块整体架构设计

2.1 模块职责定位

支付模块的核心职责是:

  • 提供统一的支付、退款、转账、查询等接口。
  • 处理微信/支付宝的异步回调。
  • 与业务系统通过回调接口解耦(如订单、拼团、充值等)。
  • 支持多业务类型(bizType)扩展,形成“支付中台”。

2.2 模块结构

pay
├─config
│      PayConfig.java                    # 微信支付参数与SDK Bean配置
│
├─controller
│      PayController.java                # 对外暴露接口:下单、回调、退款
│
├─domain
│      PayOrder.java                     # 支付订单表
│      RefundOrder.java                  # 退款订单表
│      PayNotifyRecord.java              # 回调记录表
│
├─mapper
│      PayOrderMapper.java
│      RefundOrderMapper.java
│      PayNotifyRecordMapper.java        # MyBatis-Plus数据访问层
│
├─request
│      CreateOrderRequest.java           # 下单参数
│      QueryOrderRequest.java            # 查询参数
│      RefundRequest.java                # 退款参数
│      TransferRequest.java              # 企业转账参数
│
└─service
    │  PayService.java                   # 核心支付接口(统一入口)
    │  PayOrderService.java              # 支付订单业务逻辑
    │  RefundOrderService.java           # 退款逻辑
    │  PayNotifyRecordService.java       # 回调记录逻辑
    │  PayHandler.java                   # 业务回调接口
    │  TransferHandler.java              # 企业转账回调接口
    │
    └─impl
            WechatPayService.java        # 微信支付具体实现
            PayOrderServiceImpl.java
            RefundOrderServiceImpl.java
            PayNotifyRecordServiceImpl.java

三、核心设计理念:解耦、复用、可插拔

3.1 统一的入口:PayService 接口

所有支付能力(微信、支付宝、银联等)统一实现 PayService 接口:

public interface PayService {
    AjaxResult createOrder(CreateOrderRequest request);
    String payNotify(WxPayNotifyV3Result.DecryptNotifyResult notify);
    AjaxResult refund(RefundRequest request);
    String refundNotify(WxPayRefundNotifyV3Result.DecryptNotifyResult notify);
    AjaxResult queryOrder(QueryOrderRequest request);
    AjaxResult transfer(TransferRequest request);
    String transferNotify(TransferBillsNotifyResult.DecryptNotifyResult notify);
}

3.2 微信支付实现类:WechatPayService

该类基于 binarywang/weixin-java-pay 实现,封装了完整的微信支付闭环。

package com.pindan.pay.service.impl;

import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.binarywang.wxpay.bean.merchanttransfer.TransferCreateRequest;
import com.github.binarywang.wxpay.bean.merchanttransfer.TransferCreateResult;
import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
import com.github.binarywang.wxpay.bean.request.WxPayPartnerRefundV3Request;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsRequest;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.TransferService;
import com.github.binarywang.wxpay.service.WxPayService;
import com.pindan.common.core.domain.AjaxResult;
import com.pindan.common.enums.PayOrderStatus;
import com.pindan.pay.domain.PayOrder;
import com.pindan.pay.domain.RefundOrder;
import com.pindan.pay.request.*;
import com.pindan.pay.service.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.*;

/**
 * 微信支付模块核心实现类
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class WechatPayService implements PayService {

    private final PayOrderService payOrderService;
    private final RefundOrderService refundOrderService;
    private final WxPayService wxPayService;
    private final WxPayConfig wxPayConfig;
    private final ApplicationContext applicationContext;

    /**
     * 创建支付订单(统一下单)
     * - 首先检查该业务订单是否已支付
     * - 否则创建新的支付单,并标记 mainPay=1
     * - 若存在历史记录则先全部 mainPay=0(防止重复支付)
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult createOrder(CreateOrderRequest createOrderRequest) throws WxPayException {
        log.info("创建订单:{}", createOrderRequest);
        String payOrderId = IdWorker.getIdStr();

        // 判断业务订单是否已支付(幂等校验)
        PayOrder payOrder = payOrderService.getOne(Wrappers.<PayOrder>lambdaQuery()
                .eq(PayOrder::getBizOrderNo, createOrderRequest.getBizOrderNo())
                .eq(PayOrder::getBizType, createOrderRequest.getBizType())
                .eq(PayOrder::getMainPay, 1)
                .eq(PayOrder::getStatus, PayOrderStatus.PAID.getCode())
        );

        if (Objects.nonNull(payOrder)) {
            log.info("该订单已支付:{},{}", createOrderRequest.getBizOrderNo(), createOrderRequest.getBizType());
            return AjaxResult.error("该订单已支付");
        }

        // 将历史支付记录的 mainPay 置为 0,确保本次新建为唯一主支付单
        payOrderService.update(Wrappers.<PayOrder>lambdaUpdate()
                .set(PayOrder::getMainPay, 0)
                .eq(PayOrder::getBizOrderNo, createOrderRequest.getBizOrderNo())
                .eq(PayOrder::getBizType, createOrderRequest.getBizType()));

        // 调用微信统一下单接口
        WxPayUnifiedOrderV3Result wxPayUnifiedOrderV3Result = wxPayService.unifiedOrderV3(
                TradeTypeEnum.JSAPI,
                new WxPayUnifiedOrderV3Request()
                        .setAppid(wxPayConfig.getAppId())
                        .setMchid(wxPayConfig.getMchId())
                        .setAmount(new WxPayUnifiedOrderV3Request.Amount().setCurrency("CNY").setTotal(createOrderRequest.getTotal().intValue()))
                        .setOutTradeNo(payOrderId)
                        .setAttach(JSONObject.toJSONString(Map.of("bizOrderNo", createOrderRequest.getBizOrderNo(), "bizType", createOrderRequest.getBizType())))
                        .setNotifyUrl("%s/%s".formatted(wxPayConfig.getNotifyUrl(), "/pay/payNotify"))
                        .setDescription(createOrderRequest.getDescription())
                        .setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(createOrderRequest.getPayerId()))
        );

        // 保存支付订单
        payOrderService.save(new PayOrder()
                .setId(payOrderId)
                .setMainPay(1)
                .setBizOrderNo(createOrderRequest.getBizOrderNo())
                .setBizType(createOrderRequest.getBizType())
                .setPayChannel("JSAPI")
                .setNotifyUrl("%s%s".formatted(wxPayConfig.getNotifyUrl(), "/pay/payNotify"))
                .setPrepayId(wxPayUnifiedOrderV3Result.getPrepayId())
                .setPayerId(createOrderRequest.getPayerId())
                .setTotalAmount(createOrderRequest.getTotal())
                .setCurrency("CNY")
                .setStatus(PayOrderStatus.UNPAID.getCode())
                .setCreateTime(LocalDateTime.now()));

        // 返回前端调起支付所需参数
        Object payInfo = wxPayUnifiedOrderV3Result.getPayInfo(
                TradeTypeEnum.JSAPI, wxPayConfig.getAppId(), wxPayConfig.getMchId(), wxPayConfig.getPrivateKey());
        return AjaxResult.success(payInfo);
    }

    /**
     * 微信支付回调通知
     * - 校验幂等:已支付订单直接返回成功
     * - 若检测到重复支付(mainPay=0)→ 自动退款
     * - 若首次支付成功 → 标记当前单为主单(mainPay=1)
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String payNotify(WxPayNotifyV3Result.DecryptNotifyResult payNotifyData) throws WxPayException {
        log.info("支付回调:{}", payNotifyData);
        PayOrder payOrder = payOrderService.getById(payNotifyData.getOutTradeNo());

        // 幂等:订单已支付直接返回
        if (Objects.equals(payOrder.getStatus(), PayOrderStatus.PAID.getCode())) {
            log.info("该订单已支付:{}", payOrder.getId());
            return "成功";
        }

        // 检查是否已有主支付单成功支付
        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()));

        // 若检测到重复支付(非主单),触发自动退款
        if (exists && Objects.equals(payOrder.getMainPay(), 0)) {
            refund(new RefundRequest().setPayOrderId(payOrder.getId()).setReason("订单重复支付自动回退"));
            log.info("订单重复支付自动回退:{},{},{}", payOrder.getId(), payOrder.getBizOrderNo(), payOrder.getBizType());
            return "订单重复支付自动回退";
        }

        // 若主单已处理,直接返回
        if (exists && Objects.equals(payOrder.getMainPay(), 1)) {
            log.info("回调已处理:{},{},{}", payOrder.getId(), payOrder.getBizOrderNo(), payOrder.getBizType());
            return "成功";
        }

        // 清空历史主单标识
        payOrderService.update(Wrappers.<PayOrder>lambdaUpdate()
                .set(PayOrder::getMainPay, 0)
                .eq(PayOrder::getBizOrderNo, payOrder.getBizOrderNo())
                .eq(PayOrder::getBizType, payOrder.getBizType()));

        // 标记当前单为主单并更新状态
        payOrderService.update(Wrappers.<PayOrder>lambdaUpdate()
                .eq(PayOrder::getId, payOrder.getId())
                .set(PayOrder::getStatus, PayOrderStatus.PAID.getCode())
                .set(PayOrder::getMainPay, 1)
                .set(PayOrder::getTransactionId, payNotifyData.getTransactionId())
                .set(PayOrder::getPayTime, OffsetDateTime.parse(payNotifyData.getSuccessTime()).toLocalDateTime()));

        // 业务解耦:通过 PayHandler 回调具体业务模块(例如订单系统、VIP充值等)
        Map<String, PayHandler> paymentCallbackHandlerMap = applicationContext.getBeansOfType(PayHandler.class);
        paymentCallbackHandlerMap.values().stream()
                .filter(e -> e.supports(payOrder.getBizType()))
                .findFirst()
                .ifPresentOrElse(
                        handler -> handler.payNotify(payOrder.getBizOrderNo(), payOrder.getId()),
                        () -> { throw new RuntimeException("没有找到支持的支付回调处理器 bizType=" + payOrder.getBizType()); }
                );
        return "成功";
    }

    /**
     * 发起退款
     * - 支持手动退款与重复支付自动退款
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult refund(RefundRequest refundRequest) throws WxPayException {
        log.info("退款:{}", refundRequest);
        PayOrder payOrder;

        // 若指定了 payOrderId,则直接按主键查询
        if (Objects.nonNull(refundRequest.getPayOrderId())) {
            payOrder = payOrderService.getById(refundRequest.getPayOrderId());
            refundRequest.setRefund(payOrder.getTotalAmount());
            refundRequest.setBizType(payOrder.getBizType());
            refundRequest.setBizOrderNo(payOrder.getBizOrderNo());
        } else {
            // 否则通过 bizOrderNo + bizType + mainPay 查询主单
            payOrder = payOrderService.getOne(Wrappers.<PayOrder>lambdaQuery()
                    .eq(PayOrder::getBizOrderNo, refundRequest.getBizOrderNo())
                    .eq(PayOrder::getBizType, refundRequest.getBizType())
                    .eq(PayOrder::getMainPay, 1)
                    .eq(PayOrder::getStatus, PayOrderStatus.PAID.getCode()));
        }

        if (Objects.isNull(payOrder)) {
            log.info("找不到该订单或订单已退款:{}", refundRequest);
            return AjaxResult.error("找不到该订单或订单已退款");
        }

        // 创建退款记录
        RefundOrder refundOrder = new RefundOrder()
                .setId(IdWorker.getIdStr())
                .setRefundAmount(refundRequest.getRefund().intValue())
                .setBizOrderNo(refundRequest.getBizOrderNo())
                .setBizType(refundRequest.getBizType())
                .setCreateTime(LocalDateTime.now())
                .setReason(refundRequest.getReason())
                .setStatus(0)
                .setOutTradeNo(payOrder.getId());

        // 调用微信退款接口
        WxPayRefundV3Result wxPayRefundV3Result = wxPayService.refundV3(new WxPayPartnerRefundV3Request()
                .setOutTradeNo(payOrder.getId())
                .setOutRefundNo(refundOrder.getId())
                .setReason(refundRequest.getReason())
                .setAmount(new WxPayPartnerRefundV3Request.Amount()
                        .setCurrency("CNY")
                        .setTotal(payOrder.getTotalAmount().intValue())
                        .setRefund(refundRequest.getRefund().intValue()))
                .setNotifyUrl("%s%s".formatted(wxPayConfig.getNotifyUrl(), "/pay/refundNotify")));

        refundOrder.setWxRefundId(wxPayRefundV3Result.getRefundId());
        refundOrderService.save(refundOrder);
        return AjaxResult.success("退款完成");
    }

    /**
     * 退款回调通知
     * - 更新退款状态
     * - 通知对应业务模块(PayHandler)
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String refundNotify(WxPayRefundNotifyV3Result.DecryptNotifyResult wxPayRefundNotifyV3Result) {
        log.info("退款回调:{}", wxPayRefundNotifyV3Result);
        RefundOrder refundOrder = refundOrderService.getById(wxPayRefundNotifyV3Result.getOutRefundNo());

        if (!Objects.equals(refundOrder.getStatus(), 0)) {
            log.info("回调已处理:{}", wxPayRefundNotifyV3Result);
            return "已处理";
        }

        // 更新退款状态
        refundOrderService.update(Wrappers.<RefundOrder>lambdaUpdate()
                .eq(RefundOrder::getId, refundOrder.getId())
                .set(RefundOrder::getStatus, 1)
                .set(RefundOrder::getNotifyTime, LocalDateTime.now()));

        PayOrder payOrder = payOrderService.getById(refundOrder.getOutTradeNo());

        // 若为重复支付退款,不更新主单状态
        if (Objects.equals(payOrder.getMainPay(), 0)) {
            log.warn("重复支付,已退款: payOrderId={}, bizOrderNo={}", payOrder.getId(), payOrder.getBizOrderNo());
            return "重复支付退款";
        }

        // 更新主支付单状态为已退款
        payOrderService.update(Wrappers.<PayOrder>lambdaUpdate()
                .set(PayOrder::getStatus, PayOrderStatus.REFUNDED.getCode())
                .eq(PayOrder::getId, refundOrder.getOutTradeNo()));

        // 调用业务层退款回调处理
        Map<String, PayHandler> paymentCallbackHandlerMap = applicationContext.getBeansOfType(PayHandler.class);
        paymentCallbackHandlerMap.values().stream()
                .filter(e -> e.supports(refundOrder.getBizType()))
                .findFirst()
                .ifPresentOrElse(
                        handler -> handler.refundNotify(refundOrder.getBizOrderNo(), refundOrder.getId()),
                        () -> { throw new RuntimeException("没有找到支持的退款回调处理器 bizType=" + refundOrder.getBizType()); }
                );

        log.info("回调完成:{}", wxPayRefundNotifyV3Result);
        return "成功";
    }

    /**
     * 查询支付订单状态
     * - 查询主支付单并调用微信API
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult queryOrder(QueryOrderRequest queryOrderRequest) throws WxPayException {
        PayOrder payOrder = payOrderService.getOne(Wrappers.<PayOrder>lambdaQuery()
                .eq(PayOrder::getBizOrderNo, queryOrderRequest.getBizOrderNo())
                .eq(PayOrder::getBizType, queryOrderRequest.getBizType())
                .eq(PayOrder::getMainPay, 1));
        return AjaxResult.success(wxPayService.queryOrderV3(payOrder.getTransactionId(), payOrder.getId()));
    }

    /**
     * 企业转账(提现、打款等)
     */
    @Override
    public AjaxResult transfer(TransferRequest transferRequest) throws WxPayException {
        TransferService transferService = wxPayService.getTransferService();
        TransferBillsResult transferBillsResult = transferService.transferBills(TransferBillsRequest.newBuilder()
                .appid(wxPayConfig.getAppId())
                .outBillNo(transferRequest.getOutBatchNo())
                .transferSceneId("1005")
                .notifyUrl("%s/%s".formatted(wxPayConfig.getNotifyUrl(), "/pay/transferNotify"))
                .openid(transferRequest.getOpenId())
                .userName(transferRequest.getUserName())
                .transferAmount(transferRequest.getTransferAmount().intValue())
                .transferRemark(transferRequest.getTransferRemark())
                .build());
        return AjaxResult.success("", transferBillsResult.getPackageInfo());
    }

    /**
     * 企业转账回调通知
     */
    @Override
    public String transferNotify(TransferBillsNotifyResult.DecryptNotifyResult decryptNotifyResult) throws WxPayException {
        Map<String, TransferHandler> transferHandlerMap = applicationContext.getBeansOfType(TransferHandler.class);
        transferHandlerMap.forEach((key, transferHandler) -> transferHandler.transferNotify(decryptNotifyResult.getOutBillNo()));
        return "成功";
    }
}
public interface PayHandler {
    boolean supports(String bizType);
    void payNotify(String bizOrderNo, String payOrderId);
    void refundNotify(String bizOrderNo, String refundOrderId);
}

支付模块通过 Spring 容器动态查找并调用:

Map<String, PayHandler> handlers = applicationContext.getBeansOfType(PayHandler.class);
handlers.values().stream()
    .filter(h -> h.supports(payOrder.getBizType()))
    .findFirst()
    .ifPresent(h -> h.payNotify(payOrder.getBizOrderNo(), payOrder.getId()));

四、数据模型设计

4.1 支付订单表 pay_order

CREATE TABLE `pay_order` (
  `id` varchar(255) NOT NULL COMMENT '支付主键',
  `biz_order_no` varchar(255) NOT NULL COMMENT '业务订单号',
  `biz_type` varchar(255) NOT NULL COMMENT '业务类型(order/vip/group等)',
  `pay_channel` varchar(255) NOT NULL COMMENT '支付渠道(WX_JSAPI/WX_NATIVE/ALI_APP等)',
  `total_amount` int NOT NULL COMMENT '支付金额(单位分)',
  `currency` varchar(255) DEFAULT 'CNY' COMMENT '币种',
  `status` tinyint DEFAULT '0' COMMENT '状态:0未支付,1已支付,2失败,3已关闭',
  `transaction_id` varchar(255) DEFAULT NULL COMMENT '微信/支付宝交易号',
  `payer_id` varchar(255) DEFAULT NULL COMMENT '付款人ID',
  `main_pay` tinyint(1) DEFAULT NULL COMMENT '是否为主支付订单',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='统一支付订单表';

五、main_pay 机制:重复支付场景下的数据一致性保障

在支付模块的实际运行中,一个业务订单可能会触发多次支付请求。
例如:

  • 用户多次点击“立即支付”;
  • 用户切换支付方式(如从微信换成支付宝);
  • 某些业务系统在异常重试时重复调用创建支付接口。

如果不加控制,这些请求会生成多条支付记录——它们都指向同一个业务订单,
最终可能导致 多笔支付成功、重复扣款、回调多次触发 等严重问题。

为了解决这一问题,本系统引入了 main_pay 机制,确保同一业务订单在任何情况下都能保证支付逻辑唯一且幂等。


设计思路

main_pay 字段代表“是否为主支付单”,其本质是对“重复支付”的防御性设计。

字段名类型含义
main_paytinyint(1)是否为主支付单(1=主支付,0=非主支付)

我们约定:

  • 主支付单(main_pay=1):代表该业务订单的唯一、有效支付记录;
  • 非主支付单(main_pay=0):代表重复触发的支付记录,仅作记录或自动退款处理。

也就是说,对于同一个 bizOrderNo + bizType:

  • 系统中 最多只有一条 main_pay=1 且 status=PAID 的记录;
  • 其他重复的支付请求即便被微信/支付宝受理,也会在回调阶段被自动回退。

核心逻辑实现

在创建支付订单时,系统会执行如下判断:

PayOrder payOrder = payOrderService.getOne(
    Wrappers.<PayOrder>lambdaQuery()
        .eq(PayOrder::getBizOrderNo, createOrderRequest.getBizOrderNo())
        .eq(PayOrder::getBizType, createOrderRequest.getBizType())
        .eq(PayOrder::getMainPay, 1)
        .eq(PayOrder::getStatus, PayOrderStatus.PAID.getCode())
);
if (Objects.nonNull(payOrder)) {
    return AjaxResult.error("该订单已支付");
}

若业务订单未支付成功,则会执行:

payOrderService.update(Wrappers.<PayOrder>lambdaUpdate()
    .set(PayOrder::getMainPay, 0)
    .eq(PayOrder::getBizOrderNo, createOrderRequest.getBizOrderNo())
    .eq(PayOrder::getBizType, createOrderRequest.getBizType()));

这一步会把旧的支付单全部降级为 main_pay=0,然后重新生成一个主支付单。
从而保证系统中“始终只有一条主单”在生效。


回调阶段的防重逻辑

在支付回调 (payNotify) 阶段:

  • 系统会判断是否已经存在一条 main_pay=1 且状态为已支付的记录;
  • 若存在,且当前回调对应的支付单为 main_pay=0,则自动执行退款操作;
  • 否则,将当前支付单提升为主单(main_pay=1),并更新状态为已支付。

关键逻辑如下:

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())
);

if (exists && Objects.equals(payOrder.getMainPay(), 0)) {
    refund(new RefundRequest().setPayOrderId(payOrder.getId()).setReason("订单重复支付自动回退"));
    return "订单重复支付自动回退";
}

这一机制实现了对重复支付的“幂等性兜底保护”:
无论支付平台如何重试、用户如何多次操作,最终系统状态都保持一致。

六、设计思考与经验总结

  1. 解耦优先:支付模块绝不直接依赖业务系统。
  2. 幂等性与回调安全性:重复支付、重复回调必须设计良好。
  3. 业务可插拔:PayHandler 模式让新增业务“零侵入”。
  4. 日志与异常清晰:所有回调日志都必须可追溯。
  5. 从模块到中台的演进路径清晰:
    • 抽象通用接口
    • 定义统一数据结构
    • 实现可插拔业务扩展点

七、总结

通过将支付模块从各业务系统中彻底抽离,并设计为一个通用能力层,我们成功实现了支付体系的统一化、标准化与高扩展性。

首先,模块化的设计让支付成为一个 统一入口。
无论是订单支付、会员充值还是押金冻结,所有业务都能通过相同的接口与流程完成支付闭环,极大地提升了代码复用率和维护效率。

其次,支付模块与业务系统之间的彻底解耦,使得新增业务场景不再需要改动核心支付逻辑。
业务方只需定义自己的 BizType 与回调处理器,即可快速接入支付能力。
这不仅降低了系统间的耦合度,也让整体架构更加清晰、稳健。

最后,从演进角度来看,这样的架构已经具备了支付中台化的潜质。
在未来,随着系统规模的扩大,我们只需在此模块上继续扩展多支付通道、统一账务、风控与清结算能力,就能顺利演化为一个独立的“支付中台”,为多业务线提供统一、安全、可观测的资金服务。

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

fallrain

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

点赞
< 上一篇

文章评论

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

fallrain

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

文章目录
  • 一、为什么要做“支付模块解耦”
    • 1. 可维护性差,改一处动全身
    • 2. 重复开发,难以复用
    • 3. 无法灵活支持多支付渠道
    • 4. 对账与监控复杂
  • 二、支付模块整体架构设计
    • 2.1 模块职责定位
    • 2.2 模块结构
  • 三、核心设计理念:解耦、复用、可插拔
    • 3.1 统一的入口:PayService 接口
    • 3.2 微信支付实现类:WechatPayService
  • 四、数据模型设计
    • 4.1 支付订单表 pay_order
  • 五、main_pay 机制:重复支付场景下的数据一致性保障
    • 设计思路
    • 核心逻辑实现
    • 回调阶段的防重逻辑
  • 六、设计思考与经验总结
  • 七、总结
友情连接
猫饭范文泉博客迎風别葉CODING手艺人ScarSu
归档
  • 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 © 2025 秋雨De blog ALL RIGHTS RESERVED