SpringCloud 微服务实战:支付全链路生产级落地(接口对接 + 异步通知 + 订单状态闭环)

在 SpringCloud 微服务架构下,支付集成从来不是简单调个三方接口就完事,它涉及服务通信、分布式事务、安全防护、幂等性设计、高可用保障等多个核心难点。网上 90% 的教程都只是 demo 级实现,根本无法直接落地生产。

这篇博文,我会把多年生产环境落地支付系统的经验毫无保留地分享出来,从架构设计、接口对接、异步通知处理、订单状态闭环,到生产安全、高可用、踩坑实录,全流程讲透,保证你看完就能直接落地到自己的项目里,也帮你避开那些我踩过的深坑。

本文你将学到什么

  1. SpringCloud 微服务架构下,支付系统的生产级架构设计与服务拆分
  2. 微信支付 V3 接口的硬核对接,从签名规范到代码实现,全流程无死角
  3. 支付异步通知的安全、幂等、高可用处理方案,彻底解决重复回调、掉单问题
  4. 基于状态机 + 可靠消息 + 定时兜底的订单状态闭环设计,保证数据最终一致性
  5. 支付系统生产环境的安全防护、高可用保障、监控告警全方案
  6. 10 个生产环境真实踩坑实录与解决方案,帮你少走 2 年弯路

一、业务背景与支付全链路架构总览

1.1 业务场景与服务拆分

本文以通用电商支付场景为基础,基于 SpringCloud 微服务架构做服务拆分,严格遵循单一职责原则,避免支付逻辑与订单业务强耦合:

  • 网关服务:SpringCloud Gateway,负责请求转发、鉴权、限流、日志收集,所有支付相关请求统一入口
  • 订单服务:负责订单的创建、状态管理、生命周期管控,是订单状态的唯一权威数据源
  • 支付服务:核心服务,负责三方支付接口对接、预支付订单生成、支付记录管理、支付结果查询
  • 回调通知服务:独立部署,负责三方支付异步通知的接收、验签、消息分发,与核心业务隔离
  • 公共基础服务:包括 Nacos 配置中心、Sentinel 熔断降级、RocketMQ 消息队列、Redis 分布式缓存

为什么要把回调服务独立拆分?这是我踩过无数坑后总结的核心经验:回调接口是三方支付直接访问的入口,必须保证极致的稳定性和响应速度,独立部署可以避免核心业务的波动影响回调接口的可用性,同时也便于做单独的安全防护和扩容。

1.2 核心技术栈选型

所有选型均为国内互联网公司生产环境主流版本,无冷门组件,无版本兼容问题:

组件 版本 核心作用
SpringBoot 2.7.18 项目基础框架,稳定版,无漏洞
SpringCloud Alibaba 2021.0.1.0 微服务核心套件
Nacos 2.2.3 服务注册发现 + 配置中心
SpringCloud OpenFeign 3.1.8 服务间同步通信
Sentinel 1.8.6 熔断降级、限流、流量控制
RocketMQ 4.9.5 可靠消息投递,异步解耦,最终一致性保障
Redis 6.2.7 分布式锁、幂等性校验、热点数据缓存
MyBatis-Plus 3.5.3.1 ORM 框架,简化数据库操作
微信支付 SDK 0.4.9 微信支付 V3 接口官方 SDK,简化签名验签

1.3 支付全链路架构图

下面是完整的支付全链路架构图,清晰展示从用户下单到支付完成、订单状态同步的全流程:

1.4 支付系统核心设计原则

这是我多年落地支付系统总结的 5 条铁则,任何一条不满足,生产环境都可能出大问题:

  1. 安全第一:支付系统是资金链路的核心,所有接口必须做签名验签、全程 HTTPS、敏感信息加密,杜绝任何安全漏洞
  2. 幂等性优先:所有支付相关的接口,尤其是回调接口,必须做幂等处理,杜绝重复支付、重复回调、重复业务处理
  3. 最终一致性:微服务架构下,不追求强一致性,通过可靠消息 + 兜底补偿,保证订单状态与支付状态的最终一致
  4. 可追溯性:所有支付相关的操作,必须全链路落日志,从下单、预支付、回调、状态更新,每一步都要有据可查
  5. 快速响应与降级:核心接口尤其是回调接口,必须保证极致的响应速度,非核心业务必须异步解耦,支持降级熔断,不影响主流程

二、前置准备:支付对接前的必做事项

很多新手对接支付,上来就写代码,结果踩了无数坑,其实支付对接前的准备工作,比写代码更重要。

2.1 三方支付资质与核心参数准备

以微信支付 V3 为例,你需要提前准备好以下内容,缺一不可:

  1. 资质申请:完成微信商户平台注册、认证,开通 JSAPI/NATIVE 支付权限,绑定对应的公众号 / 小程序 APPID
  2. 核心参数:商户号 (mchid)、APPID、APIv3 密钥、商户 API 证书(私钥 + 公钥)、微信支付平台证书
  3. 回调地址配置 :在商户平台配置支付结果回调地址,必须是公网可访问的 HTTPS 地址,不能带任何参数,不能有登录鉴权拦截,本地调试推荐用内网穿透工具(如 frp、花生壳)
  4. IP 白名单配置:把服务器出口 IP、本地调试公网 IP,添加到商户平台的 IP 白名单里,否则会被接口拦截

2.2 微服务环境与配置规范

  1. 环境隔离:开发、测试、生产环境的支付参数完全隔离,生产环境的密钥绝对不能提交到代码仓库,必须放在 Nacos 配置中心,并且做加密处理
  2. 配置加密:使用 Nacos 的配置加密功能,对 APIv3 密钥、证书私钥等敏感信息做加密,禁止硬编码
  3. HTTP 客户端配置:配置 OkHttp 连接池,设置合理的连接超时、读取超时、写入超时时间,避免接口调用阻塞
  4. 服务间通信配置:OpenFeign 设置超时时间、重试机制,配置 Sentinel 熔断规则,避免服务雪崩

2.3 核心表结构设计(生产级)

表结构设计直接决定了支付系统的稳定性和可维护性,下面是我经过多个项目验证的生产级表结构,包含核心的 3 张表:

