docs: 创建P0和P1改进项实现方案
- IMPL-001: 响应式编程培训方案 - IMPL-002: 敏感数据加密存储方案 - IMPL-003: 预约高峰期性能优化方案 - IMPL-004: 支付接口幂等性校验方案
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user