docs: 创建P0和P1改进项实现方案

- IMPL-001: 响应式编程培训方案
- IMPL-002: 敏感数据加密存储方案
- IMPL-003: 预约高峰期性能优化方案
- IMPL-004: 支付接口幂等性校验方案
This commit is contained in:
张翔
2026-04-05 16:48:27 +08:00
parent de302ebc9f
commit dec9085205
4 changed files with 2265 additions and 0 deletions
@@ -0,0 +1,741 @@
# 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)