# 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 "补充贷款定价敏感字段后端实施记录" ```