java 调用第三方支付接口,怎么防止错付或者多付?

防止错付和多付是支付系统的关键,以下是Java调用第三方支付接口的防护措施:

一、核心防护策略

1. 幂等性设计

@Service
public class PaymentService {
    // 使用数据库唯一索引或分布式锁
    @Transactional
    public PaymentResult pay(PaymentRequest request) {
        // 1. 生成唯一幂等键
        String idempotentKey = generateIdempotentKey(request);
        
        // 2. 检查是否已处理
        PaymentRecord record = paymentRecordDao.selectByIdempotentKey(idempotentKey);
        if (record != null && record.getStatus() == PaymentStatus.SUCCESS) {
            return PaymentResult.duplicate(record);
        }
        
        // 3. 插入幂等记录(数据库唯一约束防止重复)
        try {
            paymentRecordDao.insertIdempotentRecord(idempotentKey, PaymentStatus.PROCESSING);
        } catch (DuplicateKeyException e) {
            throw new BusinessException("重复支付请求");
        }
        
        // 4. 执行业务逻辑
        return doPay(request, idempotentKey);
    }
    
    private String generateIdempotentKey(PaymentRequest request) {
        return String.format("PAY:%s:%s:%s", 
            request.getMerchantId(),
            request.getOrderNo(),
            request.getAmount()
        );
    }
}

2. 金额校验机制

@Component
public class AmountValidator {
    
    public void validatePayment(PaymentRequest request) {
        // 1. 前置校验
        if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new ValidationException("金额必须大于0");
        }
        
        // 2. 金额范围限制
        if (request.getAmount().compareTo(new BigDecimal("1000000")) > 0) {
            throw new ValidationException("单笔支付金额超过上限");
        }
        
        // 3. 与订单金额对比
        Order order = orderService.getOrder(request.getOrderNo());
        if (order.getAmount().compareTo(request.getAmount()) != 0) {
            log.warn("支付金额与订单金额不一致: order={}, pay={}", 
                order.getAmount(), request.getAmount());
            throw new ValidationException("支付金额不匹配");
        }
        
        // 4. 小数位数校验
        if (request.getAmount().scale() > 2) {
            throw new ValidationException("金额小数位数最多2位");
        }
    }
}

二、并发控制

3. 分布式锁控制