sql 复制代码
-- 订单信息表:订单状态的唯一权威数据源
CREATE TABLE `t_order_info` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `order_no` varchar(64) NOT NULL COMMENT '订单号(全局唯一)',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `product_id` bigint NOT NULL COMMENT '商品ID',
  `order_amount` decimal(10,2) NOT NULL COMMENT '订单金额(单位:元)',
  `pay_amount` decimal(10,2) DEFAULT NULL COMMENT '实付金额(单位:元)',
  `order_status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态:0-待支付,1-支付中,2-支付成功,3-支付失败,4-已取消,5-已完成',
  `pay_type` tinyint DEFAULT NULL COMMENT '支付方式:1-微信支付,2-支付宝',
  `transaction_id` varchar(64) DEFAULT NULL COMMENT '三方支付流水号',
  `pay_time` datetime DEFAULT NULL COMMENT '支付完成时间',
  `expire_time` datetime NOT NULL COMMENT '订单过期时间',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单信息表';

-- 支付记录表:每一笔支付请求对应一条记录,与订单是多对一关系(一个订单可能有多笔支付尝试)
CREATE TABLE `t_pay_record` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `pay_no` varchar(64) NOT NULL COMMENT '支付流水号(全局唯一)',
  `order_no` varchar(64) NOT NULL COMMENT '关联订单号',
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `pay_amount` decimal(10,2) NOT NULL COMMENT '支付金额(单位:元)',
  `pay_type` tinyint NOT NULL COMMENT '支付方式:1-微信支付,2-支付宝',
  `pay_status` tinyint NOT NULL DEFAULT '0' COMMENT '支付状态:0-待支付,1-支付中,2-支付成功,3-支付失败,4-已关闭',
  `transaction_id` varchar(64) DEFAULT NULL COMMENT '三方支付流水号',
  `prepay_id` varchar(64) DEFAULT NULL COMMENT '预支付ID',
  `pay_time` datetime DEFAULT NULL COMMENT '支付完成时间',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_pay_no` (`pay_no`),
  KEY `idx_order_no` (`order_no`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付记录表';

-- 支付回调日志表:所有回调请求全量落库,排查问题的核心依据,也是幂等性的第一层保障
CREATE TABLE `t_pay_callback_log` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `order_no` varchar(64) NOT NULL COMMENT '订单号',
  `transaction_id` varchar(64) DEFAULT NULL COMMENT '三方支付流水号',
  `pay_type` tinyint NOT NULL COMMENT '支付方式:1-微信支付,2-支付宝',
  `request_body` text COMMENT '回调请求原始报文',
  `request_header` text COMMENT '回调请求头',
  `sign_verify_result` tinyint NOT NULL COMMENT '签名校验结果:0-失败,1-成功',
  `handle_result` tinyint NOT NULL DEFAULT '0' COMMENT '处理结果:0-待处理,1-处理成功,2-处理失败',
  `response_body` varchar(255) DEFAULT NULL COMMENT '响应给三方的报文',
  `callback_count` int NOT NULL DEFAULT '1' COMMENT '回调次数',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_transaction` (`order_no`,`transaction_id`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付回调日志表';

核心设计说明

  • 所有订单号、支付流水号都有唯一索引,从数据库层面保证唯一性
  • 订单状态和支付状态完全分离,订单状态由订单服务管理,支付状态由支付服务管理,避免耦合
  • 回调日志表设置了order_no+transaction_id的联合唯一索引,从数据库层面杜绝重复回调处理
  • 所有表都有创建时间和更新时间,便于排查问题和对账

三、核心模块一:三方支付接口生产级对接

本文以微信支付 V3 版本为例(目前主流版本,V2 版本已逐步淘汰),讲解支付接口的生产级对接,支付宝对接逻辑完全一致,只是签名规范和接口参数不同。

3.1 支付接口对接核心规范

微信支付 V3 接口的核心是签名机制,这是很多新手踩坑最多的地方,必须严格遵循官方规范:

  1. 签名算法:使用 SHA256 with RSA 签名算法,商户用自己的私钥对请求签名,微信支付用商户公钥验签
  2. 请求头规范 :所有接口请求必须携带Authorization头,包含签名信息、商户号、证书序列号等
  3. HTTPS 协议:所有接口必须使用 HTTPS 协议,禁止 HTTP 请求
  4. 幂等性控制 :统一下单接口使用商户订单号out_trade_no作为幂等号,相同订单号重复请求,不会创建新的预支付订单
  5. 金额规范 :微信支付接口金额单位为,必须是整数,很多新手在这里踩坑,把元当成了分,导致接口报错

3.2 统一下单接口的封装与配置

首先,在 Nacos 配置中心添加微信支付的配置,敏感信息做加密处理:

bash 复制代码
# 微信支付配置
wxpay:
  appid: 你的公众号/小程序APPID
  mchid: 你的商户号
  api-v3-key: 你的APIv3密钥(Nacos加密存储)
  private-key: 你的商户私钥(Nacos加密存储)
  cert-serial-no: 你的商户证书序列号
  platform-cert-serial-no: 微信支付平台证书序列号
  notify-url: 你的支付回调公网地址
  connect-timeout: 5000
  read-timeout: 10000

然后,配置微信支付 SDK 的客户端,单例模式,避免重复创建:

java 复制代码
@Configuration
@Slf4j
public class WxPayConfig {

    @Value("${wxpay.mchid}")
    private String mchid;

    @Value("${wxpay.private-key}")
    private String privateKey;

    @Value("${wxpay.cert-serial-no}")
    private String certSerialNo;

    @Value("${wxpay.api-v3-key}")
    private String apiV3Key;

    @Value("${wxpay.connect-timeout}")
    private int connectTimeout;

    @Value("${wxpay.read-timeout}")
    private int readTimeout;

    @Bean
    public WxPayService wxPayService() {
        log.info("初始化微信支付客户端,商户号:{}", mchid);
        WxPayConfig config = new WxPayConfig();
        config.setAppId(appid);
        config.setMchId(mchid);
        config.setPrivateKey(privateKey);
        config.setCertSerialNo(certSerialNo);
        config.setApiV3Key(apiV3Key);
        // 配置HTTP客户端
        config.setConnectTimeout(connectTimeout);
        config.setReadTimeout(readTimeout);
        // 自动更新平台证书
        config.setAutoUpdateCert(true);
        WxPayService wxPayService = new WxPayServiceImpl();
        wxPayService.setConfig(config);
        return wxPayService;
    }
}

3.3 完整代码实现:从下单到预支付参数返回

整个流程分为 4 步:

  1. 订单服务创建订单,调用支付服务生成预支付订单
  2. 支付服务校验订单信息,生成支付记录,调用微信支付统一下单接口
  3. 微信支付返回预支付参数,支付服务封装后返回给订单服务
  4. 订单服务将预支付参数返回给前端,前端唤起支付
1. 订单服务 - 创建订单并调用支付服务(OpenFeign 接口)
java 复制代码
// Feign接口定义
@FeignClient(name = "pay-service", fallback = PayFeignFallback.class)
public interface PayFeignClient {

    /**
     * 生成预支付订单
     */
    @PostMapping("/api/v1/pay/prepay")
    Result<PrepayVO> createPrepayOrder(@RequestBody PrepayDTO prepayDTO);
}

// 订单服务业务实现
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderInfoMapper orderInfoMapper;

    @Autowired
    private PayFeignClient payFeignClient;

    @Autowired
    private IdGenerator idGenerator; // 雪花算法ID生成器

    @Override
    public Result<PrepayVO> createOrder(OrderCreateDTO dto) {
        // 1. 生成订单号,规则:日期+雪花算法ID,保证全局唯一
        String orderNo = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + idGenerator.nextId();
        // 2. 计算订单金额,校验商品信息
        BigDecimal orderAmount = dto.getProductPrice().multiply(new BigDecimal(dto.getProductNum()));
        // 3. 设置订单过期时间,30分钟未支付自动取消
        LocalDateTime expireTime = LocalDateTime.now().plusMinutes(30);
        // 4. 创建订单,状态为待支付
        OrderInfo orderInfo = OrderInfo.builder()
                .orderNo(orderNo)
                .userId(dto.getUserId())
                .productId(dto.getProductId())
                .orderAmount(orderAmount)
                .orderStatus(0) // 待支付
                .expireTime(expireTime)
                .build();
        orderInfoMapper.insert(orderInfo);
        log.info("订单创建成功,订单号:{}", orderNo);

        // 5. 调用支付服务,生成预支付订单
        PrepayDTO prepayDTO = PrepayDTO.builder()
                .orderNo(orderNo)
                .userId(dto.getUserId())
                .payAmount(orderAmount)
                .payType(dto.getPayType())
                .productDescription(dto.getProductDescription())
                .build();
        Result<PrepayVO> result = payFeignClient.createPrepayOrder(prepayDTO);
        if (!result.isSuccess()) {
            log.error("预支付订单生成失败,订单号:{},原因:{}", orderNo, result.getMessage());
            return Result.fail("预支付订单生成失败");
        }

        // 6. 更新订单状态为支付中
        orderInfo.setOrderStatus(1); // 支付中
        orderInfo.setPayType(dto.getPayType());
        orderInfoMapper.updateById(orderInfo);

        return Result.ok(result.getData());
    }
}
2. 支付服务 - 生成预支付订单核心实现
java 复制代码
@Service
@Slf4j
public class PayServiceImpl implements PayService {

    @Autowired
    private WxPayService wxPayService;

    @Autowired
    private PayRecordMapper payRecordMapper;

    @Autowired
    private IdGenerator idGenerator;

    @Value("${wxpay.notify-url}")
    private String notifyUrl;

    @Override
    public Result<PrepayVO> createPrepayOrder(PrepayDTO dto) {
        String orderNo = dto.getOrderNo();
        // 1. 幂等性校验:同一个订单号,只能生成一个待支付的支付记录
        LambdaQueryWrapper<PayRecord> queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(PayRecord::getOrderNo, orderNo)
                .eq(PayRecord::getPayStatus, 0);
        PayRecord existRecord = payRecordMapper.selectOne(queryWrapper);
        if (existRecord != null) {
            log.warn("订单号{}已存在待支付记录,直接返回", orderNo);
            return Result.ok(buildPrepayVO(existRecord.getPrepayId()));
        }

        // 2. 生成支付流水号
        String payNo = "PAY" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + idGenerator.nextId();
        // 3. 金额转换:元转分,微信支付要求单位为分
        int totalFee = dto.getPayAmount().multiply(new BigDecimal("100")).intValue();
        if (totalFee <= 0) {
            return Result.fail("支付金额必须大于0");
        }

        // 4. 调用微信支付统一下单接口
        WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
        request.setOutTradeNo(payNo); // 商户订单号,用支付流水号,保证唯一
        request.setAppid(wxPayService.getConfig().getAppId());
        request.setMchid(wxPayService.getConfig().getMchId());
        request.setDescription(dto.getProductDescription());
        request.setNotifyUrl(notifyUrl);
        request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(totalFee).setCurrency("CNY"));
        // JSAPI支付需要传openid,这里根据实际支付方式调整
        request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(dto.getOpenid()));

        try {
            log.info("调用微信支付统一下单接口,支付流水号:{},订单号:{}", payNo, orderNo);
            WxPayUnifiedOrderV3Result response = wxPayService.unifiedOrderV3(TradeTypeEnum.JSAPI, request);
            String prepayId = response.getPrepayId();

            // 5. 生成支付记录,落库
            PayRecord payRecord = PayRecord.builder()
                    .payNo(payNo)
                    .orderNo(orderNo)
                    .userId(dto.getUserId())
                    .payAmount(dto.getPayAmount())
                    .payType(dto.getPayType())
                    .payStatus(1) // 支付中
                    .prepayId(prepayId)
                    .build();
            payRecordMapper.insert(payRecord);
            log.info("预支付订单生成成功,支付流水号:{},预支付ID:{}", payNo, prepayId);

            // 6. 封装前端唤起支付需要的参数,返回
            return Result.ok(buildPrepayVO(prepayId));

        } catch (WxPayException e) {
            log.error("微信支付统一下单接口调用失败,支付流水号:{},错误码:{},错误信息:{}", payNo, e.getErrorCode(), e.getErrorMsg(), e);
            return Result.fail("支付接口调用失败:" + e.getErrorMsg());
        } catch (Exception e) {
            log.error("预支付订单生成异常,支付流水号:{}", payNo, e);
            return Result.fail("预支付订单生成异常");
        }
    }

    /**
     * 封装前端唤起支付需要的参数
     */
    private PrepayVO buildPrepayVO(String prepayId) {
        // 这里根据微信支付官方规范,生成前端需要的签名、时间戳、随机数等参数
        Map<String, String> payInfo = wxPayService.getConfig().buildJsapiSign(prepayId);
        PrepayVO vo = new PrepayVO();
        vo.setAppId(payInfo.get("appId"));
        vo.setTimeStamp(payInfo.get("timeStamp"));
        vo.setNonceStr(payInfo.get("nonceStr"));
        vo.setPackageValue(payInfo.get("package"));
        vo.setSignType(payInfo.get("signType"));
        vo.setPaySign(payInfo.get("paySign"));
        return vo;
    }
}

3.4 接口对接高频踩坑与解决方案

  1. 签名错误:90% 的情况是签名串格式不符合规范,或者私钥 / 证书序列号不匹配,解决方案:严格按照官方文档拼接签名串,核对证书序列号,用官方验签工具校验
  2. 订单号重复:微信支付会对相同的 out_trade_no 做幂等处理,重复请求会报错,解决方案:用支付流水号作为 out_trade_no,保证每次请求都是唯一的
  3. IP 白名单拦截:接口调用返回 "无权限访问",解决方案:核对服务器出口 IP,添加到商户平台 IP 白名单
  4. 回调地址配置错误:微信支付回调无法到达,解决方案:确保回调地址是公网 HTTPS 地址,无参数,无登录鉴权拦截,用 Postman 先测试接口是否能正常访问
  5. 金额错误:接口返回 "金额格式错误",解决方案:严格按照微信支付要求,金额单位为分,必须是正整数

四、核心模块二:支付异步通知全链路处理

支付异步通知(回调)是支付系统最核心、最容易出问题的环节,也是生产事故的重灾区。很多人把回调接口当成普通接口来写,结果导致掉单、重复支付、客诉爆炸。

4.1 异步通知的核心痛点与设计原则

核心痛点
  1. 重复回调:三方支付如果没有收到成功响应,会按照固定频率重复发送回调(微信支付是 15s/15s/30s/1m/2m/5m/10m/30m/1h/2h/3h/3h/3h/6h/6h,24h 内共 15 次)
  2. 签名伪造:黑客可能会伪造回调请求,绕过支付流程,直接修改订单状态
  3. 回调丢失:网络波动、服务宕机,可能导致回调请求没有到达,出现掉单
  4. 业务处理超时:回调接口里做复杂业务处理,导致响应超时,三方支付重复回调
  5. 状态不一致:回调处理成功,但订单状态更新失败,导致支付成功但订单还是待支付
设计原则(铁则)
  1. 快速响应:回调接口必须在 100ms 内返回响应,绝对不能在接口里做复杂业务处理,所有业务逻辑必须异步处理
  2. 验签优先:所有回调请求,必须先做签名验签,验签失败直接拒绝,不做任何业务处理
  3. 全链路日志:所有回调请求,无论成功失败,必须全量落库,包括请求头、请求体、验签结果、处理结果、响应内容
  4. 幂等性保障:必须做多层幂等校验,杜绝重复回调导致的重复业务处理
  5. 异常隔离:回调服务必须独立部署,与核心业务隔离,避免其他业务影响回调接口的可用性

下面是异步通知处理的完整流程图:

4.2 回调接口的安全防线:签名验签硬核实现

签名验签是回调接口的第一道,也是最重要的一道安全防线,绝对不能省略,也不能随便写个校验就完事。

微信支付 V3 的验签逻辑:

  1. 从请求头中获取微信支付的证书序列号、签名、时间戳、随机数
  2. 校验证书序列号是否与微信支付平台证书序列号一致,防止伪造证书
  3. 按照官方规范拼接签名串,用微信支付平台公钥对签名进行验签
  4. 验签通过,才能继续处理,否则直接拒绝

验签核心代码实现

java 复制代码
@Service
@Slf4j
public class WxPayCallbackServiceImpl implements WxPayCallbackService {

    @Autowired
    private WxPayService wxPayService;

    @Override
    public boolean verifySign(String signature, String timestamp, String nonce, String requestBody, String serialNo) {
        try {
            // 1. 校验时间戳,防止重放攻击,时间差超过5分钟的请求直接拒绝
            long currentTime = System.currentTimeMillis() / 1000;
            long requestTime = Long.parseLong(timestamp);
            if (Math.abs(currentTime - requestTime) > 300) {
                log.error("回调请求时间戳过期,当前时间:{},请求时间:{}", currentTime, requestTime);
                return false;
            }

            // 2. 按照微信支付规范拼接签名串
            String signStr = timestamp + "\n" + nonce + "\n" + requestBody + "\n";

            // 3. 获取微信支付平台公钥,验签
            boolean verifyResult = wxPayService.verifySign(signStr, serialNo, signature);
            log.info("微信支付回调签名验签结果:{}", verifyResult);
            return verifyResult;

        } catch (NumberFormatException e) {
            log.error("时间戳格式错误", e);
            return false;
        } catch (Exception e) {
            log.error("签名验签异常", e);
            return false;
        }
    }

    @Override
    public WxPayNotifyDTO decryptNotifyData(String requestBody, String apiV3Key) {
        // 解密回调报文中的resource数据,微信支付V3回调报文是加密的
        JSONObject jsonObject = JSON.parseObject(requestBody);
        JSONObject resource = jsonObject.getJSONObject("resource");
        String ciphertext = resource.getString("ciphertext");
        String nonce = resource.getString("nonce");
        String associatedData = resource.getString("associated_data");

        // 用APIv3密钥解密AES-GCM加密的数据
        String decryptData = WxPayUtil.aes256GcmDecrypt(ciphertext, associatedData, nonce, apiV3Key);
        log.info("回调报文解密成功,明文:{}", decryptData);
        return JSON.parseObject(decryptData, WxPayNotifyDTO.class);
    }
}

4.3 三层幂等保障:彻底解决重复回调问题

只靠一层幂等校验是不够的,生产环境必须做三层幂等保障,层层拦截,杜绝重复处理:

第一层:数据库唯一索引拦截

回调日志表设置了order_no+transaction_id的联合唯一索引,相同订单号 + 相同三方支付流水号的回调,数据库会直接拒绝插入,从底层杜绝重复处理。

第二层:Redis 分布式锁拦截

对于同一个订单号的回调,用 Redis 分布式锁做前置校验,只有拿到锁的请求才能继续处理,避免高并发下重复请求穿透到数据库。

java 复制代码
@Override
public boolean checkDuplicateCallback(String orderNo, String transactionId) {
    String lockKey = "pay:callback:lock:" + orderNo + ":" + transactionId;
    // 尝试获取锁,锁超时时间10分钟,足够处理完业务
    boolean lockResult = redissonClient.getLock(lockKey).tryLock();
    if (!lockResult) {
        log.warn("订单号{}回调请求正在处理中,重复请求直接拒绝", orderNo);
        return true;
    }
    // 校验数据库是否已经存在该回调记录
    LambdaQueryWrapper<PayCallbackLog> queryWrapper = Wrappers.lambdaQuery();
    queryWrapper.eq(PayCallbackLog::getOrderNo, orderNo)
            .eq(PayCallbackLog::getTransactionId, transactionId);
    Long count = payCallbackLogMapper.selectCount(queryWrapper);
    return count > 0;
}
第三层:订单状态机前置校验

在更新订单状态的时候,必须做状态前置校验,只有待支付 / 支付中的订单,才能更新为支付成功,已经支付成功的订单,直接返回成功,不做任何处理,这是最后一道兜底防线。

4.4 生产级回调接口完整实现

java 复制代码
/**
 * 微信支付V3回调接口
 * 注意:该接口必须公网可访问,无权限拦截,全程HTTPS
 * 生产环境建议单独部署回调服务,与核心业务隔离
 */
@RestController
@RequestMapping("/api/v1/callback/wxpay")
@Slf4j
public class WxPayCallbackController {

    @Autowired
    private WxPayCallbackService wxPayCallbackService;

    @Autowired
    private PayCallbackLogMapper payCallbackLogMapper;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Value("${wxpay.platform-cert-serial-no}")
    private String platformCertSerialNo;

    @Value("${wxpay.api-v3-key}")
    private String apiV3Key;

    /**
     * 支付结果异步通知接口
     */
    @PostMapping("/notify")
    public ResponseEntity<Map<String, Object>> payNotify(HttpServletRequest request, @RequestHeader Map<String, String> headers) {
        // 构建返回结果,微信支付要求的固定格式
        Map<String, Object> result = new HashMap<>();
        String orderNo = null;
        String transactionId = null;

        try {
            // 1. 获取请求原始报文
            String requestBody = getRequestBody(request);
            log.info("收到微信支付回调,请求头:{},请求体:{}", headers, requestBody);

            // 2. 提取微信支付签名相关头信息
            String serialNo = headers.get("Wechatpay-Serial");
            String signature = headers.get("Wechatpay-Signature");
            String timestamp = headers.get("Wechatpay-Timestamp");
            String nonce = headers.get("Wechatpay-Nonce");

            // 3. 第一层校验:证书序列号校验,防止伪造证书
            if (!platformCertSerialNo.equals(serialNo)) {
                log.error("微信支付回调证书序列号不匹配,预期:{},实际:{}", platformCertSerialNo, serialNo);
                result.put("code", "FAIL");
                result.put("message", "证书序列号不匹配");
                return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
            }

            // 4. 第二层校验:签名验签,核心安全防线
            boolean signVerifyResult = wxPayCallbackService.verifySign(signature, timestamp, nonce, requestBody, serialNo);
            if (!signVerifyResult) {
                log.error("微信支付回调签名校验失败");
                result.put("code", "FAIL");
                result.put("message", "签名校验失败");
                return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
            }

            // 5. 解密回调报文,获取订单信息
            WxPayNotifyDTO notifyDTO = wxPayCallbackService.decryptNotifyData(requestBody, apiV3Key);
            orderNo = notifyDTO.getOutTradeNo(); // 这里的out_trade_no是我们生成的支付流水号
            transactionId = notifyDTO.getTransactionId();
            log.info("微信支付回调报文解密成功,支付流水号:{},三方流水号:{}", orderNo, transactionId);

            // 6. 第三层校验:幂等性校验,防止重复回调处理
            boolean isDuplicate = wxPayCallbackService.checkDuplicateCallback(orderNo, transactionId);
            if (isDuplicate) {
                log.warn("微信支付重复回调,支付流水号:{},直接返回成功", orderNo);
                result.put("code", "SUCCESS");
                result.put("message", "成功");
                return ResponseEntity.ok(result);
            }

            // 7. 记录回调日志,落库持久化
            PayCallbackLog callbackLog = PayCallbackLog.builder()
                    .orderNo(orderNo)
                    .transactionId(transactionId)
                    .payType(1)
                    .requestBody(requestBody)
                    .requestHeader(headers.toString())
                    .signVerifyResult(1)
                    .handleResult(0)
                    .build();
            payCallbackLogMapper.insert(callbackLog);

            // 8. 发送支付结果消息到RocketMQ,异步处理业务逻辑
            // 核心:这里绝对不能做复杂业务处理,必须快速返回
            PayResultMessage message = PayResultMessage.builder()
                    .payNo(orderNo)
                    .transactionId(transactionId)
                    .payAmount(new BigDecimal(notifyDTO.getAmount().getTotal()).divide(new BigDecimal("100")))
                    .payType(1)
                    .payTime(notifyDTO.getSuccessTime())
                    .build();
            rocketMQTemplate.syncSend("pay_result_topic:pay_success_tag", message);
            log.info("支付结果消息发送成功,支付流水号:{}", orderNo);

            // 9. 成功响应,必须返回200,code=SUCCESS,否则微信会重复回调
            result.put("code", "SUCCESS");
            result.put("message", "成功");
            return ResponseEntity.ok(result);

        } catch (Exception e) {
            log.error("微信支付回调处理异常,支付流水号:{}", orderNo, e);
            // 异常情况下,记录失败日志
            if (orderNo != null) {
                PayCallbackLog callbackLog = PayCallbackLog.builder()
                        .orderNo(orderNo)
                        .transactionId(transactionId)
                        .payType(1)
                        .signVerifyResult(1)
                        .handleResult(2)
                        .build();
                payCallbackLogMapper.insert(callbackLog);
            }
            // 只有业务处理失败时才返回FAIL,让微信重试,签名校验失败直接返回400
            result.put("code", "FAIL");
            result.put("message", "系统异常");
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
        }
    }

    /**
     * 获取请求原始报文
     */
    private String getRequestBody(HttpServletRequest request) throws IOException {
        StringBuilder sb = new StringBuilder();
        BufferedReader reader = request.getReader();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        return sb.toString();
    }
}

4.5 回调异常的兜底与重试机制

  1. 回调失败重试:对于处理失败的回调,我们会记录在回调日志表中,用定时任务每隔 5 分钟扫描处理失败的记录,重新发送 MQ 消息,最多重试 10 次,超过次数触发告警
  2. 死信队列处理:RocketMQ 配置死信队列,对于消费失败的消息,进入死信队列,人工介入处理,不会丢失消息
  3. 回调日志归档:每天定时归档回调日志,避免表数据量过大,影响查询性能

五、核心模块三:订单状态同步的闭环设计

支付系统的最终目标,是保证支付状态和订单状态的一致性,也就是用户付了钱,订单必须变成支付成功,绝对不能出现 "支付成功,订单还是待支付" 的掉单情况。

5.1 订单状态机设计:杜绝状态乱跳

订单状态乱跳是掉单的核心原因之一,必须用状态机严格管控订单状态的流转,禁止逆向流转,禁止随意跳状态。

下面是订单状态机流转图:

核心流转规则

  1. 只有待支付状态的订单,才能进入支付中状态
  2. 只有支付中状态的订单,才能变成支付成功 / 支付失败状态
  3. 支付成功状态的订单,不能逆向变回待支付 / 支付中状态
  4. 已取消状态的订单,不能再发起支付
  5. 所有状态变更,必须有明确的触发条件,不能随意修改

5.2 基于 RocketMQ 的可靠消息最终一致性方案

微服务架构下,订单服务和支付服务是两个独立的服务,不能用本地事务保证强一致性,我们采用可靠消息 + 最终一致性方案,这是支付场景最成熟、最稳定的方案。

方案核心逻辑

  1. 回调服务收到支付成功回调,验签通过后,发送支付结果消息到 RocketMQ
  2. RocketMQ 保证消息的可靠投递,只要消息发送成功,一定会被消费者消费
  3. 订单服务作为消费者,接收支付结果消息,更新订单状态
  4. 如果消费失败,RocketMQ 会按照退避策略重试,直到消费成功,或者进入死信队列人工处理
  5. 定时任务主动轮询兜底,就算消息丢失,也能通过主动查询补全状态

5.3 订单状态同步完整流程与代码实现

1. 支付结果消息消费者(订单服务)
java 复制代码
@Component
@Slf4j
@RocketMQMessageListener(
        topic = "pay_result_topic",
        selectorExpression = "pay_success_tag",
        consumerGroup = "order_pay_result_consumer_group",
        consumeMode = ConsumeMode.ORDERLY, // 顺序消费,保证同一个订单的消息按顺序处理
        messageModel = MessageModel.CLUSTERING // 集群消费,保证消息只被消费一次
)
public class PayResultMessageConsumer implements RocketMQListener<MessageExt> {

    @Autowired
    private OrderInfoMapper orderInfoMapper;

    @Autowired
    private PayRecordFeignClient payRecordFeignClient;

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public void onMessage(MessageExt messageExt) {
        String body = new String(messageExt.getBody(), StandardCharsets.UTF_8);
        PayResultMessage message = JSON.parseObject(body, PayResultMessage.class);
        String payNo = message.getPayNo();
        log.info("收到支付成功消息,支付流水号:{}", payNo);

        // 分布式锁,保证同一个订单的消息不会被并发处理
        String lockKey = "order:pay:lock:" + payNo;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            lock.lock(10, TimeUnit.MINUTES);

            // 1. 调用支付服务,查询支付记录,获取订单号
            Result<PayRecordVO> payRecordResult = payRecordFeignClient.getPayRecordByPayNo(payNo);
            if (!payRecordResult.isSuccess()) {
                log.error("支付记录查询失败,支付流水号:{}", payNo);
                throw new RuntimeException("支付记录查询失败");
            }
            PayRecordVO payRecord = payRecordResult.getData();
            String orderNo = payRecord.getOrderNo();

            // 2. 查询订单信息
            OrderInfo orderInfo = orderInfoMapper.selectOne(Wrappers.lambdaQuery(OrderInfo.class).eq(OrderInfo::getOrderNo, orderNo));
            if (orderInfo == null) {
                log.error("订单不存在,订单号:{}", orderNo);
                throw new RuntimeException("订单不存在");
            }

            // 3. 状态机前置校验:只有待支付/支付中的订单,才能更新为支付成功
            if (orderInfo.getOrderStatus() == 2) {
                log.warn("订单已经支付成功,订单号:{},直接返回", orderNo);
                return;
            }
            if (orderInfo.getOrderStatus() != 0 && orderInfo.getOrderStatus() != 1) {
                log.error("订单状态不允许更新为支付成功,订单号:{},当前状态:{}", orderNo, orderInfo.getOrderStatus());
                throw new RuntimeException("订单状态异常");
            }

            // 4. 金额校验:支付金额必须与订单金额一致,防止金额篡改
            if (payRecord.getPayAmount().compareTo(orderInfo.getOrderAmount()) != 0) {
                log.error("支付金额与订单金额不一致,订单号:{},订单金额:{},支付金额:{}", orderNo, orderInfo.getOrderAmount(), payRecord.getPayAmount());
                throw new RuntimeException("支付金额异常");
            }

            // 5. 更新订单状态为支付成功
            orderInfo.setOrderStatus(2);
            orderInfo.setPayAmount(payRecord.getPayAmount());
            orderInfo.setTransactionId(message.getTransactionId());
            orderInfo.setPayTime(message.getPayTime());
            orderInfoMapper.updateById(orderInfo);
            log.info("订单状态更新成功,订单号:{}", orderNo);

            // 6. 发送订单支付成功事件,触发后续业务(库存扣减、短信通知、积分发放等)
            // 后续业务单独做消费者,与订单状态更新解耦,就算失败也不影响主流程

        } catch (Exception e) {
            log.error("支付结果消息消费异常,支付流水号:{}", payNo, e);
            // 抛出异常,让RocketMQ重试
            throw e;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}
2. 支付服务 - 支付记录更新

在发送支付结果消息的同时,支付服务也会消费支付结果消息,更新支付记录的状态,保证支付记录与实际支付状态一致,代码逻辑与订单服务类似,这里不再赘述。

5.4 兜底方案:主动轮询补偿机制

就算回调和 MQ 都没问题,还是可能出现极端情况导致掉单:比如回调请求丢失、MQ 消息丢失、服务宕机导致消息消费失败。所以必须有主动轮询补偿机制,这是掉单的最后一道兜底防线。

方案实现

  1. 用 XXL-Job 定时任务,每隔 5 分钟扫描支付中状态、且创建时间超过 1 分钟的支付记录
  2. 对于这些支付记录,调用微信支付的订单查询接口,查询实际支付状态
  3. 如果查询到支付成功,但是本地支付状态还是支付中,就发送支付结果消息,补全状态
  4. 如果查询到支付失败,就更新支付状态为支付失败,同步更新订单状态
  5. 对于超过 30 分钟还在支付中的订单,自动关闭,更新订单状态为已取消

核心代码片段

java 复制代码
@XxlJob("payStatusSyncJob")
public void payStatusSyncJob() {
    log.info("开始执行支付状态同步定时任务");
    // 查询支付中状态,且创建时间超过1分钟的支付记录
    LocalDateTime createTime = LocalDateTime.now().minusMinutes(1);
    LambdaQueryWrapper<PayRecord> queryWrapper = Wrappers.lambdaQuery();
    queryWrapper.eq(PayRecord::getPayStatus, 1)
            .le(PayRecord::getCreateTime, createTime)
            .last("limit 1000"); // 每次最多处理1000条,避免压力过大
    List<PayRecord> payRecordList = payRecordMapper.selectList(queryWrapper);
    if (CollectionUtils.isEmpty(payRecordList)) {
        log.info("没有需要同步的支付记录");
        return;
    }

    for (PayRecord payRecord : payRecordList) {
        try {
            // 调用微信支付订单查询接口
            WxPayOrderQueryV3Result result = wxPayService.queryOrderV3(null, payRecord.getPayNo());
            String tradeState = result.getTradeState();

            // 支付成功,补全状态
            if ("SUCCESS".equals(tradeState)) {
                log.info("订单查询到支付成功,支付流水号:{}", payRecord.getPayNo());
                PayResultMessage message = PayResultMessage.builder()
                        .payNo(payRecord.getPayNo())
                        .transactionId(result.getTransactionId())
                        .payAmount(new BigDecimal(result.getAmount().getTotal()).divide(new BigDecimal("100")))
                        .payType(1)
                        .payTime(result.getSuccessTime())
                        .build();
                rocketMQTemplate.syncSend("pay_result_topic:pay_success_tag", message);
            }

            // 支付失败/已关闭,更新状态
            if ("CLOSED".equals(tradeState) || "PAYERROR".equals(tradeState)) {
                log.info("订单支付失败/已关闭,支付流水号:{},状态:{}", payRecord.getPayNo(), tradeState);
                payRecord.setPayStatus("CLOSED".equals(tradeState) ? 4 : 3);
                payRecordMapper.updateById(payRecord);
                // 发送支付失败消息,更新订单状态
                rocketMQTemplate.syncSend("pay_result_topic:pay_fail_tag", payRecord.getPayNo());
            }

        } catch (Exception e) {
            log.error("支付状态同步异常,支付流水号:{}", payRecord.getPayNo(), e);
        }
    }
    log.info("支付状态同步定时任务执行完成");
}

5.5 分布式事务的选型与避坑

很多人会问,为什么不用 Seata 的 AT 模式做分布式事务?这里我结合自己的经验,给大家讲清楚选型的避坑点:

  1. 支付场景不适合强一致性分布式事务:支付流程涉及三方支付接口,是外部系统,无法纳入 Seata 的分布式事务管理,强一致性根本无法实现
  2. AT 模式的性能问题:Seata AT 模式需要全局锁,高并发场景下性能很差,支付系统是核心链路,不能接受性能损耗
  3. 可靠消息最终一致性是支付场景的最优解:支付场景允许短暂的状态不一致(秒级),只要最终一致即可,可靠消息 + 定时兜底的方案,性能好、稳定性高、运维简单,是国内互联网公司支付系统的主流方案
  4. 避坑提醒:绝对不要在回调接口里用 OpenFeign 同步调用订单服务更新状态,一旦订单服务宕机或者网络波动,就会导致回调响应超时,三方支付重复回调,最终出现重复处理的问题

六、生产环境安全与高可用保障

支付系统是公司的核心资金链路,安全和高可用是重中之重,绝对不能只实现功能就完事。

6.1 支付安全的 5 道核心防线

  1. 全程 HTTPS 协议:所有支付相关的接口,包括回调接口,必须使用 HTTPS 协议,禁止 HTTP 请求,防止数据被抓包篡改
  2. 双向签名验签:请求三方支付接口用商户私钥签名,回调接口用三方支付公钥验签,所有接口必须做签名校验,杜绝伪造请求
  3. 敏感信息加密存储:商户密钥、API 密钥、证书私钥等敏感信息,必须加密存储在配置中心,禁止硬编码,禁止提交到代码仓库,生产环境密钥只有运维人员有权限查看
  4. 接口限流与防刷:用 Sentinel 对支付接口做限流,防止恶意请求刷接口;对同一个用户、同一个订单号的支付请求,做频率限制,防止重复发起支付
  5. 全链路日志与审计:所有支付相关的操作,全链路落日志,包括请求参数、响应结果、操作人、操作时间,日志至少保留 6 个月,便于对账和审计;所有订单状态变更,必须有操作记录,可追溯

6.2 高可用集群与容灾设计

  1. 服务集群部署:支付服务、回调服务、订单服务,都必须集群部署,至少 3 个节点,避免单点故障
  2. 多支付渠道容灾:对接至少 2 个支付渠道(微信 + 支付宝),配置渠道切换开关,当一个支付渠道不可用时,自动切换到另一个渠道,保证支付功能可用
  3. 熔断降级策略:用 Sentinel 配置熔断规则,当三方支付接口调用失败率超过阈值时,触发熔断,返回兜底提示,避免服务雪崩;非核心业务(比如短信、积分)支持降级,不影响主支付流程
  4. 数据库高可用:MySQL 配置主从分离、读写分离,支付相关的表用 InnoDB 引擎,行级锁,避免表锁;Redis 配置集群模式,避免缓存单点故障
  5. 机房容灾:有条件的公司,配置多机房部署,同城双活,避免单机房故障导致支付系统不可用

6.3 全链路监控与实时告警方案

生产环境的支付系统,必须有完善的监控和告警,出问题能第一时间发现,而不是等用户投诉才知道。

核心监控指标

  1. 支付接口指标:预支付接口的成功率、响应时间、QPS,失败率超过 1% 立即告警
  2. 回调接口指标:回调接口的成功率、响应时间、验签失败次数,验签失败次数超过 5 次立即告警
  3. 订单状态指标:支付成功但订单状态未更新的订单数量,超过 0 立即告警
  4. MQ 消费指标:支付结果消息的消费成功率、堆积数量,消息堆积超过 100 条立即告警
  5. 系统资源指标:服务的 CPU、内存、磁盘使用率,数据库的连接数、慢查询数量

告警方式:企业微信 / 钉钉机器人、短信、电话告警,核心告警必须有电话通知,避免错过故障处理时间。


七、踩坑实录:生产环境 10 个真实坑与解决方案

这部分是我多年来在生产环境踩过的真实的坑,每一个都导致过线上问题,分享出来帮大家避坑:

坑 1:回调接口业务处理超时,导致微信重复回调 8 次,用户收到 8 条短信

事故背景 :刚上线的时候,我把订单状态更新、库存扣减、短信发送、积分发放全写在了回调接口里,高峰期接口响应时间达到了 8s,而微信支付的回调超时时间是 5s,微信认为回调失败,连续重复回调了 8 次,导致用户收到了 8 条支付成功的短信,客诉直接炸了。解决方案

  1. 回调接口只做 3 件事:验签、幂等校验、发 MQ,其余所有业务逻辑全部异步处理,接口响应时间直接降到了 50ms 以内;
  2. 短信、积分等非核心业务,单独做消费者,与订单状态更新解耦,就算失败也不影响主流程;
  3. 回调服务单独部署,与核心业务隔离,避免其他业务影响回调接口的响应时间。

坑 2:订单号重复,导致用户 A 支付的钱,到了用户 B 的订单上

事故背景 :最开始用时间戳 + 随机数生成订单号,高并发下出现了重复的订单号,导致用户 A 支付成功后,订单状态更新到了用户 B 的订单上,出现了资损事故。解决方案

  1. 用雪花算法生成订单号,保证全局唯一,订单号规则:日期 + 雪花算法 ID,避免重复;
  2. 订单表、支付记录表都给订单号、支付流水号加唯一索引,从数据库层面杜绝重复;
  3. 预支付订单生成前,先校验订单号是否已经存在,避免重复生成。

坑 3:没有做金额校验,黑客通过篡改前端金额,用 1 分钱买了 1000 元的商品

事故背景 :最开始预支付接口直接用前端传过来的金额,没有和数据库里的订单金额做校验,黑客通过抓包篡改了请求里的金额,用 1 分钱买了 1000 元的商品,导致公司资损。解决方案

  1. 预支付接口绝对不能信任前端传过来的金额,必须从数据库里查询订单的真实金额,用订单金额生成预支付订单;
  2. 回调处理的时候,必须校验支付金额和订单金额是否一致,不一致的直接拒绝处理,触发告警;
  3. 所有金额相关的计算,全部在后端做,前端只做展示,不做计算。

坑 4:Redis 分布式锁过期,导致重复处理回调请求

事故背景 :最开始用 Redis 的 setnx 做分布式锁,锁超时时间设置了 30s,高峰期业务处理时间超过了 30s,锁过期了,另一个请求拿到了锁,导致同一个回调请求被重复处理,用户重复收到了积分。解决方案

  1. 用 Redisson 的分布式锁,自带看门狗机制,业务没处理完,锁会自动续期,不会过期;
  2. 锁的超时时间设置为业务处理最大时间的 3 倍,留足冗余;
  3. 数据库唯一索引做兜底,就算锁过期了,数据库也会拒绝重复插入。

坑 5:回调接口加了登录鉴权拦截,导致微信支付回调全部被拦截

事故背景 :项目里有全局的登录鉴权拦截器,所有接口都需要登录才能访问,回调接口也被拦截了,微信支付回调全部返回 401,微信认为回调失败,重复发送,导致大量订单掉单。解决方案

  1. 回调接口加入白名单,绕过登录鉴权拦截器,只做签名验签;
  2. 回调接口单独部署,不经过全局鉴权拦截,只做自己的安全校验;
  3. 上线前用 Postman 测试回调接口,确保公网可以正常访问,没有拦截。

坑 6:微信支付证书过期,导致所有支付接口调用失败

事故背景 :微信支付商户证书有效期是 1 年,到期前没有更换,导致所有支付接口调用失败,支付功能完全不可用,影响了全平台的交易。解决方案

  1. 配置证书过期时间监控,提前 30 天触发告警,提醒更换证书;
  2. 用微信支付的自动更新证书功能,自动更新平台证书,避免手动更换;
  3. 证书更换前,先在测试环境验证,确保没有问题再上线生产环境。

坑 7:没有做状态机前置校验,导致支付成功的订单被改成了已取消

事故背景 :订单取消的定时任务,没有做状态前置校验,把已经支付成功的订单,改成了已取消,导致用户付了钱,订单却被取消了,大量客诉。解决方案

  1. 严格按照状态机规则,所有订单状态变更,必须做前置校验,只有符合流转规则的才能变更;
  2. 订单取消操作,只能取消待支付状态的订单,支付中、支付成功的订单,绝对不能取消;
  3. 所有状态变更操作,都加乐观锁,用 version 字段控制,避免并发修改。

坑 8:三方支付接口限流,导致高峰期预支付订单生成失败

事故背景 :大促高峰期,预支付接口 QPS 过高,触发了微信支付的接口限流,导致大量用户无法生成预支付订单,无法支付。解决方案

  1. 用 Sentinel 对预支付接口做限流,控制 QPS 在三方支付的限流阈值以内;
  2. 配置重试机制,用指数退避算法,接口限流失败后,自动重试 2 次;
  3. 大促前提前和三方支付报备,申请提升限流阈值。

坑 9:敏感信息硬编码,提交到了 GitHub 公共仓库,导致密钥泄露

事故背景 :开发的时候,把商户密钥、API 密钥硬编码在了代码里,不小心提交到了 GitHub 公共仓库,被爬虫爬取,导致密钥泄露,还好及时发现,更换了密钥,没有造成资损。解决方案

  1. 所有敏感信息,全部放在配置中心,禁止硬编码在代码里;
  2. 配置.gitignore 文件,把包含敏感信息的配置文件排除,禁止提交到代码
  1. 配置.gitignore 文件,把包含敏感信息的配置文件排除,禁止提交到代码仓库;
  2. 用 Nacos 的配置加密功能,对敏感配置做加密,就算配置泄露,没有密钥也解不开;
  3. 生产环境的密钥只有运维人员有权限查看,开发人员用测试环境的密钥,环境完全隔离。

坑 10:定时任务重复执行,导致重复查询三方支付接口触发限流

事故背景 :支付状态同步的定时任务,最初用了简单的@Scheduled注解,集群部署时 3 个节点同时执行,同一时间大量请求三方支付查询接口,直接触发了微信支付的限流规则,反而导致正常的状态同步请求被拦截。解决方案

  1. 用 XXL-Job 分布式调度平台,配置任务为 "单机执行" 模式,保证同一时间只有一个节点执行定时任务;
  2. 任务执行前先获取 Redis 分布式锁,只有拿到锁的节点才能执行,执行完成后释放锁;
  3. 每次任务处理的数据量做上限控制(比如每次最多 1000 条),避免一次性查询和处理过多数据,给数据库和三方接口造成压力。

八、总结与拓展

总结

SpringCloud 微服务架构下的支付系统落地,核心不是 "实现功能",而是 "保证安全、稳定、一致"。从接口对接的签名规范,到异步通知的幂等设计,再到订单状态的闭环兜底,每一个环节都需要经过生产环境的验证,任何一个细节的疏漏,都可能引发资损或客诉。

本文分享的架构设计、代码实现、踩坑方案,都是我在多个电商、SaaS 项目中经过实战检验的成熟方案,大家可以直接复用,也可以根据自己的业务场景做调整。

拓展

  1. 对账系统 :支付系统上线后,必须配套建设日终对账系统,每天定时拉取三方支付的对账单,与本地支付记录、订单记录做逐笔核对,保证资金流水一致,这是资损防控的最后一道防线;
  2. 退款功能:退款是支付的逆向流程,核心逻辑与支付类似,也需要接口对接、异步通知、状态同步,架构上可以复用支付的设计模式,但需要注意退款的资金风险控制(比如退款金额不能超过实付金额、退款订单状态校验);
  3. 分账功能 :如果是平台型业务(如电商、外卖),还需要对接三方支付的分账接口,涉及分账比例配置、分账时机选择、分账回退等复杂逻辑,需要单独设计分账模块;
  4. 跨境支付:如果有跨境业务,需要对接跨境支付渠道(如 PayPal、Stripe),涉及汇率转换、外汇合规申报、跨境退款等,逻辑比国内支付更复杂,需要提前了解目标市场的监管要求。
相关推荐
爱吃烤鸡翅的酸菜鱼1 小时前
Spring Cloud Eureka 服务注册与发现实战详解:从原理到高可用集群搭建
java·spring·spring cloud·eureka
一叶飘零_sweeeet2 小时前
微服务接口设计全解:RESTful/RPC 规范、兼容方案与生产级实战
微服务·rpc·restful
splage2 小时前
Spring Framework 中文官方文档
java·后端·spring
刘 大 望3 小时前
MCP详细介绍以及IDE和Spring AI中应用
java·ide·人工智能·spring·ai·aigc·ai编程
daidaidaiyu13 小时前
一文学习 Spring 声明式事务源码全流程总结
java·spring
一叶飘零_sweeeet14 小时前
服务注册发现深度拆解:Nacos vs Eureka 核心原理、架构选型与生产落地
微服务·云原生·eureka·nacos·架构·注册中心
一路向北·重庆分伦15 小时前
01:服务注册与发现+配置中心-Nacos+Eureka
spring cloud
一路向北·重庆分伦17 小时前
04:服务网关Spring Cloud Gateway
spring cloud
彭于晏Yan17 小时前
Spring AI(二):入门使用
java·spring boot·spring·ai