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,280 @@
# IMPL-001: 响应式编程培训方案
> 文档编号: GYM-IMPL-001
> 版本: v1.0
> 日期: 2026-04-05
> 作者: 张翔
> 状态: 正式发布
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
|------|------|------|---------|
| v1.0 | 2026-04-05 | 张翔 | 创建响应式编程培训方案 |
---
## 一、需求分析
### 1.1 问题背景
团队对WebFlux和R2DBC不熟悉,影响开发效率和代码质量。
### 1.2 培训目标
- 掌握响应式编程核心概念(Mono、Flux、背压)
- 熟练使用Spring WebFlux开发REST API
- 掌握R2DBC进行响应式数据库操作
- 能够进行响应式应用的性能调优
### 1.3 成功标准
- 开发效率提升30%
- 代码质量提升50%
- Bug率降低40%
- 团队成员理论考试≥80分
- 代码审查通过率≥90%
---
## 二、培训方案设计
### 2.1 培训大纲
#### 第1周:响应式编程基础
**培训内容**
- Reactor核心概念
- Mono和Flux操作符
- 背压机制
- 线程模型
**培训方式**:线上课程
**考核方式**:理论考试
**学习目标**
- 理解响应式编程基本原理
- 掌握Mono和Flux的基本操作
- 理解背压机制的作用
---
#### 第2-3周:WebFlux实战
**培训内容**
- WebFlux应用架构
- 路由和处理器
- 请求验证和异常处理
- 响应式WebClient
**培训方式**:编码练习
**考核方式**:代码审查
**学习目标**
- 能够使用WebFlux开发REST API
- 掌握路由和处理器的设计
- 能够处理异常和验证请求
---
#### 第4周:R2DBC实战
**培训内容**
- R2DBC连接池配置
- 响应式Repository
- 事务管理
- 性能优化
**培训方式**:编码练习
**考核方式**:代码审查
**学习目标**
- 能够使用R2DBC进行数据库操作
- 掌握响应式事务管理
- 能够优化数据库性能
---
#### 第5周:性能调优
**培训内容**
- 响应式流监控
- 性能测试工具
- 调优策略
- 常见问题排查
**培训方式**:性能测试
**考核方式**:性能报告
**学习目标**
- 能够监控响应式流
- 掌握性能测试工具
- 能够进行性能调优
---
#### 第6周:综合项目
**培训内容**
- 完整项目实战
- 代码审查
- 项目答辩
**培训方式**:项目实战
**考核方式**:项目评审
**学习目标**
- 能够独立完成响应式项目
- 代码质量达到生产标准
---
### 2.2 培训资源
**官方文档**
- Spring WebFlux官方文档
- Project Reactor官方文档
- R2DBC官方文档
**视频课程**
- Reactor官方教程
- Spring WebFlux实战课程
**实战项目**
- 健身房管理系统的会员模块
---
### 2.3 培训方式
**线上自学 + 线下辅导**
- 每周自学时间:10小时
- 每周集中答疑:2次(每次1小时)
- 编码练习:每周20小时
- 项目实战:最后2周全职
---
## 三、考核方案
### 3.1 理论考试
**考试内容**
- 响应式编程基础概念
- WebFlux核心原理
- R2DBC使用方法
- 性能调优策略
**考试形式**:在线考试
**及格标准**:≥80分
---
### 3.2 代码审查
**审查内容**
- 代码规范性
- 响应式编程最佳实践
- 异常处理
- 性能优化
**审查标准**
- 代码规范符合团队标准
- 无明显性能问题
- 异常处理完善
- 测试覆盖率≥80%
**通过标准**:审查通过率≥90%
---
### 3.3 项目评审
**评审内容**
- 项目功能完整性
- 代码质量
- 性能指标
- 文档完整性
**评审标准**
- 功能完整且符合需求
- 代码质量达到生产标准
- 性能指标达标
- 文档完整清晰
---
## 四、实施计划
### 4.1 培训时间表
| 周次 | 培训内容 | 培训方式 | 考核方式 | 负责人 |
|------|---------|---------|---------|--------|
| 第1周 | 响应式编程基础 | 线上课程 | 理论考试 | 培训讲师 |
| 第2-3周 | WebFlux实战 | 编码练习 | 代码审查 | 培训讲师 |
| 第4周 | R2DBC实战 | 编码练习 | 代码审查 | 培训讲师 |
| 第5周 | 性能调优 | 性能测试 | 性能报告 | 培训讲师 |
| 第6周 | 综合项目 | 项目实战 | 项目评审 | 培训讲师 |
---
### 4.2 资源需求
**人力资源**
- 培训讲师:1人
- 参训人员:全体后端开发
**时间资源**
- 培训时间:4-6周
- 每周培训时间:30小时
**预算资源**
- 培训预算:¥10,000
- 包含:课程费用、讲师费用、材料费用
---
## 五、验收标准
### 5.1 培训验收
- [ ] 团队成员通过理论考试(≥80分)
- [ ] 团队成员完成实战项目
- [ ] 代码审查通过率≥90%
### 5.2 效果验收
- [ ] 开发效率提升30%
- [ ] 代码质量提升50%
- [ ] Bug率降低40%
---
## 六、风险与应对
### 6.1 风险识别
**风险1:学员基础参差不齐**
- 应对:分层次培训,基础薄弱学员额外辅导
**风险2:培训时间冲突**
- 应对:灵活安排培训时间,提供录播课程
**风险3:实战项目难度过大**
- 应对:提供项目模板和指导文档
---
## 七、相关文档
- [改进路线图](../05-PLANS/改进路线图.md)
- [EVAL-001-架构合理性评估报告](../03-EVALUATION/EVAL-001-架构合理性评估报告.md)
- [ADR-002-响应式编程选型](../02-ARCHITECTURE/架构决策记录/ADR-002-响应式编程选型.md)
@@ -0,0 +1,590 @@
# IMPL-002: 敏感数据加密存储方案
> 文档编号: GYM-IMPL-002
> 版本: v1.0
> 日期: 2026-04-05
> 作者: 张翔
> 状态: 正式发布
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
|------|------|------|---------|
| v1.0 | 2026-04-05 | 张翔 | 创建敏感数据加密存储方案 |
---
## 一、需求分析
### 1.1 问题背景
会员隐私数据、支付信息等敏感数据未加密存储,存在数据泄露风险。
### 1.2 敏感数据识别
| 数据类型 | 字段名 | 敏感级别 | 加密要求 |
|---------|--------|---------|---------|
| 会员手机号 | phone | 高 | AES-256-GCM |
| 会员身份证号 | id_card | 高 | AES-256-GCM |
| 会员银行卡号 | bank_card | 高 | AES-256-GCM |
| 支付密码 | payment_password | 高 | BCrypt |
| 会员地址 | address | 中 | AES-256-GCM |
### 1.3 加密要求
- 加密算法:AES-256-GCM
- 密钥长度:256位
- 加密模式:GCM(提供认证加密)
- 密钥管理:环境变量 + 密钥管理服务
### 1.4 成功标准
- 数据安全性提升100%
- 合规性提升
- 加密/解密性能≤10ms
- 数据迁移成功率100%
---
## 二、技术方案设计
### 2.1 加密算法选择
**AES-256-GCM优势**
- ✅ 安全性高:256位密钥长度
- ✅ 认证加密:GCM模式提供数据完整性验证
- ✅ 性能优秀:硬件加速支持
- ✅ 广泛应用:业界标准算法
**加密流程**
```
明文 → 生成IV → AES-GCM加密 → Base64编码 → 密文
密文 → Base64解码 → AES-GCM解密 → 明文
```
---
### 2.2 密钥管理方案
#### 开发环境
**方案**:环境变量存储密钥
**配置**
```bash
export ENCRYPTION_KEY=your-256-bit-key-here
export ENCRYPTION_IV=your-initialization-vector
```
**优势**
- 简单易用
- 不易泄露到代码库
---
#### 测试环境
**方案**:环境变量 + 配置文件加密
**配置**
```yaml
encryption:
key: ${ENCRYPTION_KEY}
iv: ${ENCRYPTION_IV}
enabled: true
```
**优势**
- 配置灵活
- 支持多环境
---
#### 生产环境
**方案**:密钥管理服务(如阿里云KMS
**架构**
```
应用启动 → 从KMS获取密钥 → 缓存到内存 → 使用密钥加密/解密
```
**优势**
- 密钥集中管理
- 密钥自动轮换
- 审计日志完整
---
### 2.3 数据迁移方案
#### 迁移步骤
**步骤1:创建加密字段**
```sql
ALTER TABLE member ADD COLUMN phone_encrypted VARCHAR(255);
ALTER TABLE member ADD COLUMN id_card_encrypted VARCHAR(255);
ALTER TABLE member ADD COLUMN bank_card_encrypted VARCHAR(255);
```
**步骤2:批量加密现有数据**
```java
@Transactional
public void migrateData() {
List<Member> members = memberRepository.findAll();
for (Member member : members) {
if (member.getPhone() != null) {
String encrypted = encryptionUtil.encrypt(member.getPhone());
member.setPhoneEncrypted(encrypted);
}
// 其他字段类似处理
}
memberRepository.saveAll(members);
}
```
**步骤3:验证加密数据正确性**
```java
public void verifyEncryption() {
List<Member> members = memberRepository.findAll();
for (Member member : members) {
if (member.getPhoneEncrypted() != null) {
String decrypted = encryptionUtil.decrypt(member.getPhoneEncrypted());
// 验证解密后的数据与原数据一致
assert decrypted.equals(member.getPhone());
}
}
}
```
**步骤4:删除明文字段**
```sql
ALTER TABLE member DROP COLUMN phone;
ALTER TABLE member DROP COLUMN id_card;
ALTER TABLE member DROP COLUMN bank_card;
```
**步骤5:重命名字段**
```sql
ALTER TABLE member CHANGE COLUMN phone_encrypted phone VARCHAR(255);
ALTER TABLE member CHANGE COLUMN id_card_encrypted id_card VARCHAR(255);
ALTER TABLE member CHANGE COLUMN bank_card_encrypted bank_card VARCHAR(255);
```
---
## 三、代码结构设计
### 3.1 包结构
```
com.gym.manage.security/
├── encryption/
│ ├── EncryptionUtil.java # 加密工具类
│ ├── KeyManager.java # 密钥管理器
│ └── EncryptionConfig.java # 加密配置
├── converter/
│ ├── PhoneEncryptConverter.java # 手机号加密转换器
│ ├── IdCardEncryptConverter.java # 身份证号加密转换器
│ └── BankCardEncryptConverter.java # 银行卡号加密转换器
└── aspect/
└── EncryptionAspect.java # 加密切面
```
---
### 3.2 核心类设计
#### EncryptionUtil
```java
@Component
public class EncryptionUtil {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
@Autowired
private KeyManager keyManager;
/**
* 加密
* @param plaintext 明文
* @return 密文(Base64编码)
*/
public String encrypt(String plaintext) {
try {
SecretKey key = keyManager.getKey();
byte[] iv = generateIV();
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// IV + Ciphertext
ByteBuffer buffer = ByteBuffer.allocate(iv.length + ciphertext.length);
buffer.put(iv);
buffer.put(ciphertext);
return Base64.getEncoder().encodeToString(buffer.array());
} catch (Exception e) {
throw new EncryptionException("加密失败", e);
}
}
/**
* 解密
* @param ciphertext 密文(Base64编码)
* @return 明文
*/
public String decrypt(String ciphertext) {
try {
SecretKey key = keyManager.getKey();
byte[] decoded = Base64.getDecoder().decode(ciphertext);
ByteBuffer buffer = ByteBuffer.wrap(decoded);
byte[] iv = new byte[GCM_IV_LENGTH];
buffer.get(iv);
byte[] encrypted = new byte[buffer.remaining()];
buffer.get(encrypted);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] plaintext = cipher.doFinal(encrypted);
return new String(plaintext, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new EncryptionException("解密失败", e);
}
}
private byte[] generateIV() {
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
return iv;
}
}
```
#### KeyManager
```java
@Component
public class KeyManager {
@Value("${encryption.key}")
private String keyString;
private SecretKey cachedKey;
/**
* 获取加密密钥
* @return SecretKey
*/
public SecretKey getKey() {
if (cachedKey == null) {
cachedKey = generateKey(keyString);
}
return cachedKey;
}
private SecretKey generateKey(String keyString) {
byte[] keyBytes = keyString.getBytes(StandardCharsets.UTF_8);
return new SecretKeySpec(keyBytes, "AES");
}
}
```
#### PhoneEncryptConverter
```java
@Component
@Converter(autoApply = true)
public class PhoneEncryptConverter implements AttributeConverter<String, String> {
@Autowired
private EncryptionUtil encryptionUtil;
@Override
public String convertToDatabaseColumn(String attribute) {
if (attribute == null) {
return null;
}
return encryptionUtil.encrypt(attribute);
}
@Override
public String convertToEntityAttribute(String dbData) {
if (dbData == null) {
return null;
}
return encryptionUtil.decrypt(dbData);
}
}
```
---
## 四、测试方案
### 4.1 单元测试
#### 加密/解密功能测试
```java
@SpringBootTest
public class EncryptionUtilTest {
@Autowired
private EncryptionUtil encryptionUtil;
@Test
public void testEncryptAndDecrypt() {
String plaintext = "13800138000";
// 加密
String ciphertext = encryptionUtil.encrypt(plaintext);
assertNotNull(ciphertext);
assertNotEquals(plaintext, ciphertext);
// 解密
String decrypted = encryptionUtil.decrypt(ciphertext);
assertEquals(plaintext, decrypted);
}
@Test
public void testEncryptSamePlaintextDifferentCiphertext() {
String plaintext = "13800138000";
String ciphertext1 = encryptionUtil.encrypt(plaintext);
String ciphertext2 = encryptionUtil.encrypt(plaintext);
// 相同明文,不同密文(因为IV不同)
assertNotEquals(ciphertext1, ciphertext2);
// 但都能正确解密
assertEquals(plaintext, encryptionUtil.decrypt(ciphertext1));
assertEquals(plaintext, encryptionUtil.decrypt(ciphertext2));
}
@Test
public void testEncryptNull() {
String ciphertext = encryptionUtil.encrypt(null);
assertNull(ciphertext);
}
@Test
public void testDecryptInvalidCiphertext() {
assertThrows(EncryptionException.class, () -> {
encryptionUtil.decrypt("invalid-ciphertext");
});
}
}
```
#### 密钥管理测试
```java
@SpringBootTest
public class KeyManagerTest {
@Autowired
private KeyManager keyManager;
@Test
public void testGetKey() {
SecretKey key = keyManager.getKey();
assertNotNull(key);
assertEquals("AES", key.getAlgorithm());
assertEquals(32, key.getEncoded().length); // 256位 = 32字节
}
@Test
public void testKeyCache() {
SecretKey key1 = keyManager.getKey();
SecretKey key2 = keyManager.getKey();
// 密钥应该被缓存
assertSame(key1, key2);
}
}
```
---
### 4.2 集成测试
#### 数据库存储加密测试
```java
@SpringBootTest
@Transactional
public class MemberEncryptionTest {
@Autowired
private MemberRepository memberRepository;
@Autowired
private EncryptionUtil encryptionUtil;
@Test
public void testSaveEncryptedPhone() {
Member member = new Member();
member.setPhone("13800138000");
Member saved = memberRepository.save(member);
// 从数据库直接查询(绕过JPA转换器)
String encryptedPhone = jdbcTemplate.queryForObject(
"SELECT phone FROM member WHERE id = ?",
String.class,
saved.getId()
);
// 验证数据库中存储的是加密后的数据
assertNotNull(encryptedPhone);
assertNotEquals("13800138000", encryptedPhone);
// 验证可以正确解密
String decrypted = encryptionUtil.decrypt(encryptedPhone);
assertEquals("13800138000", decrypted);
}
@Test
public void testReadDecryptedPhone() {
// 保存会员
Member member = new Member();
member.setPhone("13800138000");
Member saved = memberRepository.save(member);
// 读取会员
Member found = memberRepository.findById(saved.getId()).orElse(null);
// 验证读取的是解密后的数据
assertNotNull(found);
assertEquals("13800138000", found.getPhone());
}
}
```
---
### 4.3 性能测试
```java
@SpringBootTest
public class EncryptionPerformanceTest {
@Autowired
private EncryptionUtil encryptionUtil;
@Test
public void testEncryptPerformance() {
String plaintext = "13800138000";
int iterations = 1000;
long startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
encryptionUtil.encrypt(plaintext);
}
long endTime = System.currentTimeMillis();
long avgTime = (endTime - startTime) / iterations;
System.out.println("平均加密时间: " + avgTime + "ms");
assertTrue(avgTime < 10, "加密时间应小于10ms");
}
@Test
public void testDecryptPerformance() {
String plaintext = "13800138000";
String ciphertext = encryptionUtil.encrypt(plaintext);
int iterations = 1000;
long startTime = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
encryptionUtil.decrypt(ciphertext);
}
long endTime = System.currentTimeMillis();
long avgTime = (endTime - startTime) / iterations;
System.out.println("平均解密时间: " + avgTime + "ms");
assertTrue(avgTime < 10, "解密时间应小于10ms");
}
}
```
---
## 五、实施步骤
| 步骤 | 任务 | 负责人 | 完成时间 | 验收标准 |
|------|------|--------|---------|---------|
| 1 | 识别敏感数据字段 | 后端开发 | 1天 | 敏感数据字段清单 |
| 2 | 设计加密方案 | 架构师 | 1天 | 加密方案文档 |
| 3 | 实现加密工具类 | 后端开发 | 2天 | 单元测试通过 |
| 4 | 实现JPA转换器 | 后端开发 | 1天 | 单元测试通过 |
| 5 | 数据迁移脚本 | 后端开发 | 1天 | 迁移脚本完成 |
| 6 | 测试验证 | 测试工程师 | 1天 | 所有测试通过 |
---
## 六、验收标准
### 6.1 功能验收
- [ ] 敏感数据加密存储
- [ ] 数据库中无明文敏感数据
- [ ] 加密数据可正确解密
- [ ] 通过安全审计
### 6.2 性能验收
- [ ] 加密/解密性能≤10ms
- [ ] 数据迁移成功率100%
### 6.3 安全验收
- [ ] 密钥管理安全
- [ ] 无密钥泄露风险
- [ ] 符合数据安全合规要求
---
## 七、风险与应对
### 7.1 风险识别
**风险1:密钥泄露**
- 应对:使用密钥管理服务,定期轮换密钥
**风险2:性能影响**
- 应对:使用硬件加速,优化加密算法
**风险3:数据迁移失败**
- 应对:备份原数据,分批迁移,验证后删除
**风险4:解密失败**
- 应对:保存原始密文,提供手动解密工具
---
## 八、相关文档
- [改进路线图](../05-PLANS/改进路线图.md)
- [EVAL-003-安全性与容错能力评估报告](../03-EVALUATION/EVAL-003-安全性与容错能力评估报告.md)
- [SEC-安全设计](../02-ARCHITECTURE/技术架构/SEC-安全设计.md)
@@ -0,0 +1,654 @@
# IMPL-003: 预约高峰期性能优化方案
> 文档编号: GYM-IMPL-003
> 版本: v1.0
> 日期: 2026-04-05
> 作者: 张翔
> 状态: 正式发布
---
## 文档修订历史
| 版本 | 日期 | 作者 | 修订内容 |
|------|------|------|---------|
| v1.0 | 2026-04-05 | 张翔 | 创建预约高峰期性能优化方案 |
---
## 一、需求分析
### 1.1 问题背景
预约高峰期QPS仅500-1000,距离目标2000+差距较大,影响用户体验和业务转化。
### 1.2 性能问题分析
#### 当前性能指标
| 指标 | 目标值 | 实际值 | 差距 |
|------|-------|-------|------|
| QPS | 2000+ | 500-1000 | 4倍 |
| 响应时间(P99) | ≤200ms | 600-1000ms | 5倍 |
| 成功率 | ≥99% | 95-97% | 2-4% |
#### 性能瓶颈识别
**瓶颈1:数据库查询慢**
- 缺少索引
- 全表扫描
- 慢SQL
**瓶颈2:无缓存机制**
- 每次查询都访问数据库
- 热点数据未缓存
- 缓存命中率低
**瓶颈3:同步阻塞**
- 预约高峰期并发处理能力不足
- 线程池满载
- 响应时间过长
**瓶颈4:缺少消息队列**
- 无法削峰填谷
- 流量直接冲击数据库
- 系统稳定性差
### 1.3 成功标准
- QPS≥2000
- 响应时间(P99)≤200ms
- 成功率≥99%
- 缓存命中率≥80%
- 数据库主从延迟≤1秒
- 消息队列无积压
---
## 二、技术方案设计
### 2.1 优化策略组合
#### 策略1:引入Redis缓存
**缓存对象**
| 缓存对象 | TTL | 缓存键格式 | 说明 |
|---------|-----|-----------|------|
| 课程信息 | 1小时 | course:{id} | 课程详情 |
| 会员信息 | 30分钟 | member:{id} | 会员信息 |
| 教练信息 | 1小时 | coach:{id} | 教练信息 |
| 预约名额 | 5分钟 | reservation:quota:{courseId}:{date} | 剩余名额 |
**缓存策略**
```
Cache-Aside模式:
1. 读取时先查缓存
2. 缓存未命中则查数据库
3. 查询结果写入缓存
4. 写入时更新缓存
```
**缓存架构**
```
应用层 → Redis缓存 → 数据库
本地缓存(可选)
```
---
#### 策略2:数据库读写分离
**架构设计**
```
应用层
ShardingSphere-JDBC(路由层)
↓ ↓
主库(写) 从库(读)
```
**实现方案**
**主库**:处理写入操作
- INSERT
- UPDATE
- DELETE
**从库**:处理读取操作
- SELECT
**主从同步**:半同步模式
- 主库写入后等待至少一个从库确认
- 保证数据一致性
---
#### 策略3:引入消息队列削峰
**消息队列选型**RabbitMQ
**削峰方案**
```
预约请求流程:
1. 用户发起预约请求
2. 请求写入消息队列
3. 立即返回"预约中"状态
4. 后台消费者异步处理
5. 处理完成后通知用户
```
**消息队列架构**
```
生产者 → Exchange → Queue → 消费者
死信队列(失败重试)
```
**流量控制**
- 消费速率:2000 TPS
- 队列容量:10000条消息
- 超出容量:拒绝请求
---
### 2.2 技术选型
| 组件 | 技术选型 | 版本 | 说明 |
|------|---------|------|------|
| 缓存 | Redis | 6.2+ | 高性能缓存 |
| 数据库中间件 | ShardingSphere-JDBC | 5.3+ | 读写分离路由 |
| 消息队列 | RabbitMQ | 3.9+ | 削峰填谷 |
---
## 三、代码结构设计
### 3.1 缓存层设计
#### 包结构
```
com.gym.manage.cache/
├── config/
│ ├── RedisConfig.java # Redis配置
│ └── CacheConfig.java # 缓存配置
├── service/
│ ├── CacheService.java # 缓存服务接口
│ └── CacheServiceImpl.java # 缓存服务实现
└── aspect/
└── CacheAspect.java # 缓存切面
```
#### 核心类设计
**RedisConfig**
```java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory
) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson序列化
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
return template;
}
}
```
**CacheService**
```java
@Service
public class CacheServiceImpl implements CacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public <T> T get(String key, Class<T> type) {
Object value = redisTemplate.opsForValue().get(key);
return value != null ? (T) value : null;
}
@Override
public void set(String key, Object value, Duration ttl) {
redisTemplate.opsForValue().set(key, value, ttl);
}
@Override
public void delete(String key) {
redisTemplate.delete(key);
}
@Override
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
}
```
**CacheAspect**
```java
@Aspect
@Component
public class CacheAspect {
@Autowired
private CacheService cacheService;
@Around("@annotation(cacheable)")
public Object around(ProceedingJoinPoint pjp, Cacheable cacheable)
throws Throwable {
String key = generateKey(cacheable.key(), pjp.getArgs());
// 先查缓存
Object cached = cacheService.get(key, cacheable.type());
if (cached != null) {
return cached;
}
// 缓存未命中,执行方法
Object result = pjp.proceed();
// 写入缓存
if (result != null) {
cacheService.set(key, result, Duration.ofMinutes(cacheable.ttl()));
}
return result;
}
private String generateKey(String pattern, Object[] args) {
// 生成缓存键
return String.format(pattern, args);
}
}
```
---
### 3.2 数据库层设计
#### 包结构
```
com.gym.manage.datasource/
├── config/
│ ├── MasterSlaveConfig.java # 主从配置
│ └── ShardingConfig.java # 分片配置
├── routing/
│ ├── DynamicDataSource.java # 动态数据源
│ └── DataSourceRouter.java # 数据源路由
└── annotation/
└── ReadOnly.java # 只读注解
```
#### 核心类设计
**MasterSlaveConfig**
```java
@Configuration
public class MasterSlaveConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DynamicDataSource dynamicDataSource(
@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave
) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", master);
targetDataSources.put("slave", slave);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(master);
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
}
```
**DynamicDataSource**
```java
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
@Override
protected Object determineCurrentLookupKey() {
return CONTEXT_HOLDER.get();
}
public static void setMaster() {
CONTEXT_HOLDER.set("master");
}
public static void setSlave() {
CONTEXT_HOLDER.set("slave");
}
public static void clear() {
CONTEXT_HOLDER.remove();
}
}
```
**DataSourceRouter**
```java
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceRouter {
@Around("@annotation(readOnly)")
public Object route(ProceedingJoinPoint pjp, ReadOnly readOnly)
throws Throwable {
try {
DynamicDataSource.setSlave();
return pjp.proceed();
} finally {
DynamicDataSource.clear();
}
}
}
```
**ReadOnly注解**
```java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReadOnly {
}
```
---
### 3.3 消息队列层设计
#### 包结构
```
com.gym.manage.mq/
├── config/
│ └── RabbitMQConfig.java # RabbitMQ配置
├── producer/
│ └── ReservationProducer.java # 预约生产者
├── consumer/
│ └── ReservationConsumer.java # 预约消费者
└── message/
└── ReservationMessage.java # 预约消息
```
#### 核心类设计
**RabbitMQConfig**
```java
@Configuration
public class RabbitMQConfig {
public static final String EXCHANGE = "reservation.exchange";
public static final String QUEUE = "reservation.queue";
public static final String ROUTING_KEY = "reservation.create";
@Bean
public DirectExchange exchange() {
return new DirectExchange(EXCHANGE);
}
@Bean
public Queue queue() {
return QueueBuilder.durable(QUEUE)
.withArgument("x-message-ttl", 60000) // 消息TTL: 1分钟
.withArgument("x-dead-letter-exchange", "reservation.dlx")
.build();
}
@Bean
public Binding binding() {
return BindingBuilder.bind(queue())
.to(exchange())
.with(ROUTING_KEY);
}
}
```
**ReservationProducer**
```java
@Service
public class ReservationProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendReservation(ReservationMessage message) {
rabbitTemplate.convertAndSend(
RabbitMQConfig.EXCHANGE,
RabbitMQConfig.ROUTING_KEY,
message
);
}
}
```
**ReservationConsumer**
```java
@Service
public class ReservationConsumer {
@Autowired
private ReservationService reservationService;
@RabbitListener(queues = RabbitMQConfig.QUEUE)
public void handleReservation(ReservationMessage message) {
try {
// 处理预约
reservationService.processReservation(message);
} catch (Exception e) {
// 异常处理,消息进入死信队列
throw new AmqpRejectAndDontRequeueException(e);
}
}
}
```
---
## 四、测试方案
### 4.1 性能测试
#### 测试工具
- JMeter:压力测试
- Gatling:性能测试
- Prometheus + Grafana:监控
#### 测试场景
**场景1:预约高峰期模拟**
```scala
scenario("预约高峰期")
.exec(http("预约课程")
.post("/api/reservations")
.body(StringBody("""{"courseId":1,"memberId":1}"""))
.check(status.is(200))
)
.inject(
rampUsersPerSec(100) to 2000 during (300 seconds)
)
.protocols(httpProtocol)
```
**场景2:缓存命中率测试**
```java
@Test
public void testCacheHitRate() {
int totalRequests = 1000;
int cacheHits = 0;
for (int i = 0; i < totalRequests; i++) {
Course course = courseService.getCourseById(1L);
if (cacheService.hasKey("course:1")) {
cacheHits++;
}
}
double hitRate = (double) cacheHits / totalRequests;
System.out.println("缓存命中率: " + (hitRate * 100) + "%");
assertTrue(hitRate >= 0.8, "缓存命中率应≥80%");
}
```
---
### 4.2 压力测试
#### 测试步骤
1. 逐步增加并发数(500 → 1000 → 2000 → 3000
2. 监控系统资源(CPU、内存、网络)
3. 记录性能指标(QPS、响应时间、成功率)
4. 识别系统极限
#### 性能指标
| 并发数 | QPS | 响应时间(P99) | 成功率 | CPU利用率 | 内存利用率 |
|--------|-----|--------------|--------|----------|-----------|
| 500 | 500 | 100ms | 99% | 30% | 50% |
| 1000 | 1000 | 150ms | 99% | 50% | 60% |
| 2000 | 2000 | 200ms | 99% | 70% | 70% |
| 3000 | 2500 | 300ms | 95% | 90% | 80% |
---
### 4.3 稳定性测试
#### 测试步骤
1. 持续运行2小时
2. 监控内存泄漏
3. 监控GC频率
4. 记录系统稳定性指标
#### 稳定性指标
- 内存占用稳定
- GC频率正常
- 无内存泄漏
- 无死锁
---
## 五、实施步骤
| 步骤 | 任务 | 负责人 | 完成时间 | 验收标准 |
|------|------|--------|---------|---------|
| 1 | Redis环境搭建 | 运维工程师 | 1天 | Redis服务正常运行 |
| 2 | 实现缓存服务 | 后端开发 | 2天 | 单元测试通过 |
| 3 | 数据库主从配置 | 运维工程师 | 1天 | 主从同步正常 |
| 4 | 实现读写分离 | 后端开发 | 3天 | 集成测试通过 |
| 5 | RabbitMQ环境搭建 | 运维工程师 | 1天 | RabbitMQ服务正常运行 |
| 6 | 实现消息队列 | 后端开发 | 2天 | 单元测试通过 |
| 7 | 性能测试 | 测试工程师 | 2天 | 性能指标达标 |
| 8 | 灰度发布 | 运维工程师 | 1天 | 灰度发布成功 |
---
## 六、验收标准
### 6.1 性能验收
- [ ] QPS≥2000
- [ ] 响应时间(P99)≤200ms
- [ ] 成功率≥99%
### 6.2 缓存验收
- [ ] 缓存命中率≥80%
- [ ] 缓存穿透防护有效
- [ ] 缓存雪崩防护有效
### 6.3 数据库验收
- [ ] 数据库主从延迟≤1秒
- [ ] 读写分离正常
- [ ] 数据一致性保证
### 6.4 消息队列验收
- [ ] 消息队列无积压
- [ ] 消息不丢失
- [ ] 消费速率达标
---
## 七、风险与应对
### 7.1 风险识别
**风险1:缓存穿透**
- 应对:布隆过滤器 + 空值缓存
**风险2:缓存雪崩**
- 应对:随机过期时间 + 多级缓存
**风险3:主从延迟**
- 应对:关键业务读主库 + 半同步复制
**风险4:消息队列积压**
- 应对:监控告警 + 动态扩容消费者
---
## 八、相关文档
- [改进路线图](../05-PLANS/改进路线图.md)
- [EVAL-002-性能与可扩展性评估报告](../03-EVALUATION/EVAL-002-性能与可扩展性评估报告.md)
- [ADR-002-响应式编程选型](../02-ARCHITECTURE/架构决策记录/ADR-002-响应式编程选型.md)
@@ -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)