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

742 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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
```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<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());
}
}
```
#### 状态机测试
```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<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:第三方支付重复回调**
- 应对:幂等处理 + 幂等键去重
---
## 八、相关文档
- [改进路线图](../05-PLANS/改进路线图.md)
- [EVAL-003-安全性与容错能力评估报告](../03-EVALUATION/EVAL-003-安全性与容错能力评估报告.md)
- [SEC-安全设计](../02-ARCHITECTURE/技术架构/SEC-安全设计.md)