支付库开发从0到1常见坑位案例解析接口设计并发处理安全验证对账流程实战经验总结
前言:为什么要写这篇
说实话,写这篇不是因为我多牛,而是我踩过的坑比你想象的多得多。三年前我做第一个支付系统的时候,天真地以为把接口写好、签名验证做全就行了,结果上线第一天就出事了——一笔重复扣款,用户投诉,财务对不上账。从那天起,我才真正开始理解什么是支付系统。
这篇东西会把我们从接口设计到最终对账的整个过程,连同那些让人头秃的坑,全部摊开来讲。你如果也在做或者准备做支付库,这篇文章应该能帮你省掉几个通宵。
接口设计:别只看表面,细节全是坑
1. 幂等性设计——支付系统的第一道防线
很多人做接口的时候忽略幂等性,觉得”用户不会重复提交”。现实是:网络抖动、用户多点、重试机制……任何一个都可能触发重复请求。
真实案例:我们有一次遇到一个问题,用户在支付页面点了确认,因为网络卡了一下,他又点了一次。两次请求都到了我们的服务器,结果扣了两次钱。用户投诉的时候我们一脸懵,因为两个请求的订单号不一样——用户在界面上看到的订单号和后端生成的订单号对不上。
解决方案:幂等Key的设计。
/**
* 支付请求幂等校验
*/
public class IdempotentHandler {
private final RedisTemplate<String, String> redisTemplate;
/**
* 幂等Key的生成规则:
* 请求来源 + 商户ID + 订单号 + 请求参数哈希
*/
public String generateIdempotentKey(PaymentRequest request) {
String keySource = "payment_"
+ request.getSource() // 请求来源:APP/WEB/H5
+ "_" + request.getMchId() // 商户ID
+ "_" + request.getOrderId() // 商户订单号
+ "_" + md5(request.getBody()); // 请求体哈希
return keySource;
}
/**
* 幂等校验:先设后查,避免竞态条件
*/
public boolean tryAcquire(String idempotentKey) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(idempotentKey, "1", 24, TimeUnit.HOURS);
return Boolean.TRUE.equals(result);
}
}
关键点:
- 使用
SET NX EX原子操作,保证并发场景下的安全性 - 过期时间设为24小时,覆盖正常支付流程
- 幂等Key的生成要足够唯一,避免误判
2. 接口参数设计:字段越多越好?错
错误示范:
public class PaymentRequest {
private String orderId; // 订单号
private String amount; // 金额
private String currency; // 币种
private String mchId; // 商户ID
private String timeStamp; // 时间戳
private String nonceStr; // 随机字符串
private String sign; // 签名
private String signType; // 签名算法
private String body; // 商品描述
private String detail; // 商品详情
private String attach; // 附加数据
private String notifyUrl; // 回调地址
private String tradeType; // 交易类型
private String productId; // 商品ID
private String openId; // 用户openid
// ... 还有20多个字段
}
这个设计看起来”全面”,但实际上:
- 新增字段时需要全链路改动
- 字段太多,参数校验工作量爆炸
- 不同渠道的字段差异难以处理
推荐设计:
public class PaymentRequest {
// ===== 核心必填字段 =====
private String orderId; // 商户订单号(唯一)
private Long amount; // 金额,单位:分
private String currency; // 币种,默认CNY
private String mchId; // 商户ID
// ===== 请求元数据 =====
private String timeStamp; // 请求时间戳
private String nonceStr; // 随机字符串
private String sign; // 签名
private String signType; // 签名算法:MD5/HMAC-SHA256
// ===== 业务扩展字段(使用扩展参数Map)=====
private Map<String, String> extParams; // 渠道特有参数
// ===== 异步通知配置 =====
private String notifyUrl; // 回调地址(可为空,使用默认)
// Getter/Setter省略
}
好处:
- 核心字段明确,必填校验简单
- 扩展字段用Map存放,各渠道差异化需求互不影响
- 新增渠道时不需要改主结构
3. 签名设计:别被黑客教做人
签名是支付安全的核心,但很多人做得一塌糊涂。
常见错误:
// 错误:直接用对象toString后签名
String content = request.toString() + key;
String sign = MD5(content);
问题:
- toString()的输出格式不稳定,序列化顺序可能导致签名不一致
- 没有排除空值字段
- 没有统一的字段排序规则
正确做法:
public class PaymentSignUtils {
private static final String SEPARATOR = "|";
private static final List<String> EXCLUDE_KEYS = Arrays.asList(
"sign", "signType", "nonceStr", "timeStamp"
);
/**
* 生成签名
*/
public static String sign(PaymentRequest request, String apiKey) {
// 1. 构建待签名字符串(按字段名排序)
StringBuilder sb = new StringBuilder();
// 按字母顺序排序所有非空、非排除字段
TreeMap<String, String> sortedParams = new TreeMap<>();
Map<String, Object> paramMap = requestToMap(request);
paramMap.forEach((k, v) -> {
if (v != null && !EXCLUDE_KEYS.contains(k) && !k.equals("sign")) {
sortedParams.put(k, String.valueOf(v));
}
});
// 拼接:key=value&key=value|apiKey
sortedParams.forEach((k, v) -> {
sb.append(k).append("=").append(v).append("&");
});
sb.append(apiKey);
// 2. 根据signType选择算法
String signType = request.getSignType() != null
? request.getSignType()
: "MD5";
if ("HMAC-SHA256".equals(signType)) {
return hmacSha256(sb.toString(), apiKey);
} else {
return md5(sb.toString());
}
}
/**
* 验证签名
*/
public static boolean verify(PaymentRequest request, String apiKey) {
String expectedSign = sign(request, apiKey);
return expectedSign.equals(request.getSign());
}
private static String hmacSha256(String data, String key) {
try {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "HmacSHA256");
sha256Hmac.init(secretKey);
byte[] hash = sha256Hmac.doFinal(data.getBytes());
return BytesUtil.bytesToHex(hash);
} catch (Exception e) {
throw new RuntimeException("HMAC-SHA256签名失败", e);
}
}
}
关键点:
- 字段排序必须统一(用TreeMap)
- 排除sign本身和系统字段
- 支持多种签名算法,方便升级
- 空值字段不参与签名
并发处理:支付系统最头疼的问题
1. 并发重复支付问题
场景:用户快速点击两次支付按钮,两个请求同时到达服务器。
错误处理:
// 先查订单状态,再决定如何处理
Order order = orderRepository.findById(orderId);
if (order.getStatus() == OrderStatus.PENDING) {
// 处理支付
pay(order);
}
问题:两个请求同时通过if判断,都会执行pay(),导致重复支付。
正确做法:
/**
* 使用数据库乐观锁或分布式锁保证并发安全
*/
public PaymentResult processPayment(PaymentRequest request) {
String orderId = request.getOrderId();
// 方式一:数据库乐观锁(推荐)
int updated = orderRepository.updateStatus(
orderId,
OrderStatus.PENDING, // 期望当前状态
OrderStatus.PAYING // 目标状态
);
if (updated == 0) {
// 状态已被修改,说明有其他请求在处理
Order existingOrder = orderRepository.findById(orderId);
if (existingOrder.getStatus() == OrderStatus.PENDING) {
// 并发竞争失败,返回重复支付提示
throw new PaymentException("支付处理中,请勿重复提交");
}
// 已经支付成功或其他状态,直接返回结果
return queryPaymentResult(orderId);
}
// 乐观锁成功,继续支付流程
return executePayment(request);
}
-- 对应的SQL
UPDATE orders
SET status = 'PAYING', version = version + 1
WHERE order_id = #{orderId}
AND status = 'PENDING'
AND version = #{version}
方式二:分布式锁(高并发场景)
/**
* 基于Redis的分布式锁
*/
public class PaymentLock {
private static final String LOCK_PREFIX = "payment_lock_";
private static final long LOCK_EXPIRE_MS = 3000; // 3秒
private final RedisTemplate<String, String> redisTemplate;
/**
* 获取锁
*/
public boolean acquireLock(String orderId) {
String lockKey = LOCK_PREFIX + orderId;
String requestId = UUID.randomUUID().toString();
// SET NX EX,原子操作
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, LOCK_EXPIRE_MS, TimeUnit.MILLISECONDS);
if (Boolean.TRUE.equals(result)) {
// 将requestId存入ThreadLocal,用于后续释放
LockContext.setRequestId(requestId);
return true;
}
return false;
}
/**
* 释放锁(必须校验requestId,避免误删其他请求的锁)
*/
public void releaseLock(String orderId) {
String lockKey = LOCK_PREFIX + orderId;
String requestId = LockContext.getRequestId();
// Lua脚本保证删除的原子性
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
requestId
);
}
}
2. 资金安全:金额计算的坑
错误做法:使用Double进行金额计算
// 绝对不要用!
double amount = 0.1 + 0.2; // 结果是0.30000000000000004
正确做法:使用BigDecimal,并且用分作为最小单位
public class PaymentAmount {
/**
* 金额存储:使用Long,单位:分
* 避免浮点数精度问题
*/
private final long amountInCents;
public PaymentAmount(long amountInCents) {
if (amountInCents < 0) {
throw new IllegalArgumentException("金额不能为负数");
}
this.amountInCents = amountInCents;
}
/**
* 从元转换(String避免精度丢失)
*/
public static PaymentAmount fromYuan(String yuanStr) {
BigDecimal yuan = new BigDecimal(yuanStr);
long cents = yuan.scaleByPowerOfTen(2).longValueExact();
return new PaymentAmount(cents);
}
/**
* 金额相加
*/
public PaymentAmount add(PaymentAmount other) {
return new PaymentAmount(this.amountInCents + other.amountInCents);
}
/**
* 金额相减
*/
public PaymentAmount subtract(PaymentAmount other) {
long result = this.amountInCents - other.amountInCents;
if (result < 0) {
throw new IllegalArgumentException("余额不足");
}
return new PaymentAmount(result);
}
/**
* 转换为元字符串
*/
public String toYuanString() {
return BigDecimal.valueOf(amountInCents)
.scaleByPowerOfTen(-2)
.toPlainString();
}
public long getAmountInCents() {
return amountInCents;
}
}
3. 数据库连接池的配置陷阱
错误配置:
spring:
datasource:
hikari:
maximum-pool-size: 10 # 太小
minimum-idle: 5
connection-timeout: 30000 # 太短
idle-timeout: 600000
max-lifetime: 1800000
问题:
- 连接池太小,高峰期所有线程阻塞等待连接
- 超时时间太短,网络抖动时直接报错
推荐配置:
spring:
datasource:
hikari:
maximum-pool-size: 50 # 根据实际并发调整
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000 # 10分钟
max-lifetime: 1800000 # 30分钟
leak-detection-threshold: 60000 # 60秒检测连接泄漏
connection-test-query: SELECT 1 # 连接有效性检测
监控指标:
- Active connections:活跃连接数
- Pending requests:等待连接的请求数
- Connection acquisition time:获取连接的平均时间
安全验证:别让支付变成提款机
1. 双重签名验证
场景:支付请求从商户服务器传到我们服务器,再从我们服务器转发到支付渠道。每一层都需要签名验证。
/**
* 支付链路签名验证
*/
public class PaymentSignatureVerifier {
private final Map<String, String> merchantKeys; // 商户ID -> API密钥
/**
* 验证商户请求签名
*/
public void verifyMerchantRequest(PaymentRequest request) {
String mchId = request.getMchId();
String apiKey = merchantKeys.get(mchId);
if (apiKey == null) {
throw new SecurityException("未知的商户ID: " + mchId);
}
// 验证时间戳(防止重放攻击,有效期5分钟)
long requestTime = Long.parseLong(request.getTimeStamp());
long now = System.currentTimeMillis() / 1000;
if (Math.abs(now - requestTime) > 300) {
throw new SecurityException("请求已过期");
}
// 验证签名
String expectedSign = PaymentSignUtils.sign(request, apiKey);
if (!expectedSign.equals(request.getSign())) {
throw new SecurityException("签名验证失败");
}
// 验证金额(防止篡改)
validateAmount(request);
}
/**
* 验证金额一致性
*/
private void validateAmount(PaymentRequest request) {
// 1. 查询订单金额
Order order = orderRepository.findByOrderId(request.getOrderId());
if (order == null) {
throw new BusinessException("订单不存在");
}
// 2. 比较金额
long orderAmount = order.getAmountInCents();
long requestAmount = request.getAmount().getAmountInCents();
if (orderAmount != requestAmount) {
log.warn("金额不一致, orderId={}, orderAmount={}, requestAmount={}",
request.getOrderId(), orderAmount, requestAmount);
throw new SecurityException("订单金额不一致");
}
}
}
2. 防重放攻击
方案一:时间戳校验
/**
* 请求时间戳校验
*/
public class ReplayAttackGuard {
private static final long MAX_TIME_DIFF_MS = 5 * 60 * 1000; // 5分钟
public void validateTimeStamp(String timeStampStr) {
long requestTime = Long.parseLong(timeStampStr) * 1000; // 秒转毫秒
long now = System.currentTimeMillis();
if (Math.abs(now - requestTime) > MAX_TIME_DIFF_MS) {
throw new SecurityException("请求超时");
}
}
}
方案二:Nonce唯一性校验
/**
* Nonce唯一性校验,防止重放
*/
public class NonceGuard {
private final RedisTemplate<String, String> redisTemplate;
private static final long NONCE_EXPIRE_SECONDS = 60 * 60; // 1小时
/**
* 检查Nonce是否已使用
*/
public boolean isNonceUsed(String nonce) {
String key = "payment_nonce_" + nonce;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 标记Nonce为已使用
*/
public void markNonceUsed(String nonce) {
String key = "payment_nonce_" + nonce;
redisTemplate.opsForValue()
.set(key, "1", NONCE_EXPIRE_SECONDS, TimeUnit.SECONDS);
}
/**
* 原子操作:检查并标记
*/
public boolean checkAndMarkNonce(String nonce) {
String key = "payment_nonce_" + nonce;
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, "1", NONCE_EXPIRE_SECONDS, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
}
3. HTTPS证书校验
/**
* HTTPS证书校验配置
*/
@Configuration
public class HttpsConfig {
/**
* 信任所有证书(仅测试环境)
*/
@Bean
@Profile("test")
public CloseableHttpClient testHttpClient() throws KeyStoreException,
NoSuchAlgorithmException, KeyManagementException {
TrustStrategy trustStrategy = (X509Certificate[] chain, String authType) -> true;
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(null, trustStrategy)
.build();
return HttpClients.custom()
.setSSLContext(sslContext)
.build();
}
/**
* 严格证书校验(生产环境)
*/
@Bean
@Profile("prod")
public CloseableHttpClient prodHttpClient() {
return HttpClients.createDefault();
}
}
生产环境注意事项:
- 禁止跳过证书校验
- 定期更新证书
- 配置证书过期提醒
对账流程:钱对不上时的救星
1. 对账的核心逻辑
对账的本质:我们的记录 vs 渠道的记录,不一致的就是问题。
/**
* 对账任务
*/
public class ReconciliationService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ChannelStatementService channelStatementService;
@Autowired
private ReconciliationRepository reconciliationRepository;
/**
* 执行对账
*/
public ReconciliationResult reconcile(Date reconciliationDate) {
// 1. 获取渠道对账单
ChannelStatement statement = channelStatementService
.fetchStatement(reconciliationDate);
// 2. 获取我们的订单
List<Order> orders = orderRepository.findByDate(reconciliationDate);
// 3. 构建订单Map,方便查找
Map<String, Order> orderMap = orders.stream()
.collect(Collectors.toMap(Order::getChannelOrderId, o -> o));
// 4. 逐条比对
List<ReconciliationItem> items = new ArrayList<>();
for (StatementRecord record : statement.getRecords()) {
ReconciliationItem item = new ReconciliationItem();
item.setChannelOrderId(record.getOrderId());
item.setChannelAmount(record.getAmount());
item.setChannelStatus(record.getStatus());
item.setReconciliationDate(reconciliationDate);
Order order = orderMap.get(record.getOrderId());
if (order == null) {
// 渠道有,我们无
item.setStatus(ReconciliationStatus.CHANNEL_ONLY);
item.setDescription("渠道有记录,我方无订单");
} else {
// 对比金额
if (order.getAmountInCents() != record.getAmount()) {
item.setStatus(ReconciliationStatus.AMOUNT_MISMATCH);
item.setDescription(
String.format("金额不一致: 我方=%d, 渠道=%d",
order.getAmountInCents(), record.getAmount())
);
}
// 对比状态
else if (!order.getStatus().equals(record.getStatus())) {
item.setStatus(ReconciliationStatus.STATUS_MISMATCH);
item.setDescription(
String.format("状态不一致: 我方=%s, 渠道=%s",
order.getStatus(), record.getStatus())
);
} else {
item.setStatus(ReconciliationStatus.MATCHED);
}
}
items.add(item);
}
// 5. 检查我方有但渠道无的订单
for (Order order : orders) {
if (!orderMap.containsKey(order.getChannelOrderId())) {
// 这种情况应该在第一步就处理了,这里是双重保险
}
}
// 6. 保存对账结果
ReconciliationResult result = new ReconciliationResult();
result.setItems(items);
result.setTotalCount(items.size());
result.setMatchedCount(items.stream()
.filter(i -> i.getStatus() == ReconciliationStatus.MATCHED)
.count());
result.setUnmatchedCount(items.size() - result.getMatchedCount());
reconciliationRepository.save(result);
return result;
}
}
2. 差错处理流程
/**
* 差错处理
*/
public class DiscrepancyHandler {
/**
* 差错类型处理策略
*/
public enum DiscrepancyType {
CHANNEL_ONLY, // 渠道有,我方无
ORDER_ONLY, // 我方有,渠道无
AMOUNT_MISMATCH, // 金额不一致
STATUS_MISMATCH // 状态不一致
}
/**
* 处理差错
*/
public void handle(ReconciliationItem item) {
switch (item.getStatus()) {
case CHANNEL_ONLY:
handleChannelOnly(item);
break;
case ORDER_ONLY:
handleOrderOnly(item);
break;
case AMOUNT_MISMATCH:
handleAmountMismatch(item);
break;
case STATUS_MISMATCH:
handleStatusMismatch(item);
break;
default:
break;
}
}
/**
* 渠道有,我方无:需要补单
*/
private void handleChannelOnly(ReconciliationItem item) {
// 1. 创建待补单记录
补单订单补单 = new 补单订单();
补单.setChannelOrderId(item.getChannelOrderId());
补单.setAmount(item.getChannelAmount());
补单.setStatus(补单状态.待处理);
补单Repository.save(补单);
// 2. 发送告警
alertService.sendAlert("发现渠道单边账: " + item.getChannelOrderId());
}
/**
* 金额不一致:需要人工介入
*/
private void handleAmountMismatch(ReconciliationItem item) {
// 1. 冻结相关订单
orderRepository.freezeByChannelOrderId(item.getChannelOrderId());
// 2. 创建差错工单
差错工单工单 = new 差错工单();
工单.setType(差错类型.金额不一致);
工单.setChannelOrderId(item.getChannelOrderId());
工单.setDescription(item.getDescription());
工单.setStatus(工单状态.待处理);
工单Repository.save(工单);
// 3. 发送告警
alertService.sendAlert("金额不一致,需要人工处理: " + item.getChannelOrderId());
}
}
3. 对账时间窗口设计
问题:T+1对账,但渠道账单可能有延迟。
解决方案:多时间窗口对账
/**
* 多时间窗口对账
*/
public class MultiWindowReconciliation {
/**
* 对账时间窗口:D日账单可能在D+1~D+3日到达
*/
public void reconcileWithWindows(Date targetDate) {
// 当前窗口:T+1
Set<Date> reconciledDates = new HashSet<>();
reconcileOnce(targetDate, reconciledDates);
// 补窗口:T+2
Date window2 = DateUtils.addDays(targetDate, 1);
reconcileOnce(window2, reconciledDates);
// 补窗口:T+3
Date window3 = DateUtils.addDays(targetDate, 2);
reconcileOnce(window3, reconciledDates);
}
private void reconcileOnce(Date date, Set<Date> reconciledDates) {
if (reconciledDates.contains(date)) {
return;
}
ReconciliationResult result = reconciliationService.reconcile(date);
reconciledDates.add(date);
// 处理差错
discrepancyHandler.processAll(result.getItems());
}
}
实战经验总结:那些文档不会告诉你的事
1. 日志是救命稻草
支付系统出问题的时候,日志比人靠谱。
/**
* 支付日志记录
*/
public class PaymentLogger {
private static final Logger log = LoggerFactory.getLogger(PaymentLogger.class);
/**
* 记录支付请求
*/
public void logPaymentRequest(PaymentRequest request, String requestId) {
log.info("[支付请求] requestId={}, mchId={}, orderId={}, amount={}, source={}",
requestId,
request.getMchId(),
request.getOrderId(),
request.getAmount().getAmountInCents(),
request.getSource()
);
}
/**
* 记录支付响应
*/
public void logPaymentResponse(String requestId, PaymentResponse response, long costMs) {
log.info("[支付响应] requestId={}, resultCode={}, resultMsg={}, cost={}ms",
requestId,
response.getResultCode(),
response.getResultMsg(),
costMs
);
}
/**
* 记录异常
*/
public void logPaymentError(String requestId, Exception e) {
log.error("[支付异常] requestId={}", requestId, e);
}
}
关键日志字段:
- requestId:请求唯一标识
- mchId:商户ID
- orderId:订单号
- amount:金额
- channel:支付渠道
- resultCode:结果码
- cost:耗时
2. 监控告警:别等用户投诉
/**
* 支付监控
*/
public class PaymentMonitor {
/**
* 监控指标
*/
public void recordMetrics(String metricName, double value) {
// 使用Micrometer记录指标
MeterRegistry registry = MeterRegistrySingleton.get();
registry.counter(metricName).increment();
}
/**
* 成功率监控
*/
public void recordPaymentResult(boolean success) {
String metric = success ? "payment.success" : "payment.failure";
recordMetrics(metric, 1);
}
/**
* 延迟监控
*/
public void recordPaymentLatency(long latencyMs) {
Timer.builder("payment.latency")
.register(MeterRegistrySingleton.get())
.record(latencyMs, TimeUnit.MILLISECONDS);
}
}
关键告警规则:
- 支付成功率低于95%
- 平均响应时间超过2秒
- 单笔支付金额超过阈值
- 同一商户短时间内大量支付
3. 降级策略
/**
* 支付降级
*/
public class PaymentDegradation {
/**
* 支付渠道熔断
*/
private final CircuitBreaker circuitBreaker;
/**
* 执行支付,带熔断保护
*/
public PaymentResult payWithFallback(PaymentRequest request) {
try {
// 尝试正常支付
return paymentService.pay(request);
} catch (PaymentException e) {
// 降级:尝试其他渠道
log.warn("主渠道支付失败,尝试降级: {}", e.getMessage());
return fallbackPay(request);
}
}
/**
* 降级支付
*/
private PaymentResult fallbackPay(PaymentRequest request) {
// 1. 尝试备选渠道
List<PaymentChannel> fallbackChannels = getFallbackChannels();
for (PaymentChannel channel : fallbackChannels) {
try {
PaymentRequest fallbackRequest = adaptRequest(request, channel);
return channel.pay(fallbackRequest);
} catch (Exception e) {
log.warn("降级渠道{}失败: {}", channel.getName(), e.getMessage());
}
}
// 2. 全部失败,返回失败
throw new PaymentException("所有支付渠道均不可用");
}
}
4. 数据备份与恢复
/**
* 支付数据备份
*/
public class PaymentBackupService {
/**
* 每日全量备份
*/
@Scheduled(cron = "0 2 0 * * ?") // 每天凌晨2点
public void dailyBackup() {
// 1. 备份订单数据
orderRepository.exportAll("orders_" + LocalDate.now());
// 2. 备份流水数据
transactionRepository.exportAll("transactions_" + LocalDate.now());
// 3. 备份对账单
reconciliationRepository.exportAll("reconciliation_" + LocalDate.now());
log.info("支付数据备份完成");
}
/**
* 数据恢复
*/
public void restoreFromBackup(String backupDate) {
// 1. 停止写入
dataLockService.lock();
try {
// 2. 恢复数据
orderRepository.importFrom(backupDate);
transactionRepository.importFrom(backupDate);
// 3. 验证数据一致性
validateDataConsistency();
// 4. 解除锁定
dataLockService.unlock();
log.info("数据恢复完成: {}", backupDate);
} catch (Exception e) {
// 恢复失败,解除锁定
dataLockService.unlock();
throw e;
}
}
}
最后的话
做支付系统,敬畏心最重要。每一分钱都是用户的,我们要像对待自己的钱一样对待每一笔支付。
这篇文章里提到的坑,都是我踩过的。有些坑我踩了一次,有些坑我踩了两次、三次……希望这篇文章能让你少踩几个坑。
如果还有什么问题,欢迎交流。支付系统这条路,一个人走很快,一群人走很远。
