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),涉及汇率转换、外汇合规申报、跨境退款等,逻辑比国内支付更复杂,需要提前了解目标市场的监管要求。
相关推荐
我学上瘾了3 小时前
Spring Cloud的前世今生
后端·spring·spring cloud
朝新_8 小时前
【Spring AI 】核心知识体系梳理:从入门到实战
java·人工智能·spring
谁怕平生太急9 小时前
面试题记录:在线数据迁移
java·数据库·spring
云烟成雨TD11 小时前
Spring AI Alibaba 1.x 系列【18】Hook 接口和四大抽象类
java·人工智能·spring
StackNoOverflow12 小时前
Spring Cloud的注册中心和配置中心(Nacos)
后端·spring cloud
Flittly13 小时前
【SpringSecurity新手村系列】(2)整合 MyBatis 实现数据库认证
java·安全·spring·springboot·安全架构
devilnumber14 小时前
java中Redisson ,jedis,Lettuce和Spring Data Redis的四种深度对比和优缺点详解
java·redis·spring
砍材农夫15 小时前
spring-ai 第十一mcp server调用入门(stdio协议)
人工智能·spring·microsoft
码农阿豪15 小时前
一次 AI 调用 15 万 Token 只花了 $0.058?彻底搞懂 Token、缓存读、补全计费机制!(附完整架构图)
人工智能·spring·缓存
awljwlj15 小时前
黑马点评复习—缓存相关【包含可能的问题和基础知识复习】
java·后端·spring·缓存