diff --git a/doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-backend.md b/doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-backend.md index b612ba8..6f49310 100644 --- a/doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-backend.md +++ b/doc/implementation-report-2026-03-30-loan-pricing-sensitive-data-encryption-backend.md @@ -4,6 +4,7 @@ - 在 `ruoyi-loan-pricing` 新增 `SensitiveFieldCryptoService`,统一处理 `custName`、`idNum` 的 AES/ECB/PKCS5Padding + Base64 加解密。 - 在 `ruoyi-loan-pricing` 新增 `LoanPricingSensitiveDisplayService`,统一处理个人姓名、企业名称、身份证号、统一社会信用代码的脱敏展示。 - 在 `LoanPricingWorkflowServiceImpl` 的创建链路对 `custName`、`idNum` 加密后入库,并在列表、详情链路解密后做脱敏返回。 +- 在 `LoanPricingWorkflowServiceImpl` 的详情链路补充对 `ModelRetailOutputFields`、`ModelCorpOutputFields` 基本信息中的 `custName`、`idNum` 进行脱敏,避免模型输出区域继续暴露明文。 - 在 `LoanPricingModelService` 调用模型前显式解密 `custName`、`idNum`,保证模型入参不接收密文;同时补齐 `ModelInvokeDTO.idNum` 字段。 - 修复模型调用后更新 `modelOutputId` 时把解密后的 `custName`、`idNum` 明文回写数据库的问题,改为仅更新 `modelOutputId`。 - 在 `LoanPricingWorkflowMapper.xml` 和服务查询条件中移除按 `custName` 查询,改为按 `custIsn` 查询。 @@ -19,13 +20,14 @@ - 执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=SensitiveFieldCryptoServiceTest,LoanPricingSensitiveDisplayServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`,结果通过。 - 执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`,结果通过。 - 执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`,结果通过。 +- 执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=SensitiveFieldCryptoServiceTest,LoanPricingSensitiveDisplayServiceTest,LoanPricingWorkflowServiceImplTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`,结果通过。 - 执行 `mvn -pl ruoyi-loan-pricing -am -Dsurefire.failIfNoSpecifiedTests=false test`,结果通过。 -- 手工启动 `ruoyi-admin` 到 `18080` 端口,并将 `model.url` 指向 `http://localhost:18080/rate/pricing/mock/invokeModel` 后完成联调: -- 个人流程 `PSEC2026033004` 与企业流程 `CSEC2026033003` 创建成功,接口即时返回的 `custName`、`idNum` 为密文。 -- 通过 MySQL 查询 `loan_pricing_workflow`,确认 `cust_name`、`id_num` 落库为密文,同时 `model_output_id` 正常回填。 -- 调用列表接口 `/loanPricing/workflow/list?custIsn=PSEC2026033004`,确认返回 `custName` 为 `张*`。 -- 调用详情接口 `/loanPricing/workflow/20260330110314523`,确认个人 `custName` 为 `张*`、`idNum` 为 `1101********1234`。 -- 调用详情接口 `/loanPricing/workflow/20260330110252133`,确认企业 `custName` 为 `测试****公司`、`idNum` 为 `91*************00X`。 +- 从根工程重新打包 `ruoyi-admin.jar` 后,以 `18080` 端口启动临时后端实例,并将 `model.url` 指向 `http://localhost:18080/rate/pricing/mock/invokeModel` 完成联调。 +- 个人流程与企业流程创建成功,接口即时返回的 `custName`、`idNum` 为密文。 +- 调用参数错误场景 `/loanPricing/workflow/create/personal` 且缺少 `custIsn`,接口返回 `500`,错误信息为“客户内码不能为空”。 +- 调用列表接口按 `custIsn` 查询,确认个人返回 `custName` 为 `张*`,企业返回 `custName` 为 `测试****公司`。 +- 调用详情接口,确认流程主信息中个人返回 `张* / 1101********1234`,企业返回 `测试****公司 / 91*************00X`。 +- 调用详情接口,确认模型输出“基本信息”中个人返回 `张* / 3301********1234`,企业返回 `北京******公司 / 91*************XXX`。 ## 备注 - 联调过程中发现 `serialNum` 仍使用毫秒时间戳生成,并发创建可能触发 `uk_serial_num` 冲突;该问题为本次验证中暴露的既有风险,本次未纳入敏感字段加密方案范围内处理。 diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java index 6bf51e1..55b78e5 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java @@ -171,6 +171,7 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi ModelRetailOutputFields modelRetailOutputFields = modelRetailOutputFieldsMapper.selectById(loanPricingWorkflow.getModelOutputId()); if (Objects.nonNull(modelRetailOutputFields)) { + maskModelRetailOutputBasicInfo(modelRetailOutputFields); loanPricingWorkflow.setLoanRate(modelRetailOutputFields.getCalculateRate()); } loanPricingWorkflowVO.setModelRetailOutputFields(modelRetailOutputFields); @@ -179,6 +180,7 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi ModelCorpOutputFields modelCorpOutputFields = modelCorpOutputFieldsMapper.selectById(loanPricingWorkflow.getModelOutputId()); if (Objects.nonNull(modelCorpOutputFields)) { + maskModelCorpOutputBasicInfo(modelCorpOutputFields); loanPricingWorkflow.setLoanRate(modelCorpOutputFields.getCalculateRate()); } loanPricingWorkflowVO.setModelCorpOutputFields(modelCorpOutputFields); @@ -220,6 +222,22 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi return wrapper; } + private void maskModelRetailOutputBasicInfo(ModelRetailOutputFields modelRetailOutputFields) + { + modelRetailOutputFields.setCustName( + loanPricingSensitiveDisplayService.maskCustName(modelRetailOutputFields.getCustName())); + modelRetailOutputFields.setIdNum( + loanPricingSensitiveDisplayService.maskIdNum(modelRetailOutputFields.getIdNum())); + } + + private void maskModelCorpOutputBasicInfo(ModelCorpOutputFields modelCorpOutputFields) + { + modelCorpOutputFields.setCustName( + loanPricingSensitiveDisplayService.maskCustName(modelCorpOutputFields.getCustName())); + modelCorpOutputFields.setIdNum( + loanPricingSensitiveDisplayService.maskIdNum(modelCorpOutputFields.getIdNum())); + } + /** * 设定执行利率 * diff --git a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java index f8c9a34..96a90a9 100644 --- a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java +++ b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java @@ -175,6 +175,34 @@ class LoanPricingWorkflowServiceImplTest assertEquals("1101********1234", result.getLoanPricingWorkflow().getIdNum()); } + @Test + void shouldMaskCustNameAndIdNumInRetailModelOutputBasicInfo() + { + LoanPricingWorkflow workflow = new LoanPricingWorkflow(); + workflow.setSerialNum("P20260328001"); + workflow.setCustType("个人"); + workflow.setCustName("cipher-name"); + workflow.setIdNum("cipher-id"); + workflow.setModelOutputId(11L); + + ModelRetailOutputFields retailOutputFields = new ModelRetailOutputFields(); + retailOutputFields.setCustName("张三"); + retailOutputFields.setIdNum("110101199001011234"); + retailOutputFields.setCalculateRate("6.15"); + + when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow); + when(modelRetailOutputFieldsMapper.selectById(11L)).thenReturn(retailOutputFields); + when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三"); + when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234"); + when(loanPricingSensitiveDisplayService.maskCustName("张三")).thenReturn("张*"); + when(loanPricingSensitiveDisplayService.maskIdNum("110101199001011234")).thenReturn("1101********1234"); + + LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("P20260328001"); + + assertEquals("张*", result.getModelRetailOutputFields().getCustName()); + assertEquals("1101********1234", result.getModelRetailOutputFields().getIdNum()); + } + @Test void shouldUseCorporateModelOutputCalculateRateForWorkflowDetail() { @@ -195,4 +223,32 @@ class LoanPricingWorkflowServiceImplTest assertEquals("3.932", result.getLoanPricingWorkflow().getLoanRate()); assertEquals("3.932", result.getModelCorpOutputFields().getCalculateRate()); } + + @Test + void shouldMaskCustNameAndIdNumInCorporateModelOutputBasicInfo() + { + LoanPricingWorkflow workflow = new LoanPricingWorkflow(); + workflow.setSerialNum("C20260328001"); + workflow.setCustType("企业"); + workflow.setCustName("cipher-name"); + workflow.setIdNum("cipher-id"); + workflow.setModelOutputId(22L); + + ModelCorpOutputFields corpOutputFields = new ModelCorpOutputFields(); + corpOutputFields.setCustName("测试科技有限公司"); + corpOutputFields.setIdNum("91110000100000000X"); + corpOutputFields.setCalculateRate("3.932"); + + when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow); + when(modelCorpOutputFieldsMapper.selectById(22L)).thenReturn(corpOutputFields); + when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("测试科技有限公司"); + when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("91110000100000000X"); + when(loanPricingSensitiveDisplayService.maskCustName("测试科技有限公司")).thenReturn("测试****公司"); + when(loanPricingSensitiveDisplayService.maskIdNum("91110000100000000X")).thenReturn("91*************00X"); + + LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("C20260328001"); + + assertEquals("测试****公司", result.getModelCorpOutputFields().getCustName()); + assertEquals("91*************00X", result.getModelCorpOutputFields().getIdNum()); + } }