SpringCloud 微服务支付全链路生产级落地
在 SpringCloud 微服务架构下,支付集成涉及服务通信、分布式事务、安全防护、幂等性设计、高可用保障等多个核心难点。本文从架构设计、接口对接、异步通知处理、订单状态闭环,到生产安全、高可用、踩坑实录,全流程讲解。
一、业务背景与支付全链路架构总览
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 支付系统核心设计原则
- 安全第一:支付系统是资金链路的核心,所有接口必须做签名验签、全程 HTTPS、敏感信息加密
- 幂等性优先:所有支付相关的接口,尤其是回调接口,必须做幂等处理,杜绝重复支付、重复回调
- 最终一致性:微服务架构下,通过可靠消息 + 兜底补偿,保证订单状态与支付状态的最终一致
- 可追溯性:所有支付相关的操作,必须全链路落日志,每一步都要有据可查
- 快速响应与降级:核心接口尤其是回调接口,必须保证极致的响应速度,非核心业务必须异步解耦
二、前置准备:支付对接前的必做事项
2.1 三方支付资质与核心参数准备
以微信支付 V3 为例,需要提前准备好以下内容:
- 资质申请:完成微信商户平台注册、认证,开通 JSAPI/NATIVE 支付权限
- 核心参数:商户号 (mchid)、APPID、APIv3 密钥、商户 API 证书(私钥 + 公钥)、微信支付平台证书
- 回调地址配置:必须是公网可访问的 HTTPS 地址,不能带任何参数,不能有登录鉴权拦截
- IP 白名单配置:把服务器出口 IP、本地调试公网 IP,添加到商户平台的 IP 白名单里
2.2 微服务环境与配置规范
- 环境隔离:开发、测试、生产环境的支付参数完全隔离,生产环境的密钥绝对不能提交到代码仓库
- 配置加密:使用 Nacos 的配置加密功能,对 APIv3 密钥、证书私钥等敏感信息做加密
- HTTP 客户端配置:配置 OkHttp 连接池,设置合理的连接超时、读取超时、写入超时时间
- 服务间通信配置:OpenFeign 设置超时时间、重试机制,配置 Sentinel 熔断规则
2.3 核心表结构设计(生产级)
表结构设计直接决定了支付系统的稳定性和可维护性,包含核心的 3 张表:
-- 订单信息表:订单状态的唯一权威数据源
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 COMMENT ,
(`id`),
KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`)
) ENGINEInnoDB CHARSETutf8mb4 utf8mb4_unicode_ci COMMENT;
`t_pay_record` (
`id` AUTO_INCREMENT COMMENT ,
`pay_no` () COMMENT ,
`order_no` () COMMENT ,
`user_id` COMMENT ,
`pay_amount` (,) COMMENT ,
`pay_type` tinyint COMMENT ,
`pay_status` tinyint COMMENT ,
`transaction_id` () COMMENT ,
`prepay_id` () COMMENT ,
`pay_time` datetime COMMENT ,
`create_time` datetime COMMENT ,
`update_time` datetime COMMENT ,
(`id`),
KEY `uk_pay_no` (`pay_no`),
KEY `idx_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`)
) ENGINEInnoDB CHARSETutf8mb4 utf8mb4_unicode_ci COMMENT;
`t_pay_callback_log` (
`id` AUTO_INCREMENT COMMENT ,
`order_no` () COMMENT ,
`transaction_id` () COMMENT ,
`pay_type` tinyint COMMENT ,
`request_body` text COMMENT ,
`request_header` text COMMENT ,
`sign_verify_result` tinyint COMMENT ,
`handle_result` tinyint COMMENT ,
`response_body` () COMMENT ,
`callback_count` COMMENT ,
`create_time` datetime COMMENT ,
`update_time` datetime COMMENT ,
(`id`),
KEY `uk_order_transaction` (`order_no`,`transaction_id`),
KEY `idx_create_time` (`create_time`)
) ENGINEInnoDB CHARSETutf8mb4 utf8mb4_unicode_ci COMMENT;
核心设计说明:
所有订单号、支付流水号都有唯一索引,从数据库层面保证唯一性;订单状态和支付状态完全分离;回调日志表设置了
order_no+transaction_id的联合唯一索引,从数据库层面杜绝重复回调。
三、核心模块一:三方支付接口生产级对接
本文以微信支付 V3 版本为例,讲解支付接口的生产级对接。
3.1 支付接口对接核心规范
微信支付 V3 接口的核心是签名机制:
- 签名算法:使用 SHA256 with RSA 签名算法
- 请求头规范:所有接口请求必须携带
Authorization头 - HTTPS 协议:所有接口必须使用 HTTPS 协议
- 幂等性控制:统一下单接口使用商户订单号作为幂等号
- 金额规范:微信支付接口金额单位为分,必须是整数
3.2 统一下单接口的封装与配置
首先,在 Nacos 配置中心添加微信支付的配置,敏感信息做加密处理:
# 微信支付配置
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 的客户端,单例模式:
@Configuration
@Slf4j
public class WxPayConfig {
@Value("${wxpay.mchid}")
private String mchid;
// ... 其他配置注入
@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);
config.setConnectTimeout(connectTimeout);
config.setReadTimeout(readTimeout);
config.setAutoUpdateCert(true);
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(config);
return wxPayService;
}
}
3.3 完整代码实现:从下单到预支付参数返回
整个流程分为 4 步:
- 订单服务创建订单,调用支付服务生成预支付订单
- 支付服务校验订单信息,生成支付记录,调用微信支付统一下单接口
- 微信支付返回预支付参数,支付服务封装后返回给订单服务
- 订单服务将预支付参数返回给前端,前端唤起支付
1. 订单服务 - 创建订单并调用支付服务(OpenFeign 接口)
// 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;
@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.builder()
.orderNo(orderNo)
.userId(dto.getUserId())
.productId(dto.getProductId())
.orderAmount(orderAmount)
.orderStatus()
.expireTime(expireTime)
.build();
orderInfoMapper.insert(orderInfo);
log.info(, orderNo);
PrepayDTO.builder()
.orderNo(orderNo)
.userId(dto.getUserId())
.payAmount(orderAmount)
.payType(dto.getPayType())
.productDescription(dto.getProductDescription())
.build();
Result<PrepayVO> result = payFeignClient.createPrepayOrder(prepayDTO);
(!result.isSuccess()) {
log.error(, orderNo, result.getMessage());
Result.fail();
}
orderInfo.setOrderStatus();
orderInfo.setPayType(dto.getPayType());
orderInfoMapper.updateById(orderInfo);
Result.ok(result.getData());
}
}
2. 支付服务 - 生成预支付订单核心实现
@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();
(totalFee <= ) {
Result.fail();
}
();
request.setOutTradeNo(payNo);
request.setAppid(wxPayService.getConfig().getAppId());
request.setMchid(wxPayService.getConfig().getMchId());
request.setDescription(dto.getProductDescription());
request.setNotifyUrl(notifyUrl);
request.setAmount( .Amount().setTotal(totalFee).setCurrency());
request.setPayer( .Payer().setOpenid(dto.getOpenid()));
{
log.info(, payNo, orderNo);
wxPayService.unifiedOrderV3(TradeTypeEnum.JSAPI, request);
response.getPrepayId();
PayRecord.builder()
.payNo(payNo)
.orderNo(orderNo)
.userId(dto.getUserId())
.payAmount(dto.getPayAmount())
.payType(dto.getPayType())
.payStatus()
.prepayId(prepayId)
.build();
payRecordMapper.insert(payRecord);
log.info(, payNo, prepayId);
Result.ok(buildPrepayVO(prepayId));
} (WxPayException e) {
log.error(, payNo, e.getErrorCode(), e.getErrorMsg(), e);
Result.fail( + e.getErrorMsg());
} (Exception e) {
log.error(, payNo, e);
Result.fail();
}
}
PrepayVO {
Map<String, String> payInfo = wxPayService.getConfig().buildJsapiSign(prepayId);
();
vo.setAppId(payInfo.get());
vo.setTimeStamp(payInfo.get());
vo.setNonceStr(payInfo.get());
vo.setPackageValue(payInfo.get());
vo.setSignType(payInfo.get());
vo.setPaySign(payInfo.get());
vo;
}
}
3.4 接口对接高频踩坑与解决方案
- 签名错误:严格按照官方文档拼接签名串,核对证书序列号
- 订单号重复:用支付流水号作为 out_trade_no,保证每次请求都是唯一的
- IP 白名单拦截:核对服务器出口 IP,添加到商户平台 IP 白名单
- 回调地址配置错误:确保回调地址是公网 HTTPS 地址,无参数,无登录鉴权拦截
- 金额错误:严格按照微信支付要求,金额单位为分,必须是正整数
四、核心模块二:支付异步通知全链路处理
支付异步通知(回调)是支付系统最核心、最容易出问题的环节。
4.1 异步通知的核心痛点与设计原则
核心痛点
- 重复回调:三方支付如果没有收到成功响应,会按照固定频率重复发送回调
- 签名伪造:黑客可能会伪造回调请求
- 回调丢失:网络波动、服务宕机,可能导致回调请求没有到达
- 业务处理超时:回调接口里做复杂业务处理,导致响应超时
- 状态不一致:回调处理成功,但订单状态更新失败
设计原则(铁则)
- 快速响应:回调接口必须在 100ms 内返回响应,绝对不能在接口里做复杂业务处理
- 验签优先:所有回调请求,必须先做签名验签,验签失败直接拒绝
- 全链路日志:所有回调请求,无论成功失败,必须全量落库
- 幂等性保障:必须做多层幂等校验,杜绝重复回调导致的重复业务处理
- 异常隔离:回调服务必须独立部署,与核心业务隔离
下面是异步通知处理的完整流程图:

4.2 回调接口的安全防线:签名验签硬核实现
微信支付 V3 的验签逻辑:
- 从请求头中获取微信支付的证书序列号、签名、时间戳、随机数
- 校验证书序列号是否与微信支付平台证书序列号一致
- 按照官方规范拼接签名串,用微信支付平台公钥对签名进行验签
验签核心代码实现:
@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;
} (Exception e) {
log.error(, e);
;
}
}
WxPayNotifyDTO {
JSON.parseObject(requestBody);
jsonObject.getJSONObject();
resource.getString();
resource.getString();
resource.getString();
WxPayUtil.aes256GcmDecrypt(ciphertext, associatedData, nonce, apiV3Key);
log.info(, decryptData);
JSON.parseObject(decryptData, WxPayNotifyDTO.class);
}
}
4.3 三层幂等保障:彻底解决重复回调问题
第一层:数据库唯一索引拦截
回调日志表设置了 order_no+transaction_id 的联合唯一索引,相同订单号 + 相同三方支付流水号的回调,数据库会直接拒绝插入。
第二层:Redis 分布式锁拦截
对于同一个订单号的回调,用 Redis 分布式锁做前置校验。
@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 生产级回调接口完整实现
/**
* 微信支付 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");
headers.get();
headers.get();
headers.get();
(!platformCertSerialNo.equals(serialNo)) {
log.error(, platformCertSerialNo, serialNo);
result.put(, );
result.put(, );
ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
wxPayCallbackService.verifySign(signature, timestamp, nonce, requestBody, serialNo);
(!signVerifyResult) {
log.error();
result.put(, );
result.put(, );
ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
wxPayCallbackService.decryptNotifyData(requestBody, apiV3Key);
orderNo = notifyDTO.getOutTradeNo();
transactionId = notifyDTO.getTransactionId();
log.info(, orderNo, transactionId);
wxPayCallbackService.checkDuplicateCallback(orderNo, transactionId);
(isDuplicate) {
log.warn(, orderNo);
result.put(, );
result.put(, );
ResponseEntity.ok(result);
}
PayCallbackLog.builder()
.orderNo(orderNo)
.transactionId(transactionId)
.payType()
.requestBody(requestBody)
.requestHeader(headers.toString())
.signVerifyResult()
.handleResult()
.build();
payCallbackLogMapper.insert(callbackLog);
PayResultMessage.builder()
.payNo(orderNo)
.transactionId(transactionId)
.payAmount( (notifyDTO.getAmount().getTotal()).divide( ()))
.payType()
.payTime(notifyDTO.getSuccessTime())
.build();
rocketMQTemplate.syncSend(, message);
log.info(, orderNo);
result.put(, );
result.put(, );
ResponseEntity.ok(result);
} (Exception e) {
log.error(, orderNo, e);
(orderNo != ) {
PayCallbackLog.builder()
.orderNo(orderNo)
.transactionId(transactionId)
.payType()
.signVerifyResult()
.handleResult()
.build();
payCallbackLogMapper.insert(callbackLog);
}
result.put(, );
result.put(, );
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
String IOException {
();
request.getReader();
String line;
((line = reader.readLine()) != ) {
sb.append(line);
}
sb.toString();
}
}
4.5 回调异常的兜底与重试机制
- 回调失败重试:定时任务扫描处理失败的记录,重新发送 MQ 消息
- 死信队列处理:RocketMQ 配置死信队列,消费失败的消息进入死信队列
- 回调日志归档:每天定时归档回调日志
五、核心模块三:订单状态同步的闭环设计
5.1 订单状态机设计:杜绝状态乱跳
订单状态乱跳是掉单的核心原因之一,必须用状态机严格管控订单状态的流转。
下面是订单状态机流转图:

核心流转规则:
- 只有待支付状态的订单,才能进入支付中状态
- 只有支付中状态的订单,才能变成支付成功 / 支付失败状态
- 支付成功状态的订单,不能逆向变回待支付 / 支付中状态
- 已取消状态的订单,不能再发起支付
5.2 基于 RocketMQ 的可靠消息最终一致性方案
采用可靠消息 + 最终一致性方案:
- 回调服务收到支付成功回调,验签通过后,发送支付结果消息到 RocketMQ
- RocketMQ 保证消息的可靠投递
- 订单服务作为消费者,接收支付结果消息,更新订单状态
- 如果消费失败,RocketMQ 会按照退避策略重试
- 定时任务主动轮询兜底
5.3 订单状态同步完整流程与代码实现
1. 支付结果消息消费者(订单服务)
@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()) {
();
}
payRecordResult.getData();
payRecord.getOrderNo();
orderInfoMapper.selectOne(Wrappers.lambdaQuery(OrderInfo.class).eq(OrderInfo::getOrderNo, orderNo));
(orderInfo == ) {
();
}
(orderInfo.getOrderStatus() == ) {
log.warn(, orderNo);
;
}
(orderInfo.getOrderStatus() != && orderInfo.getOrderStatus() != ) {
();
}
(payRecord.getPayAmount().compareTo(orderInfo.getOrderAmount()) != ) {
();
}
orderInfo.setOrderStatus();
orderInfo.setPayAmount(payRecord.getPayAmount());
orderInfo.setTransactionId(message.getTransactionId());
orderInfo.setPayTime(message.getPayTime());
orderInfoMapper.updateById(orderInfo);
log.info(, orderNo);
} (Exception e) {
log.error(, payNo, e);
e;
} {
(lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
5.4 兜底方案:主动轮询补偿机制
- 用 XXL-Job 定时任务,每隔 5 分钟扫描支付中状态、且创建时间超过 1 分钟的支付记录
- 调用微信支付的订单查询接口,查询实际支付状态
- 如果查询到支付成功,补全状态;如果查询到支付失败,更新状态
- 对于超过 30 分钟还在支付中的订单,自动关闭
核心代码片段:
@XxlJob("payStatusSyncJob")
public void payStatusSyncJob() {
log.info("开始执行支付状态同步定时任务");
LocalDateTime createTime = LocalDateTime.now().minusMinutes(1);
LambdaQueryWrapper<PayRecord> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.eq(PayRecord::getPayStatus, 1).le(PayRecord::getCreateTime, createTime).last("limit 1000");
List<PayRecord> payRecordList = payRecordMapper.selectList(queryWrapper);
if (CollectionUtils.isEmpty(payRecordList)) {
return;
}
for (PayRecord payRecord : payRecordList) {
try {
WxPayOrderQueryV3Result result = wxPayService.queryOrderV3(null, payRecord.getPayNo());
String tradeState = result.getTradeState();
if ("SUCCESS".equals(tradeState)) {
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)) {
payRecord.setPayStatus(.equals(tradeState) ? : );
payRecordMapper.updateById(payRecord);
rocketMQTemplate.syncSend(, payRecord.getPayNo());
}
} (Exception e) {
log.error(, payRecord.getPayNo(), e);
}
}
}
5.5 分布式事务的选型与避坑
- 支付场景不适合强一致性分布式事务:涉及外部系统,无法纳入 Seata 管理
- AT 模式的性能问题:Seata AT 模式需要全局锁,高并发场景下性能很差
- 可靠消息最终一致性是支付场景的最优解:性能好、稳定性高、运维简单
六、生产环境安全与高可用保障
6.1 支付安全的 5 道核心防线
- 全程 HTTPS 协议:禁止 HTTP 请求
- 双向签名验签:请求三方支付接口用商户私钥签名,回调接口用三方支付公钥验签
- 敏感信息加密存储:密钥加密存储在配置中心,禁止硬编码
- 接口限流与防刷:用 Sentinel 对支付接口做限流
- 全链路日志与审计:所有支付相关的操作,全链路落日志
6.2 高可用集群与容灾设计
- 服务集群部署:至少 3 个节点
- 多支付渠道容灾:配置渠道切换开关
- 熔断降级策略:配置熔断规则
- 数据库高可用:MySQL 主从分离、读写分离
- 机房容灾:同城双活
6.3 全链路监控与实时告警方案
核心监控指标:
- 支付接口指标:成功率、响应时间、QPS
- 回调接口指标:成功率、验签失败次数
- 订单状态指标:支付成功但订单状态未更新的订单数量
- MQ 消费指标:消费成功率、堆积数量
- 系统资源指标:CPU、内存、磁盘使用率
七、踩坑实录:生产环境 10 个真实坑与解决方案
以下是生产环境中常见的真实案例与解决方案:
坑 1:回调接口业务处理超时
事故背景:回调接口里做了库存扣减、短信发送等复杂操作,导致响应超时,微信重复回调。 解决方案:回调接口只做验签、幂等校验、发 MQ,其余业务全部异步处理。
坑 2:订单号重复
事故背景:高并发下出现了重复的订单号,导致资损。 解决方案:用雪花算法生成订单号,数据库加唯一索引。
坑 3:没有做金额校验
事故背景:直接用前端传过来的金额,被篡改。 解决方案:预支付接口必须从数据库查询订单的真实金额,回调时校验金额一致性。
坑 4:Redis 分布式锁过期
事故背景:锁超时时间设置过短,业务处理未完成锁已过期。 解决方案:用 Redisson 的看门狗机制,锁超时时间设置为业务处理最大时间的 3 倍。
坑 5:回调接口加了登录鉴权拦截
事故背景:全局鉴权拦截器拦截了回调请求。 解决方案:回调接口加入白名单,绕过登录鉴权拦截器。
坑 6:微信支付证书过期
事故背景:证书到期未更换,导致支付不可用。 解决方案:配置证书过期时间监控,开启自动更新证书功能。
坑 7:没有做状态机前置校验
事故背景:定时任务误将支付成功的订单改为已取消。 解决方案:严格按照状态机规则,所有状态变更做前置校验。
坑 8:三方支付接口限流
事故背景:高峰期 QPS 过高触发限流。 解决方案:用 Sentinel 限流,配置重试机制,提前报备提升阈值。
坑 9:敏感信息硬编码
事故背景:密钥提交到 GitHub 公共仓库。 解决方案:敏感信息放在配置中心,禁止硬编码,配置.gitignore。
坑 10:定时任务重复执行
事故背景:集群部署时多个节点同时执行定时任务。 解决方案:用 XXL-Job 配置单机执行模式,或加分布式锁。
八、总结与拓展
总结
SpringCloud 微服务架构下的支付系统落地,核心不是'实现功能',而是'保证安全、稳定、一致'。从接口对接的签名规范,到异步通知的幂等设计,再到订单状态的闭环兜底,每一个环节都需要经过生产环境的验证。
拓展
- 对账系统:配套建设日终对账系统,拉取三方对账单与本地记录核对
- 退款功能:退款是支付的逆向流程,需注意资金风险控制
- 分账功能:平台型业务需对接分账接口
- 跨境支付:涉及汇率转换、外汇合规申报等



