Files
loan-pricing/docs/superpowers/plans/2026-03-30-loan-pricing-sensitive-data-encryption-backend-plan.md

345 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Loan Pricing Sensitive Data Encryption Backend Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:executing-plans to implement this plan in this repository. Do not use subagents. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让贷款定价流程在后端对 `custName``idNum` 实现密文存储,并保证列表、详情、模型输出基本信息、模型调用链路在各自边界内完成脱敏或解密。
**Architecture:** 后端在 `ruoyi-loan-pricing` 模块内新增贷款定价专用敏感字段加解密服务和展示脱敏服务,固定密钥从配置读取。`LoanPricingWorkflowServiceImpl` 在创建、列表、详情和模型输出展示链路显式接入这些服务,`LoanPricingModelService` 在调模型前显式解密,避免把密文错误透传给模型。
**Tech Stack:** Spring Boot、MyBatis Plus、JUnit 5、Mockito、Maven、JDK `javax.crypto`
---
### Task 1: 搭建敏感字段加解密与脱敏基础设施
**Files:**
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoService.java`
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingSensitiveDisplayService.java`
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoServiceTest.java`
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingSensitiveDisplayServiceTest.java`
- Modify: `ruoyi-admin/src/main/resources/application.yml`
- [ ] **Step 1: 写加解密与脱敏失败测试**
新增两个测试文件,至少覆盖以下场景:
```java
@Test
void shouldEncryptAndDecryptCustNameAndIdNum()
{
String cipher = service.encrypt("张三");
assertNotEquals("张三", cipher);
assertEquals("张三", service.decrypt(cipher));
}
@Test
void shouldRejectBlankKeyConfiguration()
{
SensitiveFieldCryptoService service = new SensitiveFieldCryptoService("");
assertThrows(IllegalStateException.class, () -> service.encrypt("张三"));
}
```
```java
@Test
void shouldMaskPersonalNameAndIdNum()
{
assertEquals("张*", displayService.maskCustName("张三"));
assertEquals("1101********1234", displayService.maskIdNum("110101199001011234"));
}
@Test
void shouldMaskCorporateNameAndCreditCode()
{
assertEquals("测试****公司", displayService.maskCustName("测试科技有限公司"));
assertEquals("91*************00X", displayService.maskIdNum("91110000100000000X"));
}
```
- [ ] **Step 2: 运行基础测试确认当前失败**
Run: `mvn -pl ruoyi-loan-pricing -am -Dtest=SensitiveFieldCryptoServiceTest,LoanPricingSensitiveDisplayServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL提示测试类或对应服务不存在。
- [ ] **Step 3: 增加配置项和最小实现**
`application.yml` 增加贷款定价敏感字段配置,例如:
```yaml
loan-pricing:
sensitive:
key: "1234567890abcdef"
```
创建加解密服务与脱敏服务,最小实现至少包含:
```java
@Service
public class SensitiveFieldCryptoService
{
public String encrypt(String plainText) { ... }
public String decrypt(String cipherText) { ... }
}
```
```java
@Service
public class LoanPricingSensitiveDisplayService
{
public String maskCustName(String custName) { ... }
public String maskIdNum(String idNum) { ... }
}
```
- [ ] **Step 4: 重新运行基础测试**
Run: `mvn -pl ruoyi-loan-pricing -am -Dtest=SensitiveFieldCryptoServiceTest,LoanPricingSensitiveDisplayServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS
- [ ] **Step 5: 提交本任务**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoService.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingSensitiveDisplayService.java ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoServiceTest.java ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingSensitiveDisplayServiceTest.java ruoyi-admin/src/main/resources/application.yml
git commit -m "新增贷款定价敏感字段加解密服务"
```
### Task 2: 接入流程创建与列表查询链路
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowListVO.java`
- Modify: `ruoyi-loan-pricing/src/main/resources/mapper/loanpricing/LoanPricingWorkflowMapper.xml`
- Modify: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- [ ] **Step 1: 写服务层失败测试,约束创建时入库加密、列表返回脱敏、查询按客户内码**
`LoanPricingWorkflowServiceImplTest` 增加至少 3 个用例:
```java
@Test
void shouldEncryptCustNameAndIdNumBeforeInsert() { ... }
@Test
void shouldMaskCustNameWhenReturningPagedWorkflowList() { ... }
@Test
void shouldUseCustIsnInsteadOfCustNameAsQueryCondition() { ... }
```
关键断言至少包括:
```java
verify(loanPricingWorkflowMapper).insert(argThat(entity ->
!Objects.equals(entity.getCustName(), "张三")
&& !Objects.equals(entity.getIdNum(), "110101199001011234")));
assertEquals("张*", result.getRecords().get(0).getCustName());
```
- [ ] **Step 2: 运行服务测试确认失败**
Run: `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL当前创建逻辑尚未加密列表链路尚未脱敏查询条件仍包含 `custName`
- [ ] **Step 3: 在创建链路显式加密敏感字段**
`LoanPricingWorkflowServiceImpl#createLoanPricing` 中补最小实现:
```java
loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getCustName()));
loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getIdNum()));
loanPricingWorkflowMapper.insert(loanPricingWorkflow);
```
要求:
- 只加密 `custName``idNum`
- `custIsn` 保持原样
- 配置缺失时直接失败,不增加明文兼容分支
- [ ] **Step 4: 收口列表查询条件并补脱敏返回**
修改点至少包含:
```java
if (StringUtils.hasText(loanPricingWorkflow.getCustIsn()))
{
wrapper.like(LoanPricingWorkflow::getCustIsn, loanPricingWorkflow.getCustIsn());
}
```
```java
pageResult.getRecords().forEach(row ->
row.setCustName(loanPricingSensitiveDisplayService.maskCustName(
sensitiveFieldCryptoService.decrypt(row.getCustName()))));
```
同步从 `buildQueryWrapper``LoanPricingWorkflowMapper.xml` 删除 `custName` 过滤条件。
- [ ] **Step 5: 重新运行服务测试**
Run: `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS
- [ ] **Step 6: 补充实体与 VO 边界说明性调整**
若测试或编译需要,在 `LoanPricingWorkflow``LoanPricingWorkflowListVO` 中补充本次链路使用的字段,并保持对象语义清晰;不要新增明文副本字段。
- [ ] **Step 7: 提交本任务**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowListVO.java ruoyi-loan-pricing/src/main/resources/mapper/loanpricing/LoanPricingWorkflowMapper.xml ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java
git commit -m "接入流程敏感字段加密与列表脱敏"
```
### Task 3: 接入详情返回、模型输出展示与模型调用链路
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowVO.java`
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
- Modify: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- [ ] **Step 1: 写详情、模型输出展示与模型调用失败测试**
新增或补充以下测试场景:
```java
@Test
void shouldMaskCustNameAndIdNumWhenReturningWorkflowDetail() { ... }
```
```java
@Test
void shouldMaskCustNameAndIdNumInRetailModelOutputBasicInfo() { ... }
```
```java
@Test
void shouldMaskCustNameAndIdNumInCorporateModelOutputBasicInfo() { ... }
```
```java
@Test
void shouldDecryptCustNameAndIdNumBeforeInvokeModel() { ... }
```
关键断言至少包括:
```java
assertEquals("张*", result.getLoanPricingWorkflow().getCustName());
assertEquals("1101********1234", result.getLoanPricingWorkflow().getIdNum());
assertEquals("张*", result.getModelRetailOutputFields().getCustName());
assertEquals("1101********1234", result.getModelRetailOutputFields().getIdNum());
verify(modelService).invokeModel(argThat(dto ->
Objects.equals("张三", dto.getCustName())
&& Objects.equals("110101199001011234", dto.getIdNum())));
```
- [ ] **Step 2: 运行详情与模型测试确认失败**
Run: `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL当前详情返回未完整脱敏模型输出“基本信息”仍会返回明文模型调用前也未解密。
- [ ] **Step 3: 在详情返回前显式解密再脱敏**
`selectLoanPricingBySerialNum` 中加入类似处理:
```java
String plainCustName = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getCustName());
String plainIdNum = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getIdNum());
loanPricingWorkflow.setCustName(loanPricingSensitiveDisplayService.maskCustName(plainCustName));
loanPricingWorkflow.setIdNum(loanPricingSensitiveDisplayService.maskIdNum(plainIdNum));
```
要求:
- 对外返回对象中不保留明文
- 保持既有测算利率与执行利率逻辑不变
-`modelRetailOutputFields``modelCorpOutputFields` 非空,同样对其 `custName``idNum` 做脱敏替换
- [ ] **Step 4: 在模型调用前显式解密**
`LoanPricingModelService#invokeModelAsync` 中补最小实现:
```java
loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getCustName()));
loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getIdNum()));
BeanUtils.copyProperties(loanPricingWorkflow, modelInvokeDTO);
```
要求:
- 只解密 `custName``idNum`
- 解密失败直接中断模型调用并记录错误
- 不修改模型输出表处理逻辑
- [ ] **Step 5: 重新运行详情与模型测试**
Run: `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS
- [ ] **Step 6: 运行贷款定价模块回归测试**
Run: `mvn -pl ruoyi-loan-pricing -am -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS若有失败先区分是否为既有问题再决定是否继续扩测。
- [ ] **Step 7: 提交本任务**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowVO.java ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java
git commit -m "接入流程详情脱敏与模型调用解密"
```
### Task 4: 补数据库脚本与后端实施记录
**Files:**
- Create: `sql/clear_loan_pricing_workflow_history.sql`
- Create: `doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-backend.md`
- [ ] **Step 1: 新增历史数据清理脚本**
创建脚本,至少包含:
```sql
DELETE FROM model_retail_output_fields;
DELETE FROM model_corp_output_fields;
DELETE FROM loan_pricing_workflow;
```
要求:
- 删除顺序满足外键或业务依赖关系
- 只清理贷款定价流程相关数据
- [ ] **Step 2: 写后端实施记录**
实施记录至少写明:
```markdown
- 已新增贷款定价敏感字段加解密服务与展示脱敏服务
- 已在流程创建链路对 `custName``idNum` 加密后入库
- 已在详情返回与列表返回链路统一脱敏
- 已在模型调用前显式解密敏感字段
- 已新增历史数据清理脚本
```
- [ ] **Step 3: 手工验证数据库落库与返回链路**
Run: 按项目现有方式启动后端,创建一条个人流程和一条企业流程,再查询列表与详情。
Expected:
- 数据库中的 `cust_name``id_num` 不等于前端提交明文
- 列表和详情返回的 `custName``idNum` 为脱敏值
- 模型输出“基本信息”页签中的 `custName``idNum` 也为脱敏值
- [ ] **Step 4: 如果为验证启动了后端进程,结束对应进程**
Run: `ps -ef | rg 'RuoYiApplication|loan-pricing|java'`
Expected: 仅停止本次验证启动的后端进程;对非本次启动进程不做处理。
- [ ] **Step 5: 提交本任务**
```bash
git add sql/clear_loan_pricing_workflow_history.sql doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-backend.md
git commit -m "补充贷款定价敏感字段后端实施记录"
```