某支付平台开发实战解析 从0到1构建高并发支付库 解决渠道对接 支付超时 对账异常等核心问题 含完整代码示例与线上踩坑经验
做支付系统这行,干得越久越觉得敬畏。三年前我加入一家电商公司,老板丢下一句话:”你们自己写一套支付库,别老依赖第三方支付接口的坑。”那时我以为就是封装几个HTTP请求的事,结果上线第一个月,对账差了八万块,财务大姐差点把我撕了。今天把这些血泪经验掏出来,希望能让你少踩几个坑。
一、为什么要自己写支付库?
很多人会问:直接用微信支付、支付宝的SDK不就行了?确实,初期这样做没问题。但当你业务量上来,问题就来了——
- 多渠道统一接口太痛苦,每次换渠道都要改业务代码
- 超时处理、重试策略各自为战,没有统一规范
- 对账逻辑分散在各处,出了问题满天找
- 支付状态的流转没有统一的视图,查个订单状态要调用三个系统
我自己总结过,支付库的核心价值就三个词:统一、可控、可追溯。
二、整体架构设计
先画个草图,心里有数再动手。
┌─────────────────────────────────────────────────────────┐
│ 业务层 │
│ 订单系统 / 退款系统 / 充值系统 / 营销活动 │
└──────────────────────┬──────────────────────────────────┘
│ 调用
┌──────────────────────▼──────────────────────────────────┐
│ 支付网关层 │
│ PaymentGateway · 统一入口 · 路由分发 · 幂等控制 │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ 渠道适配层 │
│ AlipayAdapter · WechatAdapter · UnionpayAdapter · ... │
└──────────────────────┬──────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────┐
│ 基础设施层 │
│ 超时控制 · 重试机制 · 分布式锁 · 消息队列 · 对账引擎 │
└─────────────────────────────────────────────────────────┘
这个架构的核心思想是依赖倒置——上层不关心底层是哪个渠道,底层随时可以替换。我见过太多项目把渠道代码散落在业务里,后来换渠道的时候整个团队熬夜改代码,那种痛苦我不想让你再经历。
三、核心数据结构设计
支付系统里最基础也最重要的就是这几个数据结构,设计错了后面全是坑。
/**
* 支付单 - 整个系统的核心实体
* 线上真实踩坑:字段设计不全,后期加字段改表结构,数据迁移差点把生产库搞崩
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PaymentOrder {
/**
* 支付单号,全局唯一,格式:PAY + yyyyMMddHHmmss + 8位随机数
* 不要直接用订单号作为支付单号!订单可能取消重下,但支付单必须唯一
*/
private String paymentNo;
/**
* 业务订单号,由上游系统传入
*/
private String orderNo;
/**
* 商户ID
*/
private String merchantId;
/**
* 支付金额,单位:分。注意:不要用double!浮点数在支付领域是禁忌
*/
private Long amount;
/**
* 币种,默认CNY
*/
private String currency;
/**
* 支付渠道:ALIPAY / WECHAT / UNIONPAY
*/
private String channelCode;
/**
* 渠道子类型,比如微信的JSAPI、APP、H5
*/
private String channelSubCode;
/**
* 支付状态
* PENDING - 待支付
* PROCESSING - 支付处理中
* SUCCESS - 支付成功
* CLOSED - 已关闭
* REFUNDING - 退款中
* REFUNDED - 已退款
* FAIL - 支付失败
* TIMEOUT - 支付超时
*/
private String status;
/**
* 渠道返回的交易号,用于对账和退款
* 这个字段必须持久化!很多项目只存了支付单号,退款时对不上账
*/
private String channelTransactionId;
/**
* 渠道返回的原始响应
* 出问题的时候这是最重要的排查依据
*/
private String channelResponse;
/**
* 支付超时时间,单位:分钟
* 不同渠道超时策略不同,支付宝一般2小时,微信看场景
*/
private Integer timeoutMinutes;
/**
* 超时时间戳,用于判断是否超时
*/
private Long timeoutAt;
/**
* 支付完成时间
*/
private Long successTime;
/**
* 创建时间
*/
private Long createdAt;
/**
* 更新时间
*/
private Long updatedAt;
/**
* 扩展信息,JSON格式
* 不同渠道需要的参数不一样,放这里避免频繁改表结构
*/
private String extInfo;
}
/**
* 支付请求 - 统一接口入参
* 设计原则:尽可能把各渠道的差异收敛到这里
*/
@Data
@Builder
public class PaymentRequest {
/**
* 业务订单号
*/
private String orderNo;
/**
* 商户ID
*/
private String merchantId;
/**
* 金额(分)
*/
private Long amount;
/**
* 支付渠道
*/
private String channelCode;
/**
* 支付描述
*/
private String subject;
/**
* 支付者用户ID(各渠道的openId/userId)
*/
private String payerId;
/**
* 支付者渠道ID
*/
private String payerChannelId;
/**
* 异步通知地址
* 一定要做合法性校验!曾经有漏洞让攻击者把回调地址改成自己的服务器
*/
private String notifyUrl;
/**
* 同步跳转地址
*/
private String returnUrl;
/**
* 终端IP
* 用于风控,必须传
*/
private String clientIp;
/**
* 设备信息
*/
private String deviceInfo;
/**
* 请求时间戳
*/
private Long requestTime;
/**
* 幂等键,由调用方生成,防止重复支付
*/
private String idempotentKey;
}
/**
* 支付响应 - 统一接口出参
*/
@Data
@Builder
public class PaymentResponse {
/**
* 支付单号
*/
private String paymentNo;
/**
* 是否成功
*/
private Boolean success;
/**
* 渠道返回的交易号
*/
private String channelTransactionId;
/**
* 渠道返回的支付参数
* 比如支付宝的form表单参数,微信的prepay_id
* 不同渠道返回格式不同,用Object适配
*/
private Object channelData;
/**
* 错误码
*/
private String errorCode;
/**
* 错误信息
*/
private String errorMsg;
/**
* 支付状态
*/
private String status;
/**
* 是否需要等待回调
* 有些渠道是同步返回结果的,有些需要等异步通知
*/
private Boolean needCallback;
}
四、渠道适配器模式
这是整个支付库最核心的设计。我们用接口+实现的模式,让上层业务完全不知道底层用的是哪个渠道。
/**
* 渠道适配器接口
* 所有支付渠道必须实现这个接口
*/
public interface PaymentChannelAdapter {
/**
* 获取渠道编码
*/
String getChannelCode();
/**
* 发起支付
* 这是最核心的方法,每个渠道实现不同
*/
PaymentResponse pay(PaymentRequest request);
/**
* 查询支付状态
* 用于主动轮询和对账差异修复
*/
PaymentQueryResponse query(String paymentNo);
/**
* 关闭支付(超时或用户主动取消)
*/
CloseResponse close(String paymentNo);
/**
* 发起退款
*/
RefundResponse refund(RefundRequest request);
/**
* 验证回调签名
* 安全第一!签名验证不通过直接丢弃,不要记录敏感信息
*/
boolean verifyCallbackSign(String body, Map<String, String> params);
/**
* 解析回调通知
*/
CallbackResult parseCallback(String body);
}
/**
* 支付宝适配器实现
* 基于支付宝官方SDK封装
*/
@Slf4j
@Component
@ChannelAdapter(channelCode = "ALIPAY")
public class AlipayChannelAdapter implements PaymentChannelAdapter {
@Autowired
private AlipayConfig alipayConfig;
@Autowired
private AlipayClient alipayClient;
@Override
public String getChannelCode() {
return "ALIPAY";
}
@Override
public PaymentResponse pay(PaymentRequest request) {
try {
// 构建支付宝请求参数
AlipayTradePagePayRequest payRequest = new AlipayTradePagePayRequest();
payRequest.setNotifyUrl(request.getNotifyUrl());
payRequest.setReturnUrl(request.getReturnUrl());
// 业务参数
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", request.getOrderNo());
bizContent.put("total_amount", divideAmount(request.getAmount()));
bizContent.put("subject", request.getSubject());
bizContent.put("timeout_express", request.getTimeoutMinutes() + "m");
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
// 用户ID(支付宝buyer_id)
if (StringUtils.isNotBlank(request.getPayerChannelId())) {
bizContent.put("buyer_id", request.getPayerChannelId());
}
// 终端IP,用于风控
if (StringUtils.isNotBlank(request.getClientIp())) {
bizContent.put("store_code", request.getClientIp());
}
payRequest.setBizContent(bizContent.toJSONString());
// 调用支付宝SDK
AlipayTradePagePayResponse response = alipayClient.pageExecute(payRequest);
if (response.isSuccess()) {
PaymentResponse result = PaymentResponse.builder()
.success(true)
.channelData(response.getBody()) // 支付宝返回的form表单
.status("SUCCESS")
.build();
log.info("支付宝支付请求成功, orderNo={}, response={}",
request.getOrderNo(), response.getBody());
return result;
} else {
log.warn("支付宝支付请求失败, orderNo={}, code={}, msg={}",
request.getOrderNo(), response.getCode(), response.getMsg());
return PaymentResponse.builder()
.success(false)
.errorCode(response.getCode())
.errorMsg(response.getMsg())
.status("FAIL")
.build();
}
} catch (Exception e) {
log.error("支付宝支付异常, orderNo={}", request.getOrderNo(), e);
throw new PaymentException("支付处理异常", e);
}
}
@Override
public PaymentQueryResponse query(String paymentNo) {
try {
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", paymentNo);
request.setBizContent(bizContent.toJSONString());
AlipayTradeQueryResponse response = alipayClient.execute(request);
PaymentQueryResponse result = new PaymentQueryResponse();
result.setPaymentNo(paymentNo);
if (response.isSuccess()) {
// 支付状态映射
// TRADE_SUCCESS / TRADE_CLOSED / WAIT_BUYER_PAY / TRADE_CLOSED
String tradeStatus = response.getTradeStatus();
if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
result.setStatus("SUCCESS");
result.setChannelTransactionId(response.getTradeNo());
result.setGmtPayment(response.getSendPay_date());
} else if ("TRADE_CLOSED".equals(tradeStatus)) {
result.setStatus("CLOSED");
} else {
result.setStatus("PROCESSING");
}
result.setSuccess(true);
} else {
result.setSuccess(false);
result.setErrorCode(response.getCode());
result.setErrorMsg(response.getMsg());
}
return result;
} catch (Exception e) {
log.error("支付宝查询支付状态异常, paymentNo={}", paymentNo, e);
throw new PaymentException("查询失败", e);
}
}
@Override
public CloseResponse close(String paymentNo) {
try {
AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", paymentNo);
request.setBizContent(bizContent.toJSONString());
AlipayTradeCloseResponse response = alipayClient.execute(request);
CloseResponse result = new CloseResponse();
result.setSuccess(response.isSuccess());
result.setChannelTransactionId(response.getTradeNo());
return result;
} catch (Exception e) {
log.error("支付宝关闭支付异常, paymentNo={}", paymentNo, e);
throw new PaymentException("关闭支付失败", e);
}
}
@Override
public RefundResponse refund(RefundRequest request) {
try {
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", request.getPaymentNo());
bizContent.put("refund_amount", divideAmount(request.getRefundAmount()));
bizContent.put("refund_reason", request.getReason());
bizContent.put("out_request_no", request.getRefundNo());
refundRequest.setBizContent(bizContent.toJSONString());
AlipayTradeRefundResponse response = alipayClient.execute(refundRequest);
RefundResponse result = new RefundResponse();
result.setSuccess(response.isSuccess());
result.setRefundNo(response.getTradeNo());
result.setChannelTransactionId(response.getFund_change());
return result;
} catch (Exception e) {
log.error("支付宝退款异常, paymentNo={}", request.getPaymentNo(), e);
throw new PaymentException("退款失败", e);
}
}
@Override
public boolean verifyCallbackSign(String body, Map<String, String> params) {
// 支付宝验签
return AlipaySignature.rsaCheckV1(
params,
alipayConfig.getAlipayPublicKey(),
"UTF-8",
"RSA2"
);
}
@Override
public CallbackResult parseCallback(String body) {
// 支付宝异步通知参数
Map<String, String> params = new HashMap<>();
String[] pairs = body.split("&");
for (String pair : pairs) {
String[] kv = pair.split("=", 2);
if (kv.length == 2) {
params.put(kv[0], kv[1]);
}
}
CallbackResult result = new CallbackResult();
result.setPaymentNo(params.get("out_trade_no"));
result.setChannelTransactionId(params.get("trade_no"));
result.setStatus("SUCCESS".equals(params.get("trade_status")) ? "SUCCESS" : "FAIL");
result.setAmount(parseAmount(params.get("total_amount")));
result.setFinishTime(params.get("gmt_payment"));
result.setExtInfo(params);
return result;
}
/**
* 金额转换:分 -> 元(支付宝接口要求是元,保留两位小数)
*/
private String divideAmount(Long amountInFen) {
return BigDecimal.valueOf(amountInFen)
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
.toString();
}
/**
* 金额转换:元 -> 分
*/
private Long parseAmount(String amountInYuan) {
return BigDecimal.valueOf(Double.parseDouble(amountInYuan))
.multiply(BigDecimal.valueOf(100))
.longValue();
}
}
/**
* 微信支付适配器
* 微信的签名算法和支付宝不同,注意区分
*/
@Slf4j
@Component
@ChannelAdapter(channelCode = "WECHAT")
public class WechatChannelAdapter implements PaymentChannelAdapter {
@Autowired
private WechatConfig wechatConfig;
@Autowired
private WechatPayService wechatPayService;
@Override
public String getChannelCode() {
return "WECHAT";
}
@Override
public PaymentResponse pay(PaymentRequest request) {
try {
// 微信JSAPI支付
WxPayUnifiedOrderRequest wxRequest = new WxPayUnifiedOrderRequest();
wxRequest.setOutTradeNo(request.getOrderNo());
wxRequest.setTotalFee(request.getAmount().intValue()); // 微信单位是分
wxRequest.setBody(request.getSubject());
wxRequest.setNotifyUrl(request.getNotifyUrl());
wxRequest.setSpbillCreateIp(request.getClientIp());
wxRequest.setTradeType("JSAPI");
wxRequest.setOpenid(request.getPayerChannelId());
// 小程序支付
if ("WECHAT_MINIPROGRAM".equals(request.getChannelSubCode())) {
wxRequest.setTradeType("MINIPROGRAM");
}
// H5支付
else if ("WECHAT_H5".equals(request.getChannelSubCode())) {
wxRequest.setTradeType("MWEB");
// H5支付需要传scene_info
JSONObject sceneInfo = new JSONObject();
sceneInfo.put("h5_info", JSONObject.fromObject(
Map.of("type", "Wap")));
wxRequest.setSceneInfo(sceneInfo.toJSONString());
}
WxPayResult wxResult = wechatPayService.unifiedOrder(wxRequest);
PaymentResponse result = PaymentResponse.builder()
.success(true)
.channelData(wxResult.getPrepayId())
.status("SUCCESS")
.build();
log.info("微信支付请求成功, orderNo={}, prepayId={}",
request.getOrderNo(), wxResult.getPrepayId());
return result;
} catch (Exception e) {
log.error("微信支付异常, orderNo={}", request.getOrderNo(), e);
throw new PaymentException("支付处理异常", e);
}
}
@Override
public PaymentQueryResponse query(String paymentNo) {
try {
WxPayOrderQueryRequest queryRequest = new WxPayOrderQueryRequest();
// 优先用商户订单号查询
queryRequest.setOutTradeNo(paymentNo);
WxPayOrderQueryResult result = wechatPayService.queryOrder(queryRequest);
PaymentQueryResponse response = new PaymentQueryResponse();
response.setPaymentNo(paymentNo);
// 微信订单状态
// NOTPAY - 未支付
// SUCCESS - 支付成功
// REFUND - 转入退款
// CLOSED - 已关闭
String tradeState = result.getTradeState();
if ("SUCCESS".equals(tradeState)) {
response.setStatus("SUCCESS");
response.setChannelTransactionId(result.getTransactionId());
response.setSuccess(true);
} else if ("CLOSED".equals(tradeState) || "REVOKED".equals(tradeState)) {
response.setStatus("CLOSED");
response.setSuccess(true);
} else if ("NOTPAY".equals(tradeState) || "USERPAYING".equals(tradeState)) {
response.setStatus("PROCESSING");
response.setSuccess(true);
} else {
response.setSuccess(false);
response.setErrorMsg("支付状态异常: " + tradeState);
}
return response;
} catch (Exception e) {
log.error("微信支付查询异常, paymentNo={}", paymentNo, e);
throw new PaymentException("查询失败", e);
}
}
@Override
public CloseResponse close(String paymentNo) {
try {
WxPayOrderCloseRequest closeRequest = new WxPayOrderCloseRequest();
closeRequest.setOutTradeNo(paymentNo);
wechatPayService.closeOrder(closeRequest);
CloseResponse result = new CloseResponse();
result.setSuccess(true);
return result;
} catch (Exception e) {
log.error("微信关闭支付异常, paymentNo={}", paymentNo, e);
// 微信关闭接口可能返回"订单已关闭",这种情况不算异常
if (e.getMessage() != null && e.getMessage().contains("ORDERCLOSED")) {
CloseResponse result = new CloseResponse();
result.setSuccess(true);
return result;
}
throw new PaymentException("关闭支付失败", e);
}
}
@Override
public RefundResponse refund(RefundRequest request) {
try {
WxPayRefundRequest refundRequest = new WxPayRefundRequest();
refundRequest.setOutTradeNo(request.getPaymentNo());
refundRequest.setOutRefundNo(request.getRefundNo());
refundRequest.setTotalFee(request.getAmount().intValue());
refundRequest.setRefundFee(request.getRefundAmount().intValue());
refundRequest.setRefundReason(request.getReason());
WxPayRefundResult result = wechatPayService.refund(refundRequest);
RefundResponse response = new RefundResponse();
response.setSuccess("SUCCESS".equals(result.getResultCode()));
response.setChannelTransactionId(result.getRefundId());
return response;
} catch (Exception e) {
log.error("微信退款异常, paymentNo={}", request.getPaymentNo(), e);
throw new PaymentException("退款失败", e);
}
}
@Override
public boolean verifyCallbackSign(String body, Map<String, String> params) {
// 微信回调验签
// 方式一:使用SDK自带验签
return WxPayNotifications.isValidSign(body,
wechatConfig.getApiKey(),
WxPayConstants.SignType.HMACSHA256);
}
@Override
public CallbackResult parseCallback(String body) {
// 微信回调解密和解析
CallbackResult result = new CallbackResult();
// 微信v3回调是JSON格式,需要解密
// 微信v2回调是XML格式
// 这里以v3为例
JSONObject notifyData = JSON.parseObject(body);
// 解密回调数据
String ciphertext = notifyData.getJSONObject("result_info").getString("ciphertext");
String decrypted = AesUtil.decryptToString(
ciphertext,
wechatConfig.getApiV3Key()
);
JSONObject data = JSON.parseObject(decrypted);
result.setPaymentNo(data.getString("out_trade_no"));
result.setChannelTransactionId(data.getString("transaction_id"));
result.setStatus("SUCCESS".equals(data.getString("trade_state")) ? "SUCCESS" : "FAIL");
result.setAmount(Long.valueOf(data.getString("amount")).longValue()
- Long.valueOf(data.getString("payer_total")).longValue());
result.setFinishTime(data.getString("success_time"));
return result;
}
}
五、支付网关层——统一入口
这一层负责路由、幂等、超时控制等核心逻辑。
/**
* 支付网关 - 统一入口
* 所有业务系统的支付请求都从这里进入
*/
@Slf4j
@Service
public class PaymentGateway {
@Autowired
private PaymentOrderService paymentOrderService;
@Autowired
private PaymentChannelAdapterRegistry adapterRegistry;
@Autowired
private IdempotentChecker idempotentChecker;
@Autowired
private TimeoutController timeoutController;
@Autowired
private PaymentEventPublisher eventPublisher;
/**
* 发起支付 - 核心方法
* 整个流程:幂等检查 -> 创建支付单 -> 调用渠道 -> 保存结果 -> 返回
*/
@Transactional(rollbackFor = Exception.class)
public PaymentResponse pay(PaymentRequest request) {
log.info("支付请求开始, orderNo={}, channel={}, amount={}, idempotentKey={}",
request.getOrderNo(), request.getChannelCode(), request.getAmount(),
request.getIdempotentKey());
// 1. 幂等性检查
if (StringUtils.isNotBlank(request.getIdempotentKey())) {
PaymentOrder existing = paymentOrderService.getByPaymentNo(
request.getIdempotentKey());
if (existing != null) {
log.info("幂等命中, paymentNo={}", existing.getPaymentNo());
return buildResponse(existing);
}
}
// 2. 创建支付单
PaymentOrder paymentOrder = buildPaymentOrder(request);
paymentOrderService.create(paymentOrder);
// 3. 获取渠道适配器
PaymentChannelAdapter adapter = adapterRegistry.getAdapter(
request.getChannelCode());
if (adapter == null) {
throw new PaymentException("不支持的支付渠道: " + request.getChannelCode());
}
// 4. 调用渠道支付
// 注意:这里设置了超时控制,防止渠道响应慢拖死整个系统
PaymentResponse response = timeoutController.executeWithTimeout(
() -> adapter.pay(request),
paymentOrder.getTimeoutMinutes(),
TimeUnit.MINUTES
);
// 5. 更新支付单状态
if (response.isSuccess()) {
paymentOrder.setStatus("SUCCESS");
if (response.getChannelTransactionId() != null) {
paymentOrder.setChannelTransactionId(
response.getChannelTransactionId());
}
} else {
paymentOrder.setStatus("FAIL");
}
paymentOrder.setUpdatedAt(System.currentTimeMillis());
paymentOrderService.update(paymentOrder);
// 6. 发布支付事件(异步,不阻塞主流程)
eventPublisher.publishPaymentCreatedEvent(paymentOrder);
log.info("支付请求完成, paymentNo={}, status={}",
paymentOrder.getPaymentNo(), response.getStatus());
return response;
}
/**
* 构建支付单
*/
private PaymentOrder buildPaymentOrder(PaymentRequest request) {
String paymentNo = generatePaymentNo();
return PaymentOrder.builder()
.paymentNo(paymentNo)
.orderNo(request.getOrderNo())
.merchantId(request.getMerchantId())
.amount(request.getAmount())
.currency("CNY")
.channelCode(request.getChannelCode())
.channelSubCode(request.getChannelSubCode())
.status("PENDING")
.timeoutMinutes(request.getTimeoutMinutes() != null
? request.getTimeoutMinutes() : 120)
.timeoutAt(System.currentTimeMillis() +
(request.getTimeoutMinutes() != null
? request.getTimeoutMinutes() : 120) * 60 * 1000L)
.extInfo(buildExtInfo(request))
.createdAt(System.currentTimeMillis())
.updatedAt(System.currentTimeMillis())
.build();
}
/**
* 构建扩展信息
*/
private String buildExtInfo(PaymentRequest request) {
JSONObject ext = new JSONObject();
ext.put("payer_id", request.getPayerId());
ext.put("payer_channel_id", request.getPayerChannelId());
ext.put("client_ip", request.getClientIp());
ext.put("device_info", request.getDeviceInfo());
ext.put("notify_url", request.getNotifyUrl());
ext.put("return_url", request.getReturnUrl());
ext.put("idempotent_key", request.getIdempotentKey());
return ext.toJSONString();
}
/**
* 生成支付单号
* 格式:PAY + yyyyMMddHHmmss + 8位随机数
* 保证全局唯一,且有一定的可读性
*/
private String generatePaymentNo() {
String timestamp = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String random = RandomStringUtils.randomNumeric(8);
return "PAY" + timestamp + random;
}
private PaymentResponse buildResponse(PaymentOrder order) {
return PaymentResponse.builder()
.paymentNo(order.getPaymentNo())
.success("SUCCESS".equals(order.getStatus()))
.channelTransactionId(order.getChannelTransactionId())
.status(order.getStatus())
.build();
}
}
六、渠道适配器注册中心
/**
* 渠道适配器注册中心
* 使用Spring的注解扫描自动注册,支持热插拔
*/
@Component
public class PaymentChannelAdapterRegistry {
private final Map<String, PaymentChannelAdapter> adapterMap = new ConcurrentHashMap<>();
/**
* 自动注册所有带有@ChannelAdapter注解的适配器
*/
@Autowired
private List<PaymentChannelAdapter> adapters;
@PostConstruct
public void init() {
for (PaymentChannelAdapter adapter : adapters) {
adapterMap.put(adapter.getChannelCode(), adapter);
log.info("支付渠道适配器注册成功: {}", adapter.getChannelCode());
}
log.info("支付渠道适配器初始化完成,共注册 {} 个渠道", adapterMap.size());
}
/**
* 根据渠道编码获取适配器
*/
public PaymentChannelAdapter getAdapter(String channelCode) {
PaymentChannelAdapter adapter = adapterMap.get(channelCode);
if (adapter == null) {
log.error("未找到支付渠道适配器: {}", channelCode);
}
return adapter;
}
/**
* 检查渠道是否支持
*/
public boolean supports(String channelCode) {
return adapterMap.containsKey(channelCode);
}
}
/**
* 渠道适配器注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ChannelAdapter {
String channelCode();
}
七、超时控制——线上最坑的地方
超时处理是支付系统最容易出问题的地方之一。我见过太多项目因为超时逻辑写得烂,导致:
- 用户付了钱但系统认为超时关闭了
- 渠道已经扣款但本地状态没更新
- 重复调用渠道造成重复支付
我们的解决方案是三层超时控制:
/**
* 支付超时控制器
* 三层防护:执行超时 + 主动轮询 + 定时对账
*/
@Slf4j
@Service
public class TimeoutController {
@Autowired
private PaymentOrderService paymentOrderService;
@Autowired
private PaymentChannelAdapterRegistry adapterRegistry;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 带超时的执行
* 使用CompletableFuture实现,避免线程阻塞
*/
public <T> T executeWithTimeout(Supplier<T> supplier, long timeout, TimeUnit unit) {
try {
return CompletableFuture.supplyAsync(supplier)
.get(timeout, unit);
} catch (TimeoutException e) {
log.warn("支付请求超时, timeout={} {}", timeout, unit);
throw new PaymentTimeoutException("支付请求超时,请稍后查询支付结果");
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof PaymentException) {
throw (PaymentException) cause;
}
throw new PaymentException("支付处理异常", cause);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new PaymentException("支付请求被中断", e);
}
}
/**
* 超时定时任务
* 每分钟扫描一次超时的支付单,主动查询渠道状态
*/
@Scheduled(cron = "0 * * * * ?")
public void handleTimeoutPayments() {
log.info("开始处理超时支付单");
List<PaymentOrder> timeoutOrders = paymentOrderService
.findTimeoutOrders(LocalDateTime.now());
if (CollectionUtils.isEmpty(timeoutOrders)) {
return;
}
for (PaymentOrder order : timeoutOrders) {
try {
handleSingleTimeoutOrder(order);
} catch (Exception e) {
log.error("处理超时支付单异常, paymentNo={}", order.getPaymentNo(), e);
}
}
}
/**
* 处理单个超时支付单
* 核心逻辑:主动查询渠道状态,根据结果决定是补单还是关闭
*/
private void handleSingleTimeoutOrder(PaymentOrder order) {
log.info("处理超时支付单, paymentNo={}, channel={}",
order.getPaymentNo(), order.getChannelCode());
PaymentChannelAdapter adapter = adapterRegistry.getAdapter(
order.getChannelCode());
if (adapter == null) {
log.error("渠道适配器不存在, paymentNo={}", order.getPaymentNo());
return;
}
try {
// 主动查询渠道支付状态
PaymentQueryResponse queryResponse = adapter.query(order.getPaymentNo());
if (queryResponse.isSuccess() && "SUCCESS".equals(queryResponse.getStatus())) {
// 渠道已支付,补单
handleSuccessPayment(order, queryResponse);
} else if ("CLOSED".equals(queryResponse.getStatus())) {
// 渠道已关闭
order.setStatus("CLOSED");
paymentOrderService.update(order);
} else {
// 仍未支付,关闭本地支付单
closePaymentOrder(order);
}
} catch (Exception e) {
log.error("查询渠道支付状态异常, paymentNo={}", order.getPaymentNo(), e);
// 查询失败不关闭,留给下一次轮询或对账处理
}
}
/**
* 处理渠道已支付的情况(补单)
* 这是最容易出错的地方!必须加分布式锁,防止并发补单
*/
@Transactional(rollbackFor = Exception.class)
public void handleSuccessPayment(PaymentOrder order, PaymentQueryResponse queryResponse) {
String lockKey = "payment:补单:" + order.getPaymentNo();
// 分布式锁,防止并发补单
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (!locked) {
log.warn("补单锁已存在,跳过, paymentNo={}", order.getPaymentNo());
return;
}
try {
// 二次确认:再查一次,防止重复处理
PaymentOrder currentOrder = paymentOrderService.getByPaymentNo(
order.getPaymentNo());
if ("SUCCESS".equals(currentOrder.getStatus())) {
log.info("支付单已处理,跳过, paymentNo={}", order.getPaymentNo());
return;
}
// 更新支付单状态
currentOrder.setStatus("SUCCESS");
currentOrder.setChannelTransactionId(
queryResponse.getChannelTransactionId());
currentOrder.setSuccessTime(System.currentTimeMillis());
currentOrder.setUpdatedAt(System.currentTimeMillis());
paymentOrderService.update(currentOrder);
// 发布支付成功事件,通知业务系统
PaymentSuccessEvent event = new PaymentSuccessEvent(this,
currentOrder, queryResponse);
SpringContextHolder.publishEvent(event);
log.info("补单成功, paymentNo={}, channelTransactionId={}",
currentOrder.getPaymentNo(),
queryResponse.getChannelTransactionId());
} finally {
redisTemplate.delete(lockKey);
}
}
/**
* 关闭支付单
*/
@Transactional(rollbackFor = Exception.class)
public void closePaymentOrder(PaymentOrder order) {
if (!"PENDING".equals(order.getStatus()) && !"PROCESSING".equals(order.getStatus())) {
return;
}
// 先尝试调用渠道关闭接口
PaymentChannelAdapter adapter = adapterRegistry.getAdapter(
order.getChannelCode());
if (adapter != null) {
try {
adapter.close(order.getPaymentNo());
} catch (Exception e) {
log.warn("渠道关闭支付失败,但不影响本地状态, paymentNo={}",
order.getPaymentNo(), e);
}
}
order.setStatus("CLOSED");
order.setUpdatedAt(System.currentTimeMillis());
paymentOrderService.update(order);
log.info("支付单已关闭, paymentNo={}", order.getPaymentNo());
}
}
八、对账系统——财务的生命线
对账做得好不好,直接决定你能不能睡安稳觉。我们当时因为对账问题,财务每个月都要加班三天。
8.1 对账核心流程
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 渠道对账单 │────▶│ 下载解析 │────▶│ 数据比对 │────▶│ 差异处理 │
│ (每日凌晨) │ │ 文件格式 │ │ 金额/状态 │ │ 自动/人工 │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ 差异告警 │
│ 推送钉钉/邮件 │
└─────────────┘
8.2 对账引擎实现
/**
* 对账引擎
* 核心能力:下载对账单 -> 解析 -> 比对 -> 生成差异报告
*/
@Slf4j
@Service
public class ReconciliationEngine {
@Autowired
private PaymentOrderService paymentOrderService;
@Autowired
private ChannelStatementDownloader statementDownloader;
@Autowired
private StatementParserFactory parserFactory;
@Autowired
private ReconciliationResultService resultService;
@Autowired
private AlertService alertService;
/**
* 执行对账
* 按渠道、按日期执行
*/
@Transactional(rollbackFor = Exception.class)
public ReconciliationReport reconcile(String channelCode, LocalDate date) {
log.info("开始对账, channel={}, date={}", channelCode, date);
ReconciliationReport report = new ReconciliationReport();
report.setChannelCode(channelCode);
report.setReconcileDate(date);
report.setStartTime(System.currentTimeMillis());
try {
// 1. 下载渠道对账单
List<StatementRecord> channelRecords =
statementDownloader.download(channelCode, date);
report.setChannelRecordCount(channelRecords.size());
report.setChannelTotalAmount(
channelRecords.stream()
.mapToLong(StatementRecord::getAmount)
.sum());
if (channelRecords.isEmpty()) {
log.warn("未获取到对账单, channel={}, date={}", channelCode, date);
report.setStatus("NO_STATEMENT");
return report;
}
// 2. 查询本地支付记录
List<PaymentOrder> localRecords = paymentOrderService
.findByDateAndChannel(date, channelCode);
report.setLocalRecordCount(localRecords.size());
report.setLocalTotalAmount(
localRecords.stream()
.filter(o -> "SUCCESS".equals(o.getStatus()))
.mapToLong(PaymentOrder::getAmount)
.sum());
// 3. 构建本地索引,便于快速查找
Map<String, PaymentOrder> localIndex = localRecords.stream()
.collect(Collectors.toMap(
PaymentOrder::getPaymentNo,
Function.identity(),
(a, b) -> a));
Map<String, PaymentOrder> channelIndex = channelRecords.stream()
.collect(Collectors.toMap(
StatementRecord::getChannelTransactionId,
Function.identity(),
(a, b) -> a));
// 4. 比对
List<ReconciliationDifference> differences = compare(
channelRecords, localRecords, localIndex, channelIndex);
report.setDifferences(differences);
report.setDifferenceCount(differences.size());
// 5. 判断是否平衡
boolean isBalanced = differences.stream()
.allMatch(d -> d.getType() == DifferenceType.NORMAL);
report.setBalanced(isBalanced);
// 6. 保存对账结果
resultService.save(report);
// 7. 如果有差异,发送告警
if (!isBalanced) {
alertService.sendReconciliationAlert(report);
}
report.setEndTime(System.currentTimeMillis());
log.info("对账完成, channel={}, date={}, 平衡={}, 差异数={}",
channelCode, date, isBalanced, differences.size());
} catch (Exception e) {
log.error("对账异常, channel={}, date={}", channelCode, date, e);
report.setStatus("ERROR");
report.setErrorMessage(e.getMessage());
alertService.sendReconciliationAlert(report);
}
return report;
}
/**
* 核心比对逻辑
* 四种差异类型:
* 1. CHANNEL_ONLY - 渠道有,本地无(漏单)
* 2. LOCAL_ONLY - 本地有,渠道无(多单)
* 3. AMOUNT_DIFF - 金额不一致
* 4. STATUS_DIFF - 状态不一致
*/
private List<ReconciliationDifference> compare(
List<StatementRecord> channelRecords,
List<PaymentOrder> localRecords,
Map<String, PaymentOrder> localIndex,
Map<String, StatementRecord> channelIndex) {
List<ReconciliationDifference> differences = new ArrayList<>();
// 渠道有但本地没有的记录(漏单)
for (StatementRecord channelRecord : channelRecords) {
PaymentOrder localRecord = localIndex.get(
channelRecord.getChannelTransactionId());
if (localRecord == null) {
differences.add(ReconciliationDifference.builder()
.type(DifferenceType.CHANNEL_ONLY)
.channelTransactionId(channelRecord.getChannelTransactionId())
.channelAmount(channelRecord.getAmount())
.localAmount(0L)
.description("渠道有记录但本地无记录,疑似漏单")
.build());
} else if (!channelRecord.getAmount().equals(localRecord.getAmount())) {
differences.add(ReconciliationDifference.builder()
.type(DifferenceType.AMOUNT_DIFF)
.channelTransactionId(channelRecord.getChannelTransactionId())
.channelAmount(channelRecord.getAmount())
.localAmount(localRecord.getAmount())
.description(String.format("金额不一致:渠道%s分,本地%s分",
channelRecord.getAmount(), localRecord.getAmount()))
.build());
} else if (!"SUCCESS".equals(localRecord.getStatus())) {
differences.add(ReconciliationDifference.builder()
.type(DifferenceType.STATUS_DIFF)
.channelTransactionId(channelRecord.getChannelTransactionId())
.channelAmount(channelRecord.getAmount())
.localAmount(localRecord.getAmount())
.description(String.format("状态不一致:渠道成功,本地状态为%s",
localRecord.getStatus()))
.build());
}
}
// 本地有但渠道没有的记录(多单)
for (PaymentOrder localRecord : localRecords) {
if (localRecord.getChannelTransactionId() == null) {
// 没有渠道交易号的,可能是支付未完成
continue;
}
StatementRecord channelRecord = channelIndex.get(
localRecord.getChannelTransactionId());
if (channelRecord == null && "SUCCESS".equals(localRecord.getStatus())) {
differences.add(ReconciliationDifference.builder()
.type(DifferenceType.LOCAL_ONLY)
.channelTransactionId(localRecord.getChannelTransactionId())
.channelAmount(0L)
.localAmount(localRecord.getAmount())
.description("本地有成功记录但渠道无记录,疑似掉单")
.build());
}
}
return differences;
}
}
/**
* 对账单记录
*/
@Data
@Builder
public class StatementRecord {
/**
* 渠道交易号
*/
private String channelTransactionId;
/**
* 商户订单号
*/
private String orderNo;
/**
* 交易金额(分)
*/
private Long amount;
/**
* 交易时间
*/
private LocalDateTime transactionTime;
/**
* 交易状态
*/
private String status;
/**
* 手续费
*/
private Long fee;
/**
* 结算金额
*/
private Long settleAmount;
}
8.3 对账差异自动处理
/**
* 对账差异处理器
* 根据差异类型自动或人工处理
*/
@Slf4j
@Service
public class DifferenceHandler {
@Autowired
private PaymentOrderService paymentOrderService;
@Autowired
private PaymentGateway paymentGateway;
@Autowired
private RefundService refundService;
/**
* 处理对账差异
*/
@Transactional(rollbackFor = Exception.class)
public void handleDifference(ReconciliationDifference difference) {
log.info("处理对账差异, type={}, channelTransactionId={}",
difference.getType(), difference.getChannelTransactionId());
switch (difference.getType()) {
case CHANNEL_ONLY:
handleChannelOnly(difference);
break;
case LOCAL_ONLY:
handleLocalOnly(difference);
break;
case AMOUNT_DIFF:
handleAmountDiff(difference);
break;
case STATUS_DIFF:
handleStatusDiff(difference);
break;
default:
log.warn("未知差异类型: {}", difference.getType());
}
}
/**
* 渠道有记录,本地无记录
* 通常是本地支付单创建失败或状态更新失败导致的
* 处理:补录支付单
*/
private void handleChannelOnly(ReconciliationDifference difference) {
log.info("补录漏单, channelTransactionId={}, amount={}",
difference.getChannelTransactionId(), difference.getChannelAmount());
// 这里需要根据实际情况补录,可能需要查询渠道订单详情
// 简化起见,直接标记为需要人工处理
difference.setStatus(DifferenceStatus.MANUAL_HANDLING);
log.warn("漏单需要人工处理: {}", difference.getChannelTransactionId());
}
/**
* 本地有记录,渠道无记录
* 通常是支付未完成或渠道侧失败
* 处理:查询渠道状态,如果确实未支付则关闭
*/
private void handleLocalOnly(ReconciliationDifference difference) {
log.info("处理本地多单, channelTransactionId={}",
difference.getChannelTransactionId());
PaymentOrder order = paymentOrderService
.getByPaymentNo(difference.getChannelTransactionId());
if (order == null) {
return;
}
// 查询渠道确认
PaymentChannelAdapter adapter = adapterRegistry.getAdapter(order.getChannelCode());
if (adapter != null) {
PaymentQueryResponse query = adapter.query(order.getPaymentNo());
if (!query.isSuccess() || !"SUCCESS".equals(query.getStatus())) {
// 渠道确认未支付,关闭本地订单
order.setStatus("CLOSED");
paymentOrderService.update(order);
difference.setStatus(DifferenceStatus.AUTO_FIXED);
}
}
}
/**
* 金额不一致
* 这是最严重的问题,必须人工介入
*/
private void handleAmountDiff(ReconciliationDifference difference) {
log.error("金额不一致,必须人工处理!channelTransactionId={}, 渠道:{}分, 本地:{}分",
difference.getChannelTransactionId(),
difference.getChannelAmount(),
difference.getLocalAmount());
difference.setStatus(DifferenceStatus.MANUAL_HANDLING);
// 发送紧急告警
alertService.sendCriticalAlert(difference);
}
/**
* 状态不一致
* 渠道显示成功但本地未成功
*/
private void handleStatusDiff(ReconciliationDifference difference) {
log.info("处理状态不一致, channelTransactionId={}",
difference.getChannelTransactionId());
// 查询渠道确认
PaymentChannelAdapter adapter = adapterRegistry.getAdapter(
difference.getChannelTransactionId());
// 补单逻辑与超时处理类似
// ...
}
}
九、支付回调处理——最后一个小时的战场
支付回调是整个支付链路中最容易被忽视但风险最高的环节。回调地址被篡改、签名验证不严格、重复处理……任何一个漏洞都可能导致资损。
/**
* 支付回调控制器
* 安全是第一要务!
*/
@Slf4j
@RestController
@RequestMapping("/api/payment/callback")
public class PaymentCallbackController {
@Autowired
private PaymentOrderService paymentOrderService;
@Autowired
private PaymentChannelAdapterRegistry adapterRegistry;
@Autowired
private PaymentEventPublisher eventPublisher;
@Autowired
private IdempotentChecker idempotentChecker;
/**
* 支付宝回调
* 注意:支付宝回调会重复发送多次,必须幂等处理
*/
@PostMapping("/alipay/notify")
public String alipayNotify(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
HttpServletRequest requestWrapper = new HttpServletRequestWrapper(request);
// 获取所有参数
Map<String, String[]> parameterMap = requestWrapper.getParameterMap();
for (Entry<String, String[]> entry : parameterMap.entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
try {
// 1. 验证签名 - 这一步绝对不能省!
PaymentChannelAdapter adapter = adapterRegistry.getAdapter("ALIPAY");
if (!adapter.verifyCallbackSign(null, params)) {
log.warn("支付宝回调签名验证失败");
return "failure";
}
// 2. 验单 - 检查订单是否存在且状态正确
String orderNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String totalAmount = params.get("total_amount");
PaymentOrder order = paymentOrderService.getByOrderNo(orderNo);
if (order == null) {
log.warn("支付宝回调:订单不存在, orderNo={}", orderNo);
return "failure";
}
// 3. 幂等性检查
if (!idempotentChecker.tryAcquire("callback:" + tradeNo)) {
log.info("支付宝回调:重复通知,已处理, tradeNo={}", tradeNo);
return "success";
}
// 4. 业务验证
if (!"TRADE_SUCCESS".equals(tradeStatus) &&
!"TRADE_FINISHED".equals(tradeStatus)) {
log.info("支付宝回调:交易未成功, tradeNo={}, status={}",
tradeNo, tradeStatus);
return "success"; // 不是成功状态,直接返回success让支付宝不再重试
}
// 金额校验
long amountInFen = new BigDecimal(totalAmount)
.multiply(BigDecimal.valueOf(100)).longValue();
if (amountInFen != order.getAmount()) {
log.error("支付宝回调:金额不一致!orderNo={}, 回调:{}分, 本地:{}分",
orderNo, amountInFen, order.getAmount());
// 金额不一致是严重问题,需要告警
alertService.sendCriticalAlert(
String.format("支付宝回调金额不一致:orderNo=%s, 回调=%d分, 本地=%d分",
orderNo, amountInFen, order.getAmount()));
return "failure";
}
// 5. 更新支付单状态
order.setStatus("SUCCESS");
order.setChannelTransactionId(tradeNo);
order.setSuccessTime(System.currentTimeMillis());
order.setUpdatedAt(System.currentTimeMillis());
paymentOrderService.update(order);
// 6. 发布支付成功事件
eventPublisher.publishPaymentSuccessEvent(order);
log.info("支付宝回调处理成功, orderNo={}, tradeNo={}, amount={}",
orderNo, tradeNo, amountInFen);
return "success";
} catch (Exception e) {
log.error("支付宝回调处理异常, orderNo={}",
params.get("out_trade_no"), e);
return "failure";
}
}
/**
* 微信支付回调
*/
@PostMapping("/wechat/notify")
public String wechatNotify(@RequestBody String body,
@RequestHeader("Wechatpay-Signature") String signature,
@RequestHeader("Wechatpay-Timestamp") String timestamp,
@RequestHeader("Wechatpay-Nonce") String nonce) {
try {
// 1. 验证签名
PaymentChannelAdapter adapter = adapterRegistry.getAdapter("WECHAT");
Map<String, String> params = parseCallbackParams(body);
if (!adapter.verifyCallbackSign(body, params)) {
log.warn("微信回调签名验证失败");
return buildFailResponse("签名验证失败");
}
// 2. 解析回调
CallbackResult callbackResult = adapter.parseCallback(body);
// 3. 幂等性检查
if (!idempotentChecker.tryAcquire(
"callback:" + callbackResult.getChannelTransactionId())) {
log.info("微信回调:重复通知,已处理");
return buildSuccessResponse();
}
// 4. 更新支付单
PaymentOrder order = paymentOrderService
.getByPaymentNo(callbackResult.getPaymentNo());
if (order == null) {
log.warn("微信回调:订单不存在, paymentNo={}",
callbackResult.getPaymentNo());
return buildFailResponse("订单不存在");
}
if ("SUCCESS".equals(callbackResult.getStatus())) {
order.setStatus("SUCCESS");
order.setChannelTransactionId(
callbackResult.getChannelTransactionId());
order.setSuccessTime(System.currentTimeMillis());
order.setUpdatedAt(System.currentTimeMillis());
paymentOrderService.update(order);
eventPublisher.publishPaymentSuccessEvent(order);
}
return buildSuccessResponse();
} catch (Exception e) {
log.error("微信回调处理异常", e);
return buildFailResponse("处理异常");
}
}
private String buildSuccessResponse() {
return "{\"code\":\"SUCCESS\",\"message\":\"成功\"}";
}
private String buildFailResponse(String message) {
return String.format("{\"code\":\"FAIL\",\"message\":\"%s\"}", message);
}
}
十、线上踩坑实录
坑一:双重支付问题
现象:用户点击支付按钮两次,渠道扣了两次款。
原因:前端防抖没做好 + 后端幂等性缺失。
解决方案:
/**
* 幂等性检查器
* 使用Redis的SET NX EX命令实现分布式幂等锁
*/
@Slf4j
@Service
public class IdempotentChecker {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 尝试获取幂等锁
* @param key 幂等键
* @return true-获取成功,false-已存在
*/
public boolean tryAcquire(String key) {
String redisKey = "idempotent:" + key;
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(redisKey, "1", 24, TimeUnit.HOURS);
return Boolean.TRUE.equals(result);
}
/**
* 释放幂等锁(在支付完成后的对账中清理)
*/
public void release(String key) {
redisTemplate.delete("idempotent:" + key);
}
}
坑二:回调地址被篡改
现象:攻击者截获回调请求,修改通知地址到自己的服务器。
原因:没有对回调地址做合法性校验。
解决方案:
/**
* 回调地址白名单校验
*/
public class NotifyUrlValidator {
private static final Set<String> ALLOWED_HOSTS = new HashSet<>(Arrays.asList(
"api.example.com",
"pay.example.com"
));
public static boolean isValid(String notifyUrl) {
if (StringUtils.isBlank(notifyUrl)) {
return false;
}
try {
URI uri = new URI(notifyUrl);
String host = uri.getHost();
return ALLOWED_HOSTS.contains(host);
} catch (URISyntaxException e) {
return false;
}
}
}
坑三:对账时间窗口问题
现象:对账单下载的是T日的,但实际交易可能跨天。
原因:没有考虑渠道对账单的延迟。
解决方案:
/**
* 对账时间窗口配置
* 对账单延迟2天下载,确保所有交易都已入账
*/
@Configuration
public class ReconciliationConfig {
/**
* 对账单延迟天数(T+N)
*/
@Value("${reconciliation.statement.delay-days:2}")
private int statementDelayDays;
/**
* 对账批次大小
*/
@Value("${reconciliation.batch.size:1000}")
private int batchSize;
/**
* 对账超时时间(分钟)
*/
@Value("${reconciliation.timeout.minutes:30}")
private int timeoutMinutes;
/**
* 获取对账日期(T-N)
*/
public LocalDate getReconcileDate() {
return LocalDate.now().minusDays(statementDelayDays);
}
}
坑四:支付超时与渠道状态不同步
现象:本地认为支付超时关闭了,但渠道实际已扣款。
原因:超时判断逻辑有问题,主动查询渠道的机制不完善。
解决方案:
/**
* 支付状态同步任务
* 每小时执行一次,同步所有待确认状态的支付单
*/
@Slf4j
@Scheduled(cron = "0 0 * * * ?")
public void syncPaymentStatus() {
log.info("开始同步支付状态");
List<PaymentOrder> pendingOrders = paymentOrderService
.findPendingOrders();
for (PaymentOrder order : pendingOrders) {
try {
PaymentChannelAdapter adapter = adapterRegistry
.getAdapter(order.getChannelCode());
PaymentQueryResponse query = adapter.query(order.getPaymentNo());
if (query.isSuccess() && "SUCCESS".equals(query.getStatus())) {
// 渠道已支付,更新本地状态
order.setStatus("SUCCESS");
order.setChannelTransactionId(query.getChannelTransactionId());
order.setSuccessTime(System.currentTimeMillis());
paymentOrderService.update(order);
eventPublisher.publishPaymentSuccessEvent(order);
log.info("同步支付成功, paymentNo={}", order.getPaymentNo());
}
} catch (Exception e) {
log.error("同步支付状态异常, paymentNo={}",
order.getPaymentNo(), e);
}
}
log.info("支付状态同步完成,共处理 {} 条记录", pendingOrders.size());
}
十一、监控与告警
没有监控的支付系统是裸奔。
/**
* 支付监控指标
*/
@Slf4j
@Component
public class PaymentMetrics {
@Autowired
private MeterRegistry meterRegistry;
/**
* 支付成功率
*/
public void recordPaymentSuccess(String channelCode) {
meterRegistry.counter("payment.success",
"channel", channelCode).increment();
}
/**
* 支付失败率
*/
public void recordPaymentFail(String channelCode, String errorCode) {
meterRegistry.counter("payment.fail",
"channel", channelCode,
"error_code", errorCode).increment();
}
/**
* 支付耗时
*/
public void recordPaymentDuration(String channelCode, long durationMs) {
meterRegistry.timer("payment.duration",
"channel", channelCode).record(Duration.ofMillis(durationMs));
}
/**
* 对账差异数
*/
public void recordReconciliationDifference(String channelCode,
DifferenceType type) {
meterRegistry.counter("reconciliation.difference",
"channel", channelCode,
"type", type.name()).increment();
}
}
/**
* 支付告警服务
*/
@Slf4j
@Service
public class AlertService {
@Autowired
private DingTalkNotifier dingTalkNotifier;
@Autowired
private EmailNotifier emailNotifier;
/**
* 发送对账差异告警
*/
public void sendReconciliationAlert(ReconciliationReport report) {
String message = String.format(
"【对账告警】渠道: %s, 日期: %s, 差异数: %d, 是否平衡: %s",
report.getChannelCode(),
report.getReconcileDate(),
report.getDifferenceCount(),
report.isBalanced());
dingTalkNotifier.send(message);
emailNotifier.send("对账差异告警", message);
log.warn("对账差异告警已发送: {}", message);
}
/**
* 发送紧急告警(金额不一致等严重问题)
*/
public void sendCriticalAlert(String message) {
// 紧急告警直接打电话
dingTalkNotifier.sendCritical(message);
emailNotifier.sendCritical("支付系统紧急告警", message);
log.error("紧急告警已发送: {}", message);
}
}
十二、上线前的 checklist
最后分享一个上线前的检查清单,这些都是血泪教训:
□ 1. 所有金额字段使用Long(分),不用double/float
□ 2. 所有支付操作都有幂等性保证
□ 3. 回调地址有白名单校验
□ 4. 签名验证在所有回调入口都执行
□ 5. 渠道超时时间合理配置(建议30秒)
□ 6. 分布式锁覆盖所有补单操作
□ 7. 对账差异有自动处理 + 人工处理双重机制
□ 8. 关键操作有完整日志(订单号、金额、时间)
□ 9. 监控指标覆盖成功率、耗时、差异数
□ 10. 告警规则配置合理(不要过于敏感也不要漏报)
□ 11. 回滚方案准备就绪
□ 12. 压测通过(至少模拟10倍预期峰值流量)
□ 13. 灰度发布策略明确
□ 14. 值班表排好,关键时期有人值守
□ 15. 应急预案文档就绪
支付系统没有完美的设计,只有不断迭代的经验。这套库我们也在持续优化,现在已经在处理日均百万级的支付请求了。如果你正在做类似的项目,希望这些经验能帮到你。有什么问题欢迎交流,咱们一起把支付系统做得更稳。
