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

14 KiB
Raw Blame History

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: 让贷款定价流程在后端对 custNameidNum 实现密文存储,并保证列表、详情、模型输出基本信息、模型调用链路在各自边界内完成脱敏或解密。

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: 写加解密与脱敏失败测试

新增两个测试文件,至少覆盖以下场景:

@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("张三"));
}
@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 增加贷款定价敏感字段配置,例如:

loan-pricing:
  sensitive:
    key: "1234567890abcdef"

创建加解密服务与脱敏服务,最小实现至少包含:

@Service
public class SensitiveFieldCryptoService
{
    public String encrypt(String plainText) { ... }
    public String decrypt(String cipherText) { ... }
}
@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: 提交本任务
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 个用例:

@Test
void shouldEncryptCustNameAndIdNumBeforeInsert() { ... }

@Test
void shouldMaskCustNameWhenReturningPagedWorkflowList() { ... }

@Test
void shouldUseCustIsnInsteadOfCustNameAsQueryCondition() { ... }

关键断言至少包括:

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 中补最小实现:

loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getCustName()));
loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getIdNum()));
loanPricingWorkflowMapper.insert(loanPricingWorkflow);

要求:

  • 只加密 custNameidNum

  • custIsn 保持原样

  • 配置缺失时直接失败,不增加明文兼容分支

  • Step 4: 收口列表查询条件并补脱敏返回

修改点至少包含:

if (StringUtils.hasText(loanPricingWorkflow.getCustIsn()))
{
    wrapper.like(LoanPricingWorkflow::getCustIsn, loanPricingWorkflow.getCustIsn());
}
pageResult.getRecords().forEach(row ->
        row.setCustName(loanPricingSensitiveDisplayService.maskCustName(
                sensitiveFieldCryptoService.decrypt(row.getCustName()))));

同步从 buildQueryWrapperLoanPricingWorkflowMapper.xml 删除 custName 过滤条件。

  • Step 5: 重新运行服务测试

Run: mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test Expected: PASS

  • Step 6: 补充实体与 VO 边界说明性调整

若测试或编译需要,在 LoanPricingWorkflowLoanPricingWorkflowListVO 中补充本次链路使用的字段,并保持对象语义清晰;不要新增明文副本字段。

  • Step 7: 提交本任务
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: 写详情、模型输出展示与模型调用失败测试

新增或补充以下测试场景:

@Test
void shouldMaskCustNameAndIdNumWhenReturningWorkflowDetail() { ... }
@Test
void shouldMaskCustNameAndIdNumInRetailModelOutputBasicInfo() { ... }
@Test
void shouldMaskCustNameAndIdNumInCorporateModelOutputBasicInfo() { ... }
@Test
void shouldDecryptCustNameAndIdNumBeforeInvokeModel() { ... }

关键断言至少包括:

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 中加入类似处理:

String plainCustName = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getCustName());
String plainIdNum = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getIdNum());
loanPricingWorkflow.setCustName(loanPricingSensitiveDisplayService.maskCustName(plainCustName));
loanPricingWorkflow.setIdNum(loanPricingSensitiveDisplayService.maskIdNum(plainIdNum));

要求:

  • 对外返回对象中不保留明文

  • 保持既有测算利率与执行利率逻辑不变

  • modelRetailOutputFieldsmodelCorpOutputFields 非空,同样对其 custNameidNum 做脱敏替换

  • Step 4: 在模型调用前显式解密

LoanPricingModelService#invokeModelAsync 中补最小实现:

loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getCustName()));
loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getIdNum()));
BeanUtils.copyProperties(loanPricingWorkflow, modelInvokeDTO);

要求:

  • 只解密 custNameidNum

  • 解密失败直接中断模型调用并记录错误

  • 不修改模型输出表处理逻辑

  • 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: 提交本任务
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: 新增历史数据清理脚本

创建脚本,至少包含:

DELETE FROM model_retail_output_fields;
DELETE FROM model_corp_output_fields;
DELETE FROM loan_pricing_workflow;

要求:

  • 删除顺序满足外键或业务依赖关系

  • 只清理贷款定价流程相关数据

  • Step 2: 写后端实施记录

实施记录至少写明:

- 已新增贷款定价敏感字段加解密服务与展示脱敏服务
- 已在流程创建链路对 `custName``idNum` 加密后入库
- 已在详情返回与列表返回链路统一脱敏
- 已在模型调用前显式解密敏感字段
- 已新增历史数据清理脚本
  • Step 3: 手工验证数据库落库与返回链路

Run: 按项目现有方式启动后端,创建一条个人流程和一条企业流程,再查询列表与详情。 Expected:

  • 数据库中的 cust_nameid_num 不等于前端提交明文

  • 列表和详情返回的 custNameidNum 为脱敏值

  • 模型输出“基本信息”页签中的 custNameidNum 也为脱敏值

  • Step 4: 如果为验证启动了后端进程,结束对应进程

Run: ps -ef | rg 'RuoYiApplication|loan-pricing|java' Expected: 仅停止本次验证启动的后端进程;对非本次启动进程不做处理。

  • Step 5: 提交本任务
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 "补充贷款定价敏感字段后端实施记录"