跳到主要内容SpringCloud 微服务支付全链路生产级实践:接口对接与状态闭环 | 极客日志JavaWeChatPayjava
SpringCloud 微服务支付全链路生产级实践:接口对接与状态闭环
阐述基于 SpringCloud 微服务架构的支付系统生产级落地方案。内容包括微信支付 V3 接口对接、签名验签、异步通知处理及订单状态闭环设计。重点解决重复回调、掉单、资损等问题,采用可靠消息最终一致性、分布式锁、状态机及定时兜底机制保障数据一致性与高可用性。涵盖数据库表结构、核心代码实现及生产环境常见故障排查与解决方案。
涅槃凤凰35 浏览 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 三方支付资质与核心参数准备
- 资质申请:完成微信商户平台注册、认证,开通 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 ON UPDATE CURRENT_TIMESTAMP 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 版本为例,讲解支付接口的生产级对接。
3.1 支付接口对接核心规范
- 签名算法:使用 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
@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 完整代码实现:从下单到预支付参数返回
- 订单服务创建订单,调用支付服务生成预支付订单
- 支付服务校验订单信息,生成支付记录,调用微信支付统一下单接口
- 微信支付返回预支付参数,支付服务封装后返回给订单服务
- 订单服务将预支付参数返回给前端,前端唤起支付
1. 订单服务 - 创建订单并调用支付服务(OpenFeign 接口)
@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) {
String orderNo = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + idGenerator.nextId();
BigDecimal orderAmount = dto.getProductPrice().multiply(new BigDecimal(dto.getProductNum()));
LocalDateTime expireTime = LocalDateTime.now().plusMinutes(30);
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);
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("预支付订单生成失败");
}
orderInfo.setOrderStatus(1);
orderInfo.setPayType(dto.getPayType());
orderInfoMapper.updateById(orderInfo);
return 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();
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()));
}
String payNo = "PAY" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + idGenerator.nextId();
int totalFee = dto.getPayAmount().multiply(new BigDecimal("100")).intValue();
if (totalFee <= 0) {
return Result.fail("支付金额必须大于 0");
}
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"));
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(dto.getOpenid()));
try {
log.info("调用微信支付统一下单接口,支付流水号:{},订单号:{}", payNo, orderNo);
WxPayUnifiedOrderV3Result response = wxPayService.unifiedOrderV3(TradeTypeEnum.JSAPI, request);
String prepayId = response.getPrepayId();
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);
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 接口对接高频踩坑与解决方案
- 签名错误:严格按照官方文档拼接签名串,核对证书序列号
- 订单号重复:用支付流水号作为 out_trade_no,保证每次请求都是唯一的
- IP 白名单拦截:核对服务器出口 IP,添加到商户平台 IP 白名单
- 回调地址配置错误:确保回调地址是公网 HTTPS 地址,无参数,无登录鉴权拦截
- 金额错误:严格按照微信支付要求,金额单位为分,必须是正整数
四、核心模块二:支付异步通知全链路处理
支付异步通知(回调)是支付系统最核心、最容易出问题的环节。
4.1 异步通知的核心痛点与设计原则
核心痛点
- 重复回调:三方支付如果没有收到成功响应,会按照固定频率重复发送回调
- 签名伪造:黑客可能会伪造回调请求
- 回调丢失:网络波动、服务宕机,可能导致回调请求没有到达
- 业务处理超时:回调接口里做复杂业务处理,导致响应超时
- 状态不一致:回调处理成功,但订单状态更新失败
设计原则(铁则)
- 快速响应:回调接口必须在 100ms 内返回响应,绝对不能在接口里做复杂业务处理
- 验签优先:所有回调请求,必须先做签名验签,验签失败直接拒绝
- 全链路日志:所有回调请求,无论成功失败,必须全量落库
- 幂等性保障:必须做多层幂等校验,杜绝重复回调导致的重复业务处理
- 异常隔离:回调服务必须独立部署,与核心业务隔离
4.2 回调接口的安全防线:签名验签硬核实现
- 从请求头中获取微信支付的证书序列号、签名、时间戳、随机数
- 校验证书序列号是否与微信支付平台证书序列号一致
- 按照官方规范拼接签名串,用微信支付平台公钥对签名进行验签
@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 {
long currentTime = System.currentTimeMillis() / 1000;
long requestTime = Long.parseLong(timestamp);
if (Math.abs(currentTime - requestTime) > 300) {
log.error("回调请求时间戳过期,当前时间:{},请求时间:{}", currentTime, requestTime);
return false;
}
String signStr = timestamp + "\n" + nonce + "\n" + requestBody + "\n";
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) {
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");
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 分布式锁做前置校验。
@Override
public boolean checkDuplicateCallback(String orderNo, String transactionId) {
String lockKey = "pay:callback:lock:" + orderNo + ":" + transactionId;
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 生产级回调接口完整实现
@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 {
String requestBody = getRequestBody(request);
log.info("收到微信支付回调,请求头:{},请求体:{}", headers, requestBody);
String serialNo = headers.get("Wechatpay-Serial");
String signature = headers.get("Wechatpay-Signature");
String timestamp = headers.get("Wechatpay-Timestamp");
String nonce = headers.get("Wechatpay-Nonce");
if (!platformCertSerialNo.equals(serialNo)) {
log.error("微信支付回调证书序列号不匹配,预期:{},实际:{}", platformCertSerialNo, serialNo);
result.put("code", "FAIL");
result.put("message", "证书序列号不匹配");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
}
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);
}
WxPayNotifyDTO notifyDTO = wxPayCallbackService.decryptNotifyData(requestBody, apiV3Key);
orderNo = notifyDTO.getOutTradeNo();
transactionId = notifyDTO.getTransactionId();
log.info("微信支付回调报文解密成功,支付流水号:{},三方流水号:{}", orderNo, transactionId);
boolean isDuplicate = wxPayCallbackService.checkDuplicateCallback(orderNo, transactionId);
if (isDuplicate) {
log.warn("微信支付重复回调,支付流水号:{},直接返回成功", orderNo);
result.put("code", "SUCCESS");
result.put("message", "成功");
return ResponseEntity.ok(result);
}
PayCallbackLog callbackLog = PayCallbackLog.builder()
.orderNo(orderNo)
.transactionId(transactionId)
.payType(1)
.requestBody(requestBody)
.requestHeader(headers.toString())
.signVerifyResult(1)
.handleResult(0)
.build();
payCallbackLogMapper.insert(callbackLog);
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);
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);
}
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 回调异常的兜底与重试机制
- 回调失败重试:定时任务扫描处理失败的记录,重新发送 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);
Result<PayRecordVO> payRecordResult = payRecordFeignClient.getPayRecordByPayNo(payNo);
if (!payRecordResult.isSuccess()) {
throw new RuntimeException("支付记录查询失败");
}
PayRecordVO payRecord = payRecordResult.getData();
String orderNo = payRecord.getOrderNo();
OrderInfo orderInfo = orderInfoMapper.selectOne(Wrappers.lambdaQuery(OrderInfo.class).eq(OrderInfo::getOrderNo, orderNo));
if (orderInfo == null) {
throw new RuntimeException("订单不存在");
}
if (orderInfo.getOrderStatus() == 2) {
log.warn("订单已经支付成功,订单号:{},直接返回", orderNo);
return;
}
if (orderInfo.getOrderStatus() != 0 && orderInfo.getOrderStatus() != 1) {
throw new RuntimeException("订单状态异常");
}
if (payRecord.getPayAmount().compareTo(orderInfo.getOrderAmount()) != 0) {
throw new RuntimeException("支付金额异常");
}
orderInfo.setOrderStatus(2);
orderInfo.setPayAmount(payRecord.getPayAmount());
orderInfo.setTransactionId(message.getTransactionId());
orderInfo.setPayTime(message.getPayTime());
orderInfoMapper.updateById(orderInfo);
log.info("订单状态更新成功,订单号:{}", orderNo);
} catch (Exception e) {
log.error("支付结果消息消费异常,支付流水号:{}", payNo, e);
throw e;
} finally {
if (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("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);
}
}
}
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 微服务架构下的支付系统落地,核心不是'实现功能',而是'保证安全、稳定、一致'。从接口对接的签名规范,到异步通知的幂等设计,再到订单状态的闭环兜底,每一个环节都需要经过生产环境的验证。
拓展
- 对账系统:配套建设日终对账系统,拉取三方对账单与本地记录核对
- 退款功能:退款是支付的逆向流程,需注意资金风险控制
- 分账功能:平台型业务需对接分账接口
- 跨境支付:涉及汇率转换、外汇合规申报等
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online