# IMPL-004: 支付接口幂等性校验方案 > 文档编号: GYM-IMPL-004 > 版本: v1.0 > 日期: 2026-04-05 > 作者: 张翔 > 状态: 正式发布 --- ## 文档修订历史 | 版本 | 日期 | 作者 | 修订内容 | |------|------|------|---------| | v1.0 | 2026-04-05 | 张翔 | 创建支付接口幂等性校验方案 | --- ## 一、需求分析 ### 1.1 问题背景 支付接口缺少幂等性校验,可能导致: - 用户重复点击支付按钮,产生多笔订单 - 网络超时重试,导致重复扣款 - 支付回调重复通知,导致订单状态异常 ### 1.2 影响范围 **用户体验**: - 重复扣款导致用户投诉 - 订单状态不一致 **财务风险**: - 资金对账困难 - 退款流程复杂 **业务风险**: - 订单状态不一致 - 数据完整性问题 ### 1.3 成功标准 - 支付接口幂等性覆盖率100% - 通过重复支付测试 - 通过并发支付测试 - 幂等检查性能≤10ms --- ## 二、技术方案设计 ### 2.1 幂等性实现方案 #### 方案架构 ``` 支付请求流程: 1. 前端生成唯一订单号 2. 后端检查订单号是否已存在 3. 不存在则创建支付流水 4. 调用第三方支付 5. 支付回调处理(幂等) 6. 更新订单状态(幂等) ``` #### 核心机制 **机制1:唯一订单号** - 格式:UUID + 时间戳 + 业务标识 - 示例:PAY-20260405-UUID-001 - 作用:全局唯一标识 **机制2:支付流水表** - 记录所有支付请求 - 字段:流水ID、订单ID、支付单号、金额、状态、请求号 - 作用:幂等性保证 **机制3:分布式锁** - 实现:Redis分布式锁 - 锁键:payment:lock:{requestNo} - 作用:防止并发重复支付 **机制4:状态机** - 订单状态流转控制 - 状态:待支付、支付中、支付成功、支付失败、取消支付 - 作用:状态一致性保证 --- ### 2.2 支付状态机 #### 状态定义 ```java public enum PaymentState { PENDING, // 待支付 PAYING, // 支付中 SUCCESS, // 支付成功 FAILED, // 支付失败 CANCELLED // 取消支付 } ``` #### 事件定义 ```java public enum PaymentEvent { PAY, // 发起支付 SUCCESS, // 支付成功 FAIL, // 支付失败 CANCEL // 取消支付 } ``` #### 状态转换规则 ``` 状态转换图: 待支付 ──PAY──> 支付中 ──SUCCESS──> 支付成功 │ │ │ └──FAIL──> 支付失败 │ └──CANCEL──> 取消支付 转换规则: - 待支付 → 支付中:发起支付 - 支付中 → 支付成功:支付成功回调 - 支付中 → 支付失败:支付失败回调 - 待支付 → 取消支付:用户取消 ``` --- ## 三、代码结构设计 ### 3.1 包结构 ``` com.gym.manage.payment/ ├── idempotent/ │ ├── IdempotentService.java # 幂等性服务接口 │ ├── IdempotentServiceImpl.java # 幂等性服务实现 │ └── IdempotentAspect.java # 幂等性切面 ├── statemachine/ │ ├── PaymentStateMachine.java # 支付状态机 │ ├── PaymentState.java # 支付状态 │ └── PaymentEvent.java # 支付事件 ├── entity/ │ ├── PaymentFlow.java # 支付流水实体 │ └── PaymentOrder.java # 支付订单实体 ├── repository/ │ ├── PaymentFlowRepository.java # 支付流水Repository │ └── PaymentOrderRepository.java # 支付订单Repository ├── service/ │ ├── PaymentService.java # 支付服务接口 │ └── PaymentServiceImpl.java # 支付服务实现 └── controller/ └── PaymentController.java # 支付控制器 ``` --- ### 3.2 核心类设计 #### PaymentFlow实体 ```java @Entity @Table(name = "payment_flow", uniqueConstraints = @UniqueConstraint(columnNames = "requestNo")) public class PaymentFlow { @Id @GeneratedValue(strategy = GenerationType.UUID) private String flowId; // 流水ID @Column(unique = true, nullable = false) private String requestNo; // 请求号(幂等键) @Column(nullable = false) private String orderId; // 订单ID private String paymentNo; // 支付单号 @Column(nullable = false, precision = 10, scale = 2) private BigDecimal amount; // 支付金额 @Enumerated(EnumType.STRING) private PaymentState state; // 支付状态 private String channel; // 支付渠道 private String errorMessage; // 错误信息 @CreatedDate private LocalDateTime createTime; // 创建时间 @LastModifiedDate private LocalDateTime updateTime; // 更新时间 } ``` #### IdempotentServiceImpl ```java @Service @Transactional public class IdempotentServiceImpl implements IdempotentService { @Autowired private RedisTemplate redisTemplate; @Autowired private PaymentFlowRepository flowRepository; private static final String LOCK_PREFIX = "payment:lock:"; private static final Duration LOCK_TIMEOUT = Duration.ofMinutes(5); @Override public PaymentFlow checkAndCreate(String requestNo, PaymentRequest request) { // 1. 分布式锁 String lockKey = LOCK_PREFIX + requestNo; Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", LOCK_TIMEOUT); if (!locked) { throw new PaymentException("支付处理中,请勿重复提交"); } try { // 2. 检查流水是否已存在 PaymentFlow existFlow = flowRepository .findByRequestNo(requestNo) .orElse(null); if (existFlow != null) { return existFlow; // 幂等返回 } // 3. 创建新流水 PaymentFlow flow = new PaymentFlow(); flow.setRequestNo(requestNo); flow.setOrderId(request.getOrderId()); flow.setAmount(request.getAmount()); flow.setState(PaymentState.PENDING); flow.setChannel(request.getChannel()); return flowRepository.save(flow); } finally { redisTemplate.delete(lockKey); } } @Override public PaymentFlow check(String requestNo) { return flowRepository.findByRequestNo(requestNo).orElse(null); } } ``` #### PaymentStateMachine ```java @Component public class PaymentStateMachine { @Autowired private PaymentOrderRepository orderRepository; /** * 状态转换(幂等) */ @Transactional public boolean transit(String orderId, PaymentEvent event) { PaymentOrder order = orderRepository.findById(orderId) .orElseThrow(() -> new PaymentException("订单不存在")); PaymentState currentState = order.getState(); // 检查状态转换是否合法 if (!canTransit(currentState, event)) { return false; // 幂等返回 } // 执行状态转换 PaymentState newState = getNextState(currentState, event); order.setState(newState); orderRepository.save(order); return true; } private boolean canTransit(PaymentState current, PaymentEvent event) { // 状态转换规则 switch (current) { case PENDING: return event == PaymentEvent.PAY || event == PaymentEvent.CANCEL; case PAYING: return event == PaymentEvent.SUCCESS || event == PaymentEvent.FAIL; default: return false; // 已终态,幂等返回 } } private PaymentState getNextState(PaymentState current, PaymentEvent event) { switch (current) { case PENDING: if (event == PaymentEvent.PAY) return PaymentState.PAYING; if (event == PaymentEvent.CANCEL) return PaymentState.CANCELLED; break; case PAYING: if (event == PaymentEvent.SUCCESS) return PaymentState.SUCCESS; if (event == PaymentEvent.FAIL) return PaymentState.FAILED; break; default: break; } return current; } } ``` #### IdempotentAspect ```java @Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class IdempotentAspect { @Autowired private IdempotentService idempotentService; @Around("@annotation(idempotent)") public Object handleIdempotent( ProceedingJoinPoint pjp, Idempotent idempotent ) throws Throwable { // 1. 获取幂等键 String requestNo = extractRequestNo(pjp); // 2. 检查幂等性 Object result = idempotentService.check(requestNo); if (result != null) { return result; // 幂等返回 } // 3. 执行业务逻辑 return pjp.proceed(); } private String extractRequestNo(ProceedingJoinPoint pjp) { // 从方法参数中提取requestNo Object[] args = pjp.getArgs(); for (Object arg : args) { if (arg instanceof PaymentRequest) { return ((PaymentRequest) arg).getRequestNo(); } } throw new PaymentException("未找到幂等键"); } } ``` #### PaymentServiceImpl ```java @Service @Transactional public class PaymentServiceImpl implements PaymentService { @Autowired private IdempotentService idempotentService; @Autowired private PaymentStateMachine stateMachine; @Autowired private PaymentFlowRepository flowRepository; @Autowired private ThirdPartyPaymentService thirdPartyService; @Override public PaymentResponse pay(PaymentRequest request) { // 1. 幂等性检查并创建流水 PaymentFlow flow = idempotentService.checkAndCreate( request.getRequestNo(), request ); // 如果流水已存在,直接返回 if (flow.getState() != PaymentState.PENDING) { return buildResponse(flow); } // 2. 状态转换:待支付 → 支付中 stateMachine.transit(flow.getFlowId(), PaymentEvent.PAY); try { // 3. 调用第三方支付 ThirdPartyPaymentResponse response = thirdPartyService.pay( flow.getFlowId(), flow.getAmount() ); // 4. 更新支付单号 flow.setPaymentNo(response.getPaymentNo()); flowRepository.save(flow); // 5. 返回支付结果 return buildResponse(flow); } catch (Exception e) { // 6. 支付失败,状态转换 stateMachine.transit(flow.getFlowId(), PaymentEvent.FAIL); flow.setErrorMessage(e.getMessage()); flowRepository.save(flow); throw new PaymentException("支付失败", e); } } @Override public void handleCallback(PaymentCallback callback) { // 1. 查询支付流水 PaymentFlow flow = flowRepository.findById(callback.getFlowId()) .orElseThrow(() -> new PaymentException("流水不存在")); // 2. 幂等性检查:如果已成功,直接返回 if (flow.getState() == PaymentState.SUCCESS) { return; // 幂等返回 } // 3. 状态转换 PaymentEvent event = callback.isSuccess() ? PaymentEvent.SUCCESS : PaymentEvent.FAIL; stateMachine.transit(flow.getFlowId(), event); // 4. 更新流水 flowRepository.save(flow); } private PaymentResponse buildResponse(PaymentFlow flow) { PaymentResponse response = new PaymentResponse(); response.setFlowId(flow.getFlowId()); response.setPaymentNo(flow.getPaymentNo()); response.setState(flow.getState()); response.setAmount(flow.getAmount()); return response; } } ``` --- ## 四、测试方案 ### 4.1 单元测试 #### 幂等性服务测试 ```java @SpringBootTest @Transactional public class IdempotentServiceTest { @Autowired private IdempotentService idempotentService; @Autowired private PaymentFlowRepository flowRepository; @Test public void testCheckAndCreate() { String requestNo = "REQ-123456"; PaymentRequest request = new PaymentRequest(); request.setRequestNo(requestNo); request.setOrderId("ORDER-001"); request.setAmount(new BigDecimal("100.00")); // 第一次创建 PaymentFlow flow1 = idempotentService.checkAndCreate(requestNo, request); assertNotNull(flow1); assertEquals(PaymentState.PENDING, flow1.getState()); // 第二次创建(相同requestNo) PaymentFlow flow2 = idempotentService.checkAndCreate(requestNo, request); assertEquals(flow1.getFlowId(), flow2.getFlowId()); // 幂等返回 } @Test public void testConcurrentCreate() throws InterruptedException { String requestNo = "REQ-789012"; int threadCount = 100; CountDownLatch latch = new CountDownLatch(threadCount); List flows = Collections.synchronizedList(new ArrayList<>()); for (int i = 0; i < threadCount; i++) { new Thread(() -> { try { PaymentFlow flow = idempotentService.checkAndCreate( requestNo, new PaymentRequest() ); flows.add(flow); } finally { latch.countDown(); } }).start(); } latch.await(); // 验证只创建了一笔流水 assertEquals(1, flows.stream() .map(PaymentFlow::getFlowId) .distinct() .count()); } } ``` #### 状态机测试 ```java @SpringBootTest @Transactional public class PaymentStateMachineTest { @Autowired private PaymentStateMachine stateMachine; @Autowired private PaymentOrderRepository orderRepository; @Test public void testStateTransit() { // 创建订单 PaymentOrder order = new PaymentOrder(); order.setOrderId("ORDER-001"); order.setState(PaymentState.PENDING); orderRepository.save(order); // 状态转换:待支付 → 支付中 boolean result1 = stateMachine.transit("ORDER-001", PaymentEvent.PAY); assertTrue(result1); PaymentOrder order1 = orderRepository.findById("ORDER-001").orElse(null); assertEquals(PaymentState.PAYING, order1.getState()); // 状态转换:支付中 → 支付成功 boolean result2 = stateMachine.transit("ORDER-001", PaymentEvent.SUCCESS); assertTrue(result2); PaymentOrder order2 = orderRepository.findById("ORDER-001").orElse(null); assertEquals(PaymentState.SUCCESS, order2.getState()); // 状态转换:支付成功 → 支付失败(非法转换) boolean result3 = stateMachine.transit("ORDER-001", PaymentEvent.FAIL); assertFalse(result3); // 幂等返回 } } ``` --- ### 4.2 集成测试 #### 完整支付流程测试 ```java @SpringBootTest @Transactional public class PaymentIntegrationTest { @Autowired private PaymentService paymentService; @Autowired private PaymentFlowRepository flowRepository; @Test public void testCompletePaymentFlow() { // 1. 发起支付 PaymentRequest request = new PaymentRequest(); request.setRequestNo("REQ-001"); request.setOrderId("ORDER-001"); request.setAmount(new BigDecimal("100.00")); PaymentResponse response = paymentService.pay(request); assertNotNull(response.getFlowId()); assertEquals(PaymentState.PAYING, response.getState()); // 2. 模拟支付成功回调 PaymentCallback callback = new PaymentCallback(); callback.setFlowId(response.getFlowId()); callback.setSuccess(true); paymentService.handleCallback(callback); // 3. 验证流水状态 PaymentFlow flow = flowRepository.findById(response.getFlowId()).orElse(null); assertEquals(PaymentState.SUCCESS, flow.getState()); } @Test public void testDuplicatePayment() { String requestNo = "REQ-002"; // 第一次支付 PaymentRequest request = new PaymentRequest(); request.setRequestNo(requestNo); request.setOrderId("ORDER-002"); request.setAmount(new BigDecimal("100.00")); PaymentResponse response1 = paymentService.pay(request); // 第二次支付(相同requestNo) PaymentResponse response2 = paymentService.pay(request); // 验证幂等性 assertEquals(response1.getFlowId(), response2.getFlowId()); } } ``` --- ### 4.3 压力测试 #### 并发支付测试 ```java @SpringBootTest public class PaymentConcurrencyTest { @Autowired private PaymentService paymentService; @Autowired private PaymentFlowRepository flowRepository; @Test public void testConcurrentPayment() throws InterruptedException { int threadCount = 100; CountDownLatch latch = new CountDownLatch(threadCount); String requestNo = "REQ-CONCURRENT-001"; for (int i = 0; i < threadCount; i++) { new Thread(() -> { try { PaymentRequest request = new PaymentRequest(); request.setRequestNo(requestNo); request.setOrderId("ORDER-CONCURRENT-001"); request.setAmount(new BigDecimal("100.00")); paymentService.pay(request); } finally { latch.countDown(); } }).start(); } latch.await(); // 验证只创建了一笔支付流水 List flows = flowRepository.findByRequestNo(requestNo); assertEquals(1, flows.size()); } } ``` --- ## 五、实施步骤 | 步骤 | 任务 | 负责人 | 完成时间 | 验收标准 | |------|------|--------|---------|---------| | 1 | 设计幂等性方案 | 架构师 | 1天 | 方案文档完成 | | 2 | 创建支付流水表 | 后端开发 | 1天 | 数据库表创建完成 | | 3 | 实现幂等性服务 | 后端开发 | 2天 | 单元测试通过 | | 4 | 实现状态机 | 后端开发 | 1天 | 单元测试通过 | | 5 | 实现幂等性切面 | 后端开发 | 1天 | 单元测试通过 | | 6 | 单元测试 | 后端开发 | 1天 | 单元测试通过 | | 7 | 集成测试 | 测试工程师 | 1天 | 集成测试通过 | | 8 | 压力测试 | 测试工程师 | 1天 | 性能指标达标 | --- ## 六、验收标准 ### 6.1 功能验收 - [ ] 支付接口幂等性覆盖率100% - [ ] 通过重复支付测试 - [ ] 通过并发支付测试 - [ ] 支付回调幂等处理 ### 6.2 性能验收 - [ ] 幂等检查性能≤10ms - [ ] 分布式锁获取≤5ms ### 6.3 安全验收 - [ ] 无重复扣款风险 - [ ] 订单状态一致性保证 --- ## 七、风险与应对 ### 7.1 风险识别 **风险1:分布式锁失效** - 应对:数据库唯一索引兜底 **风险2:状态机死锁** - 应对:超时自动释放 + 监控告警 **风险3:支付流水表过大** - 应对:定期归档历史流水 **风险4:第三方支付重复回调** - 应对:幂等处理 + 幂等键去重 --- ## 八、相关文档 - [改进路线图](../05-PLANS/改进路线图.md) - [EVAL-003-安全性与容错能力评估报告](../03-EVALUATION/EVAL-003-安全性与容错能力评估报告.md) - [SEC-安全设计](../02-ARCHITECTURE/技术架构/SEC-安全设计.md)