@Service
public class ConcurrentPaymentService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    public PaymentResult safePay(PaymentRequest request) {
        String lockKey = "PAY_LOCK:" + request.getOrderNo();
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试获取锁,等待5秒,持有30秒
            if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
                return processPayment(request);
            } else {
                throw new BusinessException("支付处理中,请稍后");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BusinessException("支付处理被中断");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

三、支付流程控制

4. 状态机管理

@Component
public class PaymentStateMachine {
    
    private StateMachine<PaymentState, PaymentEvent> stateMachine;
    
    @Transactional
    public PaymentResult processPayment(String orderNo, PaymentEvent event) {
        // 1. 获取当前状态
        PaymentState currentState = getCurrentState(orderNo);
        
        // 2. 状态转移校验
        if (!canTransition(currentState, event)) {
            throw new IllegalStateException("状态转移非法");
        }
        
        // 3. 发送状态事件
        boolean accepted = stateMachine.sendEvent(
            MessageBuilder.withPayload(event)
                .setHeader("orderNo", orderNo)
                .build()
        );
        
        if (!accepted) {
            throw new BusinessException("支付状态更新失败");
        }
    }
    
    private boolean canTransition(PaymentState from, PaymentEvent event) {
        // 定义允许的状态转移
        Map<PaymentState, Set<PaymentEvent>> transitions = Map.of(
            PaymentState.INIT, Set.of(PaymentEvent.PAY_REQUEST),
            PaymentState.PROCESSING, Set.of(PaymentEvent.PAY_SUCCESS, PaymentEvent.PAY_FAIL),
            PaymentState.SUCCESS, Set.of(PaymentEvent.REFUND_REQUEST) // 成功后只能退款
        );
        return transitions.getOrDefault(from, Collections.emptySet()).contains(event);
    }
}

四、对账与监控

5. 定时对账任务

@Component
@Slf4j
public class ReconciliationJob {
    
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
    public void dailyReconciliation() {
        // 1. 查询当天所有支付记录
        List<PaymentRecord> localRecords = paymentDao.getTodayRecords();
        
        // 2. 从第三方获取对账单
        List<ThirdPartyRecord> thirdPartyRecords = thirdPartyService.getDailyBill();
        
        // 3. 比对差异
        ReconciliationResult result = reconcile(localRecords, thirdPartyRecords);
        
        if (!result.isBalanced()) {
            // 4. 告警并处理差异
            alertService.sendAlert(result.getDiscrepancies());
            handleDiscrepancies(result.getDiscrepancies());
        }
        
        // 5. 记录对账结果
        reconciliationDao.saveResult(result);
    }
    
    private ReconciliationResult reconcile(List<PaymentRecord> local, 
                                         List<ThirdPartyRecord> thirdParty) {
        ReconciliationResult result = new ReconciliationResult();
        
        // 金额汇总比对
        BigDecimal localTotal = local.stream()
            .map(PaymentRecord::getAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
            
        BigDecimal thirdPartyTotal = thirdParty.stream()
            .map(ThirdPartyRecord::getAmount)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
            
        result.setLocalTotal(localTotal);
        result.setThirdPartyTotal(thirdPartyTotal);
        
        // 找出差异记录
        findDiscrepancies(local, thirdParty, result);
        
        return result;
    }
}

6. 支付监控

@Component
@Slf4j
public class PaymentMonitor {
    
    @EventListener
    public void monitorPayment(PaymentEvent event) {
        // 实时监控支付异常
        Metrics.counter("payment.total", "status", event.getStatus().name()).increment();
        
        if (event.getStatus() == PaymentStatus.FAILED) {
            log.error("支付失败: orderNo={}, reason={}", 
                event.getOrderNo(), event.getErrorMessage());
            
            // 异常支付告警
            alertService.sendPaymentFailureAlert(event);
            
            // 记录到异常表用于人工核查
            exceptionDao.save(PaymentException.fromEvent(event));
        }
    }
}

五、完整支付流程示例

@Service
@Transactional
@Slf4j
public class SafePaymentService {
    
    public PaymentResult securePay(PaymentRequest request) {
        // 1. 参数校验
        validateRequest(request);
        
        // 2. 订单校验
        Order order = validateOrder(request.getOrderNo());
        
        // 3. 金额校验
        validateAmount(order, request.getAmount());
        
        // 4. 幂等性检查
        String idempotentKey = generateIdempotentKey(request);
        if (isDuplicateRequest(idempotentKey)) {
            return getExistingResult(idempotentKey);
        }
        
        // 5. 获取分布式锁
        try (LockGuard guard = distributedLock.lock(request.getOrderNo())) {
            
            // 6. 状态检查
            if (order.getStatus() == OrderStatus.PAID) {
                throw new BusinessException("订单已支付");
            }
            
            // 7. 创建支付记录
            PaymentRecord record = createPaymentRecord(request, idempotentKey);
            
            try {
                // 8. 调用第三方支付
                ThirdPartyResponse response = thirdPartyClient.pay(request);
                
                // 9. 更新支付状态
                updatePaymentStatus(record, response);
                
                // 10. 更新订单状态
                updateOrderStatus(order, OrderStatus.PAID);
                
                // 11. 记录成功日志
                logPaymentSuccess(record, response);
                
                return PaymentResult.success(record);
                
            } catch (Exception e) {
                // 12. 失败处理
                handlePaymentFailure(record, e);
                throw new PaymentException("支付失败", e);
            }
        }
    }
    
    private boolean isDuplicateRequest(String idempotentKey) {
        // 检查Redis或数据库
        return redisTemplate.hasKey("payment:idempotent:" + idempotentKey);
    }
}

六、配置建议

# application.yml
payment:
  security:
    # 金额限制
    max-amount: 1000000
    min-amount: 0.01
    # 频率限制
    rate-limit: 10  # 每分钟最大请求数
    # 超时设置
    timeout: 30s
  # 对账配置
  reconciliation:
    enabled: true
    cron: "0 0 2 * * ?"
    alert-threshold: 0.01  # 差异超过0.01元告警

七、最佳实践总结

  1. 事前预防
    • 前端金额二次确认
    • 支付密码/短信验证码
    • 支付限额控制
  2. 事中控制
    • 幂等性设计
    • 分布式锁
    • 状态机校验
    • 实时监控
  3. 事后核对
    • 定时对账
    • 差异自动处理
    • 人工审核机制
    • 完整操作日志
  4. 监控告警
    • 大额支付实时告警
    • 异常频次监控
    • 对账差异告警
    • 系统健康度监控

通过这些措施,可以大幅降低错付和多付的风险,确保支付系统的安全可靠。


作 者:南烛
链 接:https://www.itnotes.top/archives/951
来 源:IT笔记
文章版权归作者所有,转载请注明出处!


上一篇
下一篇