Files
gym-manage/docs/06-IMPLEMENTATION/IMPL-004-支付接口幂等性校验方案.md
T
张翔 dec9085205 docs: 创建P0和P1改进项实现方案
- IMPL-001: 响应式编程培训方案
- IMPL-002: 敏感数据加密存储方案
- IMPL-003: 预约高峰期性能优化方案
- IMPL-004: 支付接口幂等性校验方案
2026-04-05 16:48:27 +08:00

20 KiB
Raw Blame History

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🔒{requestNo}
  • 作用:防止并发重复支付

机制4:状态机

  • 订单状态流转控制
  • 状态:待支付、支付中、支付成功、支付失败、取消支付
  • 作用:状态一致性保证

2.2 支付状态机

状态定义

public enum PaymentState {
    PENDING,      // 待支付
    PAYING,       // 支付中
    SUCCESS,      // 支付成功
    FAILED,       // 支付失败
    CANCELLED     // 取消支付
}

事件定义

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实体

@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

@Service
@Transactional
public class IdempotentServiceImpl implements IdempotentService {
    
    @Autowired
    private RedisTemplate<String, String> 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

@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

@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

@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 单元测试

幂等性服务测试

@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<PaymentFlow> 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());
    }
}

状态机测试

@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 集成测试

完整支付流程测试

@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 压力测试

并发支付测试

@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<PaymentFlow> 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:第三方支付重复回调

  • 应对:幂等处理 + 幂等键去重

八、相关文档