新增贷款定价敏感信息加密实施计划

This commit is contained in:
wkc
2026-03-30 10:47:25 +08:00
parent a7d5661275
commit 2854c0bb38
3 changed files with 502 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
# 贷款定价流程客户敏感信息加密计划实施记录
## 实施时间
- 2026-03-30
## 修改内容
- 新增贷款定价敏感信息加密后端实施计划
- 新增贷款定价敏感信息加密前端实施计划
- 明确后端采用统一加解密服务 + 统一展示脱敏服务的实施路径
- 明确前端仅做查询项收口和脱敏值消费,不承担加解密
- 明确测试命令、数据库清理脚本、实施记录与提交节点
## 文档路径
- `docs/superpowers/plans/2026-03-30-loan-pricing-sensitive-data-encryption-backend-plan.md`
- `docs/superpowers/plans/2026-03-30-loan-pricing-sensitive-data-encryption-frontend-plan.md`
- `doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-plans.md`
## 计划结论
- 计划已按仓库要求拆分为后端执行文档和前端执行文档
- 后端计划覆盖密钥配置、敏感字段加解密、列表/详情脱敏、模型调用前解密和历史数据清理
- 前端计划覆盖查询项切换为客户内码、脱敏值展示消费、构建验证和联调验证
- 两份计划都采用最短路径实现,不引入明密文兼容分支
## 说明
- 已检查计划文档保存路径,执行计划保存至 `docs/superpowers/plans`
- 仓库约束禁止启用 subagent本次计划复审采用本地自检方式处理
- 当前仅完成计划文档,不包含代码实现

View File

@@ -0,0 +1,330 @@
# 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 shouldDecryptCustNameAndIdNumBeforeInvokeModel() { ... }
```
关键断言至少包括:
```java
assertEquals("张*", result.getLoanPricingWorkflow().getCustName());
assertEquals("1101********1234", result.getLoanPricingWorkflow().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));
```
要求:
- 对外返回对象中不保留明文
- 保持既有测算利率与执行利率逻辑不变
- [ ] **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` 为脱敏值
- [ ] **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 "补充贷款定价敏感字段后端实施记录"
```

View File

@@ -0,0 +1,145 @@
# Loan Pricing Sensitive Data Encryption Frontend 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:** 前端不承担任何加解密逻辑,只做查询项收口与脱敏值展示消费。列表页从按 `custName` 查询切换为按 `custIsn` 查询,详情页保持现有结构,继续直接渲染后端返回字段。
**Tech Stack:** Vue 2、Element UI、RuoYi 前端工程、npm
---
### Task 1: 收口流程列表页查询条件为客户内码
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
- [ ] **Step 1: 核对当前查询项与请求参数**
Run: `sed -n '1,140p' ruoyi-ui/src/views/loanPricing/workflow/index.vue`
Expected: 能看到当前页面仍存在“客户名称”查询项和 `queryParams.custName`
- [ ] **Step 2: 将查询项改为客户内码**
把列表页查询区调整为类似结构:
```vue
<el-form-item label="客户内码" prop="custIsn">
<el-input
v-model="queryParams.custIsn"
placeholder="请输入客户内码"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
```
同步把查询参数从:
```js
custName: undefined
```
改为:
```js
custIsn: undefined
```
- [ ] **Step 3: 核对 API 层无需额外字段转换**
检查 `ruoyi-ui/src/api/loanPricing/workflow.js`,确认 `listWorkflow(query)` 继续透传 `params: query` 即可;若代码风格需要,仅补充注释说明,不新增映射逻辑。
- [ ] **Step 4: 重新检查源码确认客户名称查询已移除**
Run: `rg -n 'custName|custIsn|客户名称|客户内码' ruoyi-ui/src/views/loanPricing/workflow/index.vue ruoyi-ui/src/api/loanPricing/workflow.js`
Expected: 列表页查询区不再出现 `queryParams.custName`,而是使用 `custIsn`
- [ ] **Step 5: 提交本任务**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/index.vue ruoyi-ui/src/api/loanPricing/workflow.js
git commit -m "调整流程列表按客户内码查询"
```
### Task 2: 固化列表页与详情页的脱敏展示消费
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/detail.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
- [ ] **Step 1: 核对当前页面直接消费后端字段的位置**
Run: `rg -n 'custName|idNum' ruoyi-ui/src/views/loanPricing/workflow/index.vue ruoyi-ui/src/views/loanPricing/workflow/detail.vue ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
Expected: 能定位列表和详情页中所有 `custName``idNum` 的展示位置。
- [ ] **Step 2: 去掉任何可能的前端二次处理设想,只保留直接展示**
如果页面中需要加说明性注释,保持最小实现,例如:
```vue
<el-descriptions-item label="客户名称">{{ detailData.custName }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ detailData.idNum }}</el-descriptions-item>
```
要求:
- 不新增“查看明文”按钮
- 不新增复制原值功能
- 不在前端自行做脱敏算法
- [ ] **Step 3: 执行前端构建验证**
Run: `npm --prefix ruoyi-ui run build:prod`
Expected: PASS输出包含 `Build complete.`
- [ ] **Step 4: 页面联调确认脱敏展示**
Run: 按项目现有方式启动前端并进入贷款定价流程列表页、详情页。
Expected:
- 列表页客户名称显示为脱敏值
- 个人详情页客户名称、证件号码显示为脱敏值
- 企业详情页客户名称、证件号码显示为脱敏值
- [ ] **Step 5: 如果为验证启动了前端进程,结束对应进程**
Run: `ps -ef | rg 'ruoyi-ui|vue-cli-service|npm'`
Expected: 仅停止本次联调启动的前端进程;对非本次启动进程不做处理。
- [ ] **Step 6: 提交本任务**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/index.vue ruoyi-ui/src/views/loanPricing/workflow/detail.vue ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue
git commit -m "接入流程敏感字段前端脱敏展示"
```
### Task 3: 补前端实施记录
**Files:**
- Create: `doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-frontend.md`
- [ ] **Step 1: 编写前端实施记录**
实施记录至少写明:
```markdown
- 流程列表页查询项已从客户名称切换为客户内码
- 前端不承担 `custName``idNum` 加解密逻辑
- 列表页与详情页均直接展示后端返回的脱敏值
- 已完成前端构建验证与页面联调
```
- [ ] **Step 2: 核对文档路径**
Run: `ls doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-frontend.md`
Expected: 文件位于仓库 `doc/` 目录,不写错到其他文档路径。
- [ ] **Step 3: 提交本任务**
```bash
git add doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-frontend.md
git commit -m "补充贷款定价敏感字段前端实施记录"
```