支付系统设计
问题
如何设计一个可靠的支付系统?
答案
支付流程
核心表设计
支付单表
CREATE TABLE payment_order (
id BIGINT PRIMARY KEY,
payment_no VARCHAR(64) UNIQUE, -- 支付单号
order_no VARCHAR(64), -- 关联订单号
channel VARCHAR(32), -- 支付渠道(WECHAT/ALIPAY)
amount DECIMAL(10,2), -- 支付金额
status TINYINT, -- 0待支付 1支付中 2已支付 3已退款
third_party_no VARCHAR(128), -- 第三方流水号
callback_time DATETIME,
created_at DATETIME,
INDEX idx_order (order_no)
);
幂等处理
支付回调幂等
@Transactional
public void handleCallback(PaymentCallback callback) {
PaymentOrder payment = paymentMapper.findByPaymentNo(callback.getPaymentNo());
// 幂等:已处理过的直接返回成功
if (payment.getStatus() == PaymentStatus.PAID) return;
// CAS 更新状态:待支付 → 已支付
int rows = paymentMapper.updateStatus(
payment.getId(), PaymentStatus.UNPAID, PaymentStatus.PAID
);
if (rows == 0) return; // 并发场景下已被其他线程处理
// 通知订单服务
orderService.onPaymentSuccess(payment.getOrderNo());
}
对账系统
每日对账流程
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨 1 点
public void dailyReconciliation() {
LocalDate yesterday = LocalDate.now().minusDays(1);
// 1. 拉取第三方支付账单
List<ThirdPartyBill> thirdBills = paymentChannel.downloadBill(yesterday);
// 2. 查询本地支付记录
List<PaymentOrder> localOrders = paymentMapper.findByDate(yesterday);
// 3. 对比差异
Map<String, ThirdPartyBill> thirdMap = thirdBills.stream()
.collect(Collectors.toMap(ThirdPartyBill::getPaymentNo, Function.identity()));
for (PaymentOrder local : localOrders) {
ThirdPartyBill third = thirdMap.remove(local.getPaymentNo());
if (third == null) {
// 本地有但第三方没有 → 可能支付失败未回调
handleLocalExtra(local);
} else if (!local.getAmount().equals(third.getAmount())) {
// 金额不一致 → 告警人工处理
alertService.sendAlert("金额不一致: " + local.getPaymentNo());
}
}
// thirdMap 中剩余的:第三方有但本地没有 → 可能漏单
thirdMap.values().forEach(this::handleThirdExtra);
}
常见面试问题
Q1: 如何保证支付不重复扣款?
答案:
- 唯一支付单号:同一订单只能创建一个支付单
- 幂等回调:用支付单号做幂等键,CAS 更新状态
- 对账兜底:每日对账发现异常自动或人工处理
Q2: 退款怎么设计?
答案:
- 创建退款单关联原支付单
- 调用第三方退款接口
- 退款也是异步回调,需要幂等处理
- 部分退款记录已退金额,总退款 ≤ 原支付金额
Q3: 支付掉单怎么处理?
答案:
- 主动查询:定时任务扫描超时未回调的支付单(如 5 分钟),主动调第三方查询接口
- 补偿回调:查到已支付则补充执行后续逻辑
- 对账兜底:T+1 对账修复遗漏