From c37456983f95377e525cb4188b08ef7c02d5205c Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Wed, 15 Apr 2026 18:23:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E5=85=AC=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + ...04-15-Breadcrumb重复key修复前端实施记录.md | 90 ++++++++++++++++++ ...-04-15-上虞对公展示指标对齐前端实施计划.md | 26 +++++ ...-04-15-上虞对公展示指标对齐后端实施计划.md | 39 ++++++++ ...2026-04-15-审计字段自动填充后端实施记录.md | 22 +++++ ...详情测算结果与风险分析分组调整前端实施记录.md | 24 +++++ doc/2026-04-15-开发库补列SQL落盘实施记录.md | 16 ++++ ...$上虞对公利率测算_上传字段与展示字段 .xlsx | Bin 0 -> 165 bytes doc/上虞对公利率测算_上传字段与展示字段 .xlsx | Bin 0 -> 17170 bytes .../config/handler/MyMetaHandler.java | 34 +++++++ .../dto/CorporateLoanPricingCreateDTO.java | 16 +++- .../domain/dto/ModelInvokeDTO.java | 20 +++- .../domain/entity/LoanPricingWorkflow.java | 17 +++- .../domain/entity/ModelCorpOutputFields.java | 20 +++- .../service/LoanPricingModelService.java | 12 +++ .../impl/LoanPricingWorkflowServiceImpl.java | 5 + .../util/LoanPricingConverter.java | 6 +- .../src/main/resources/data/corp_output.json | 20 ++-- .../service/LoanPricingModelServiceTest.java | 26 +++++ .../LoanPricingWorkflowServiceImplTest.java | 29 ++++++ ruoyi-ui/src/components/Breadcrumb/index.vue | 5 +- ruoyi-ui/src/components/Breadcrumb/utils.js | 9 ++ .../components/CorporateCreateDialog.vue | 84 +++++++--------- .../components/CorporateWorkflowDetail.vue | 9 +- .../components/ModelOutputDisplay.vue | 39 +++----- ...an_pricing_alter_20260415_repay_method.sql | 26 +++++ sql/loan_pricing_prod_init_20260331.sql | 12 ++- sql/loan_pricing_required_data_20260328.sql | 26 ++--- sql/loan_pricing_schema_20260328.sql | 12 ++- sql/loan_pricing_workflow.sql | 5 +- sql/model_corp.sql | 9 +- test_api/test_corporate_create.http | 43 +++++---- test_api/test_corporate_create.sh | 59 +++++++----- 33 files changed, 598 insertions(+), 165 deletions(-) create mode 100644 doc/2026-04-15-Breadcrumb重复key修复前端实施记录.md create mode 100644 doc/2026-04-15-上虞对公展示指标对齐前端实施计划.md create mode 100644 doc/2026-04-15-上虞对公展示指标对齐后端实施计划.md create mode 100644 doc/2026-04-15-审计字段自动填充后端实施记录.md create mode 100644 doc/2026-04-15-对公流程详情测算结果与风险分析分组调整前端实施记录.md create mode 100644 doc/2026-04-15-开发库补列SQL落盘实施记录.md create mode 100644 doc/~$上虞对公利率测算_上传字段与展示字段 .xlsx create mode 100644 doc/上虞对公利率测算_上传字段与展示字段 .xlsx create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/config/handler/MyMetaHandler.java create mode 100644 ruoyi-ui/src/components/Breadcrumb/utils.js create mode 100644 sql/loan_pricing_alter_20260415_repay_method.sql diff --git a/.gitignore b/.gitignore index 51f3bb8..2be6f5e 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ nbdist/ !*/build/*.xml logs/ +.playwright-cli/ +ruoyi-ui/tests +*/src/test \ No newline at end of file diff --git a/doc/2026-04-15-Breadcrumb重复key修复前端实施记录.md b/doc/2026-04-15-Breadcrumb重复key修复前端实施记录.md new file mode 100644 index 0000000..3d159ee --- /dev/null +++ b/doc/2026-04-15-Breadcrumb重复key修复前端实施记录.md @@ -0,0 +1,90 @@ +# Breadcrumb 重复 key 修复前端实施记录 + +## 1. 实际改动内容 + +### 1.1 修复 Breadcrumb 重复 key 告警 + +修改文件: + +- `ruoyi-ui/src/components/Breadcrumb/index.vue` +- `ruoyi-ui/src/components/Breadcrumb/utils.js` + +改动内容: + +- 将 Breadcrumb 列表项的 `key` 生成逻辑从直接使用 `item.path` 调整为统一调用 `buildBreadcrumbItemKey` +- 新增 `buildBreadcrumbItemKey(item, index)` 工具方法,使用 `path + title + index` 组合生成稳定且唯一的 key +- 保持现有面包屑展示逻辑不变,不调整路由结构、不修改首页与当前页的展示顺序 + +根因说明: + +- 当前项目的 Breadcrumb 会在非首页场景外额外插入一个 `首页` 面包屑,路径固定为 `'/index'` +- 当当前页面本身也对应 `'/index'` 时,原逻辑使用 `item.path` 作为 `transition-group` 的 key,会同时生成两个 `'/index'` +- Vue 因此抛出 `Duplicate keys detected: '/index'` + +### 1.2 增加最小回归测试 + +修改文件: + +- `ruoyi-ui/tests/breadcrumb-duplicate-key.test.js` + +改动内容: + +- 新增最小 Node 断言脚本 +- 校验当两个 Breadcrumb 条目 path 同为 `'/index'` 时,生成的 key 仍然唯一 +- 锁定本次问题,避免后续调整 Breadcrumb 时再次引入相同告警 + +## 2. 验证结果 + +### 2.1 Node 版本 + +项目中未提供 `.nvmrc`,因此未能直接执行 `nvm use` 自动切换。 + +实际使用版本: + +- `nvm use 14.21.3` + +### 2.2 测试命令 + +已执行: + +- `cd ruoyi-ui && source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && node tests/breadcrumb-duplicate-key.test.js` + +结果: + +- 测试通过 +- 输出 `breadcrumb duplicate key assertions passed` + +### 2.3 构建命令 + +已执行: + +- `cd ruoyi-ui && source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && npm run build:prod` + +结果: + +- 构建成功 +- 输出 `DONE Build complete. The dist directory is ready to be deployed.` + +### 2.4 构建告警 + +存在 webpack 资源体积告警: + +- `asset size limit` +- `entrypoint size limit` + +说明: + +- 这些是现有项目静态资源体积告警 +- 本次 Breadcrumb 修复未引入新的构建错误或新的语法告警 + +## 3. 影响范围 + +- 仅涉及前端 Breadcrumb 组件 +- 未修改后端代码 +- 未修改贷款定价业务字段逻辑 + +## 4. 当前结论 + +- `Duplicate keys detected: '/index'` 的 Breadcrumb 告警已修复 +- 修复方式限定在组件 key 生成逻辑,属于最短路径处理 +- 前端回归测试与生产构建均已通过 diff --git a/doc/2026-04-15-上虞对公展示指标对齐前端实施计划.md b/doc/2026-04-15-上虞对公展示指标对齐前端实施计划.md new file mode 100644 index 0000000..041cb85 --- /dev/null +++ b/doc/2026-04-15-上虞对公展示指标对齐前端实施计划.md @@ -0,0 +1,26 @@ +# 2026-04-15 上虞对公展示指标对齐前端实施计划 + +## 改动内容 +- 对齐 [CorporateCreateDialog.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue) 的对公新增弹窗: + - 新增 `repayMethod` + - `loanTerm` 改为 `1-6` 年下拉 + - `collType` 改为 `一类/二类/三类/四类` + - 对外提交字段改为 `isTradeBuildEnt` + - `isGreenLoan`、`isTradeBuildEnt`、`collThirdParty` 统一提交 `0/1` + - 移除 `isAgriGuar`、`isTechEnt` +- 对齐 [CorporateWorkflowDetail.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue) 的流程详情录入字段展示: + - 新增 `还款方式` + - `贷款期限` 改为 `借款期限` + - 保留 `绿色贷款`、`贸易和建筑业企业`、`抵质押类型`、`抵质押物是否三方所有` + - 移除 `省农担担保贷款`、`科技型企业` +- 对齐 [ModelOutputDisplay.vue](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue) 的企业模型输出展示口径: + - 展示 `repayMethod`、`isTradeBuildEnt` + - 不再展示 `省农担担保贷款`、`科技型企业` +- 新增/更新前端静态断言: + - [corporate-create-input-params.test.js](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/tests/corporate-create-input-params.test.js) + - [corporate-display-fields.test.js](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-ui/tests/corporate-display-fields.test.js) + +## 验证记录 +- `source ~/.nvm/nvm.sh && nvm use 14 >/dev/null && node tests/corporate-create-input-params.test.js` +- `source ~/.nvm/nvm.sh && nvm use 14 >/dev/null && node tests/corporate-display-fields.test.js` +- `source ~/.nvm/nvm.sh && nvm use 14 >/dev/null && npm run build:prod` diff --git a/doc/2026-04-15-上虞对公展示指标对齐后端实施计划.md b/doc/2026-04-15-上虞对公展示指标对齐后端实施计划.md new file mode 100644 index 0000000..0884d83 --- /dev/null +++ b/doc/2026-04-15-上虞对公展示指标对齐后端实施计划.md @@ -0,0 +1,39 @@ +# 2026-04-15 上虞对公展示指标对齐后端实施计划 + +## 改动内容 +- 对齐对公创建接口 DTO 与模型调用 DTO: + - [CorporateLoanPricingCreateDTO.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java) + - [ModelInvokeDTO.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java) + - 新增 `repayMethod` + - 对外字段改为 `isTradeBuildEnt` + - `loanTerm` 校验为 `1-6` + - `collType` 校验为 `一类/二类/三类/四类` +- 对齐流程实体、详情出参和模型输出镜像: + - [LoanPricingWorkflow.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java) + - [ModelCorpOutputFields.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelCorpOutputFields.java) + - [LoanPricingConverter.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java) + - [LoanPricingModelService.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java) + - [LoanPricingWorkflowServiceImpl.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java) + - 内部继续复用 `isTradeConstruction` 落库,外部统一返回 `isTradeBuildEnt` + - `isAgriGuar`、`isTechEnt` 从对外 JSON 隐藏 + - 企业模型输出补充 `repayMethod`、`isTradeBuildEnt` 展示镜像 +- 对齐 mock 与 SQL 资产: + - [corp_output.json](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/main/resources/data/corp_output.json) + - [loan_pricing_workflow.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/sql/loan_pricing_workflow.sql) + - [loan_pricing_schema_20260328.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/sql/loan_pricing_schema_20260328.sql) + - [loan_pricing_prod_init_20260331.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/sql/loan_pricing_prod_init_20260331.sql) + - [loan_pricing_required_data_20260328.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/sql/loan_pricing_required_data_20260328.sql) + - [loan_pricing_alter_20260415_repay_method.sql](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/sql/loan_pricing_alter_20260415_repay_method.sql) + - [test_corporate_create.http](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/test_api/test_corporate_create.http) + - [test_corporate_create.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/test_api/test_corporate_create.sh) + - `loan_pricing_workflow` 增加 `repay_method` + - mock 数据和接口样例统一为 Excel 字段名与 `0/1` 口径 + - 补充独立增量 SQL,便于其他环境按最小影响同步结构 +- 新增/更新后端定向单测: + - [LoanPricingModelServiceCorporateParamsTest.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceCorporateParamsTest.java) + - [LoanPricingModelServiceTest.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java) + - [LoanPricingWorkflowServiceImplTest.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java) + - [LoanPricingConverterTest.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java) + +## 验证记录 +- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServiceCorporateParamsTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest,LoanPricingConverterTest -Dsurefire.failIfNoSpecifiedTests=false test` diff --git a/doc/2026-04-15-审计字段自动填充后端实施记录.md b/doc/2026-04-15-审计字段自动填充后端实施记录.md new file mode 100644 index 0000000..b7c6612 --- /dev/null +++ b/doc/2026-04-15-审计字段自动填充后端实施记录.md @@ -0,0 +1,22 @@ +# 2026-04-15 审计字段自动填充后端实施记录 + +## 背景 + +- 贷款定价流程实体已经声明了 MyBatis-Plus 的 `FieldFill`,但当前分支缺少迁移源分支中的统一审计填充处理器。 +- 导致 `insert` 和 `updateById` 执行时,`createBy`、`createTime`、`updateBy`、`updateTime` 不会自动写入或刷新。 + +## 本次改动 + +- 新增 [MyMetaHandler.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-framework/src/main/java/com/ruoyi/framework/config/handler/MyMetaHandler.java),恢复与迁移源分支一致的统一审计填充逻辑。 +- 审计人格式保持与源分支一致,统一写入 `昵称-用户名`。 +- 新增 [MyMetaHandlerTest.java](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-framework/src/test/java/com/ruoyi/framework/config/handler/MyMetaHandlerTest.java),覆盖插入填充与更新填充两个核心场景。 + +## 验证结果 + +- 执行 `mvn -pl ruoyi-framework -am -Dtest=MyMetaHandlerTest -Dsurefire.failIfNoSpecifiedTests=false test`,通过。 +- 执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=MyMetaHandlerTest,LoanPricingWorkflowServiceImplTest,LoanPricingModelServiceTest,LoanPricingModelServiceCorporateParamsTest,LoanPricingModelServicePersonalParamsTest -Dsurefire.failIfNoSpecifiedTests=false test`,通过。 + +## 影响说明 + +- 所有使用 MyBatis-Plus 自动填充并声明对应字段的实体,在当前登录上下文下执行新增和更新时,都会自动维护审计字段。 +- 本次未改动贷款定价业务入参、SQL 结构和前端页面行为。 diff --git a/doc/2026-04-15-对公流程详情测算结果与风险分析分组调整前端实施记录.md b/doc/2026-04-15-对公流程详情测算结果与风险分析分组调整前端实施记录.md new file mode 100644 index 0000000..03552b5 --- /dev/null +++ b/doc/2026-04-15-对公流程详情测算结果与风险分析分组调整前端实施记录.md @@ -0,0 +1,24 @@ +# 对公流程详情测算结果与风险分析分组调整前端实施记录 + +## 变更日期 +- 2026-04-15 + +## 变更范围 +- 前端页面:`ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue` +- 前端校验:`ruoyi-ui/tests/corporate-create-input-params.test.js` +- 前端校验:`ruoyi-ui/tests/corporate-display-fields.test.js` + +## 实施内容 +- 将对公流程详情“模型输出”卡片中的“测算结果”从原“风险度与测算结果”合并分组中拆出。 +- 按页面要求将对公模型输出分组顺序调整为“基本信息 → 测算结果 → 忠诚度分析 → 贡献度分析 → 关联度分析 → 风险分析”。 +- 保留“风险分析”在模型输出卡片末尾,仅调整展示分组,不修改接口字段、父组件传参和格式化逻辑。 +- 补充前端断言,校验对公模型输出存在独立“测算结果”“风险分析”标题,且不再保留“风险度与测算结果”合并标题。 + +## 影响说明 +- 本次仅涉及前端详情页展示层,不涉及后端接口、数据库脚本和模型测算逻辑。 +- 对公流程详情页中,用户可在基本信息后直接查看测算结果,风险分析独立展示且位于模型输出末尾。 + +## 验证结果 +- 执行 `node ruoyi-ui/tests/corporate-create-input-params.test.js`,断言通过。 +- 执行 `node ruoyi-ui/tests/corporate-display-fields.test.js`,断言通过。 +- 执行 `cd ruoyi-ui && nvm use 14.21.3 && npm run build:prod`,前端生产构建通过。 diff --git a/doc/2026-04-15-开发库补列SQL落盘实施记录.md b/doc/2026-04-15-开发库补列SQL落盘实施记录.md new file mode 100644 index 0000000..be0dcf8 --- /dev/null +++ b/doc/2026-04-15-开发库补列SQL落盘实施记录.md @@ -0,0 +1,16 @@ +# 2026-04-15 开发库补列 SQL 落盘实施记录 + +## 修改内容 +- 将开发库已执行的对公字段补列 SQL 整理并保存到 `sql/loan_pricing_alter_20260415_repay_method.sql`。 +- 在原有 `loan_pricing_workflow.repay_method` 基础上,补充 `model_corp_output_fields` 的以下字段变更语句: + - `repay_method` + - `is_trade_build_ent` + - `loan_rate_history` + - `min_rate_product` + - `smooth_range` + - `final_calculate_rate` + - `reference_rate` + +## 结果 +- 现有 SQL 文件已可直接用于同步开发库本次字段补齐变更。 +- 文件内容与本次实际执行到开发库的语句保持一致。 diff --git a/doc/~$上虞对公利率测算_上传字段与展示字段 .xlsx b/doc/~$上虞对公利率测算_上传字段与展示字段 .xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3c3a794080821fc614a37e537889505a44d8e2ae GIT binary patch literal 165 ZcmZQB&rVh#9WXPLGh{O)Gbqpn0sshg4^aRB literal 0 HcmV?d00001 diff --git a/doc/上虞对公利率测算_上传字段与展示字段 .xlsx b/doc/上虞对公利率测算_上传字段与展示字段 .xlsx new file mode 100644 index 0000000000000000000000000000000000000000..679fcd96dc409d386a783ac443e57faa3a1205df GIT binary patch literal 17170 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 literal 0 HcmV?d00001 diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/handler/MyMetaHandler.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/handler/MyMetaHandler.java new file mode 100644 index 0000000..29ac851 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/handler/MyMetaHandler.java @@ -0,0 +1,34 @@ +package com.ruoyi.framework.config.handler; + +import java.util.Date; + +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.ruoyi.common.utils.SecurityUtils; + +/** + * 实体类自动配置创建日期和更新日期 + */ +@Component +public class MyMetaHandler implements MetaObjectHandler +{ + @Override + public void insertFill(MetaObject metaObject) + { + String auditUser = SecurityUtils.getLoginUser().getUser().getNickName() + '-' + SecurityUtils.getUsername(); + this.setFieldValByName("createBy", auditUser, metaObject); + this.setFieldValByName("createTime", new Date(), metaObject); + this.setFieldValByName("updateBy", auditUser, metaObject); + this.setFieldValByName("updateTime", new Date(), metaObject); + } + + @Override + public void updateFill(MetaObject metaObject) + { + String auditUser = SecurityUtils.getLoginUser().getUser().getNickName() + '-' + SecurityUtils.getUsername(); + this.setFieldValByName("updateBy", auditUser, metaObject); + this.setFieldValByName("updateTime", new Date(), metaObject); + } +} diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java index 2675651..6ea5326 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java @@ -19,12 +19,18 @@ public class CorporateLoanPricingCreateDTO implements Serializable { @NotBlank(message = "客户内码不能为空") private String custIsn; + private String custType; + private String custName; private String idType; private String idNum; + @NotBlank(message = "还款方式不能为空") + @Pattern(regexp = "^(分期|不分期)$", message = "还款方式必须是:分期、不分期之一") + private String repayMethod; + @NotBlank(message = "担保方式不能为空") @Pattern(regexp = "^(信用|保证|抵押|质押)$", message = "担保方式必须是:信用、保证、抵押、质押之一") private String guarType; @@ -32,16 +38,16 @@ public class CorporateLoanPricingCreateDTO implements Serializable { @NotBlank(message = "申请金额不能为空") private String applyAmt; + @NotBlank(message = "借款期限不能为空") + @Pattern(regexp = "^[1-6]$", message = "借款期限必须是 1 到 6 年") private String loanTerm; - private String isAgriGuar; - private String isGreenLoan; - private String isTechEnt; - - private String isTradeConstruction; + private String isTradeBuildEnt; + @NotBlank(message = "抵质押类型不能为空") + @Pattern(regexp = "^(一类|二类|三类|四类)$", message = "抵质押类型必须是:一类、二类、三类、四类之一") private String collType; private String collThirdParty; diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java index d65bbfb..ac2a94f 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java @@ -43,6 +43,12 @@ public class ModelInvokeDTO { */ private String guarType; + /** + * 还款方式(必填) + * 可选值:分期/不分期 + */ + private String repayMethod; + /** * 中间业务_个人_快捷支付(非必填) * 可选值:true/false @@ -103,6 +109,18 @@ public class ModelInvokeDTO { */ private String isAgriGuar; + /** + * 绿色贷款(非必填) + * 可选值:0/1 + */ + private String isGreenLoan; + + /** + * 贸易和建筑业企业(非必填) + * 可选值:0/1 + */ + private String isTradeBuildEnt; + /** * 是否纳税信用等级A级(非必填) * 可选值:true/false @@ -137,7 +155,7 @@ public class ModelInvokeDTO { /** * 抵质押类型(非必填) - * 可选值:一类/二类/三类 + * 可选值:一类/二类/三类/四类 */ private String collType; diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java index f64a162..3c82e5f 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java @@ -1,6 +1,7 @@ package com.ruoyi.loanpricing.domain.entity; import com.baomidou.mybatisplus.annotation.*; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import javax.validation.constraints.NotBlank; @@ -48,6 +49,9 @@ public class LoanPricingWorkflow implements Serializable @NotBlank(message = "担保方式不能为空") private String guarType; + /** 还款方式: 分期/不分期 */ + private String repayMethod; + /** 中间业务_个人_快捷支付: true/false */ private String midPerQuickPay; @@ -65,7 +69,7 @@ public class LoanPricingWorkflow implements Serializable private String applyAmt; /** - * 贷款期限 + * 借款期限(年) */ private String loanTerm; @@ -79,13 +83,21 @@ public class LoanPricingWorkflow implements Serializable private String isManufacturing; /** 省农担担保贷款: true/false */ + @JsonIgnore private String isAgriGuar; /** * 贸易和建筑业企业标识: true/false */ + @JsonIgnore private String isTradeConstruction; + /** + * 贸易和建筑业企业标识: 0/1 + */ + @TableField(exist = false) + private String isTradeBuildEnt; + /** * 绿色贷款: true/false */ @@ -94,6 +106,7 @@ public class LoanPricingWorkflow implements Serializable /** * 科技型企业: true/false */ + @JsonIgnore private String isTechEnt; /** 是否纳税信用等级A级: true/false */ @@ -111,7 +124,7 @@ public class LoanPricingWorkflow implements Serializable /** 循环功能: true/false */ private String loanLoop; - /** 抵质押类型: 一线/一类/二类 */ + /** 抵质押类型: 一类/二类/三类/四类 */ private String collType; /** 抵质押物是否三方所有: true/false */ diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelCorpOutputFields.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelCorpOutputFields.java index c7ec5ad..a5f234a 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelCorpOutputFields.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelCorpOutputFields.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import java.util.Date; @@ -31,6 +32,9 @@ public class ModelCorpOutputFields { private String idType; // 证件号码 private String idNum; + // 还款方式 + @TableField(exist = false) + private String repayMethod; // 基准利率 private String baseLoanRate; // 我行首贷客户 @@ -79,12 +83,14 @@ public class ModelCorpOutputFields { private String isCleanEnt; // 开立基本结算账户 private String hasSettleAcct; + // 贸易和建筑业企业 + @TableField(exist = false) + private String isTradeBuildEnt; // 省农担担保贷款 + @JsonIgnore private String isAgriGuar; // 绿色贷款 private String isGreenLoan; - // 科技型企业 - private String isTechEnt; // BP_企业客户类别 private String bpEntType; // TOTAL_BP_关联度 @@ -119,6 +125,16 @@ public class ModelCorpOutputFields { private String totalBp; // 测算利率 private String calculateRate; + // 历史利率 + private String loanRateHistory; + // 产品最低利率下限 + private String minRateProduct; + // 平滑幅度 + private String smoothRange; + // 最终测算利率 + private String finalCalculateRate; + // 参考利率 + private String referenceRate; @TableField(fill = FieldFill.INSERT) private Date createTime; diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java index c4b457e..93c8338 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingModelService.java @@ -59,10 +59,15 @@ public class LoanPricingModelService { } ModelInvokeDTO modelInvokeDTO = new ModelInvokeDTO(); BeanUtils.copyProperties(loanPricingWorkflow, modelInvokeDTO); + modelInvokeDTO.setIsTradeBuildEnt(toZeroOne(loanPricingWorkflow.getIsTradeConstruction())); if ("个人".equals(loanPricingWorkflow.getCustType())) { normalizePersonalModelInvokeDTO(modelInvokeDTO); } + if ("企业".equals(loanPricingWorkflow.getCustType())) + { + normalizeCorporateModelInvokeDTO(modelInvokeDTO); + } JSONObject response = modelService.invokeModel(modelInvokeDTO); if (loanPricingWorkflow.getCustType().equals("个人")){ // 个人模型 @@ -94,6 +99,13 @@ public class LoanPricingModelService { modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty())); } + private void normalizeCorporateModelInvokeDTO(ModelInvokeDTO modelInvokeDTO) + { + modelInvokeDTO.setIsGreenLoan(toZeroOne(modelInvokeDTO.getIsGreenLoan())); + modelInvokeDTO.setIsTradeBuildEnt(toZeroOne(modelInvokeDTO.getIsTradeBuildEnt())); + modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty())); + } + private String toZeroOne(String value) { if ("true".equals(value) || "1".equals(value)) 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 6d80edb..e17838e 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 @@ -78,6 +78,8 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi { loanPricingWorkflow.setRunType("1"); } + loanPricingWorkflow.setCustType("企业".equals(loanPricingWorkflow.getCustType()) ? "企业" : loanPricingWorkflow.getCustType()); + loanPricingWorkflow.setIsTradeBuildEnt(loanPricingWorkflow.getIsTradeConstruction()); loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getCustName())); loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getIdNum())); @@ -163,6 +165,7 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi String plainIdNum = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getIdNum()); loanPricingWorkflow.setCustName(loanPricingSensitiveDisplayService.maskCustName(plainCustName)); loanPricingWorkflow.setIdNum(loanPricingSensitiveDisplayService.maskIdNum(plainIdNum)); + loanPricingWorkflow.setIsTradeBuildEnt(loanPricingWorkflow.getIsTradeConstruction()); loanPricingWorkflowVO.setLoanPricingWorkflow(loanPricingWorkflow); if (Objects.nonNull(loanPricingWorkflow.getModelOutputId())){ @@ -180,6 +183,8 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi if (Objects.nonNull(modelCorpOutputFields)) { maskModelCorpOutputBasicInfo(modelCorpOutputFields); + modelCorpOutputFields.setRepayMethod(loanPricingWorkflow.getRepayMethod()); + modelCorpOutputFields.setIsTradeBuildEnt(loanPricingWorkflow.getIsTradeBuildEnt()); loanPricingWorkflow.setLoanRate(modelCorpOutputFields.getCalculateRate()); } loanPricingWorkflowVO.setModelCorpOutputFields(modelCorpOutputFields); diff --git a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java index ca0715e..873f8aa 100644 --- a/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java +++ b/ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java @@ -52,15 +52,15 @@ public class LoanPricingConverter { entity.setCustName(dto.getCustName()); entity.setIdType(dto.getIdType()); entity.setIdNum(dto.getIdNum()); + entity.setRepayMethod(dto.getRepayMethod()); entity.setGuarType(dto.getGuarType()); entity.setApplyAmt(dto.getApplyAmt()); entity.setCollType(dto.getCollType()); entity.setCollThirdParty(dto.getCollThirdParty()); // 映射企业特有字段 - entity.setIsAgriGuar(dto.getIsAgriGuar()); entity.setIsGreenLoan(dto.getIsGreenLoan()); - entity.setIsTechEnt(dto.getIsTechEnt()); - entity.setIsTradeConstruction(dto.getIsTradeConstruction()); + entity.setIsTradeConstruction(dto.getIsTradeBuildEnt()); + entity.setIsTradeBuildEnt(dto.getIsTradeBuildEnt()); entity.setLoanTerm(dto.getLoanTerm()); return entity; } diff --git a/ruoyi-loan-pricing/src/main/resources/data/corp_output.json b/ruoyi-loan-pricing/src/main/resources/data/corp_output.json index 5752edc..65e8f93 100644 --- a/ruoyi-loan-pricing/src/main/resources/data/corp_output.json +++ b/ruoyi-loan-pricing/src/main/resources/data/corp_output.json @@ -4,11 +4,12 @@ "tokenId": "17364055486305E7F4722M8IPFWNL8TOBEB", "mappingOutputFields": { "custIsn": "CUST20260121001", - "custType": "企业客户", - "guarType": "抵押担保", + "custType": "企业", + "guarType": "抵押", "custName": "北京智联科技有限公司", "idType": "营业执照", "idNum": "91110108MA00XXXXXX", + "repayMethod": "分期", "baseLoanRate": "3.45", "isFirstLoan": "N", "faithDay": "730", @@ -33,17 +34,16 @@ "bpPayroll": "4.1", "isCleanEnt": "Y", "hasSettleAcct": "Y", - "isAgriGuar": "N", - "isGreenLoan": "Y", - "isTechEnt": "Y", + "isGreenLoan": "1", + "isTradeBuildEnt": "0", "bpEntType": "7.5", "totoalBpRelevance": "9.2", - "loanTerm": "36", + "loanTerm": "6", "bpLoanTerm": "3.3", - "applyAmt": "5000000.00", + "applyAmt": "1000000.00", "bpLoanAmount": "5.8", - "collType": "房产抵押", - "collThirdParty": "N", + "collType": "四类", + "collThirdParty": "1", "bpCollateral": "4.5", "greyCust": "N", "prinOverdue": "N", @@ -65,4 +65,4 @@ "workflowVersion": 14, "callTime": 1736405548630, "status": 1 -} \ No newline at end of file +} diff --git a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java index e983273..31e3bf5 100644 --- a/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java +++ b/ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java @@ -87,4 +87,30 @@ class LoanPricingModelServiceTest !Objects.equals("张三", entity.getCustName()) && !Objects.equals("110101199001011234", entity.getIdNum()))); } + + @Test + void shouldSendCorporateRepayMethodAndTradeBuildFlagToModel() + { + LoanPricingWorkflow workflow = new LoanPricingWorkflow(); + workflow.setId(3L); + workflow.setCustType("企业"); + workflow.setCustName("cipher-name"); + workflow.setIdNum("cipher-id"); + workflow.setRepayMethod("分期"); + workflow.setIsTradeConstruction("1"); + + JSONObject response = new JSONObject(); + response.put("calculateRate", "4.10"); + + when(loanPricingWorkflowMapper.selectById(3L)).thenReturn(workflow); + when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("上虞测试企业"); + when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("91330000123456789X"); + when(modelService.invokeModel(any())).thenReturn(response); + + loanPricingModelService.invokeModelAsync(3L); + + verify(modelService).invokeModel(argThat((ModelInvokeDTO dto) -> + Objects.equals("分期", dto.getRepayMethod()) + && Objects.equals("1", dto.getIsTradeBuildEnt()))); + } } 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 15e0bcc..29f4be1 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 @@ -253,4 +253,33 @@ class LoanPricingWorkflowServiceImplTest assertEquals("测试****公司", result.getModelCorpOutputFields().getCustName()); assertEquals("91*************00X", result.getModelCorpOutputFields().getIdNum()); } + + @Test + void shouldReturnCorporateDisplaySheetFieldsInWorkflowDetail() + { + LoanPricingWorkflow workflow = new LoanPricingWorkflow(); + workflow.setSerialNum("C20260415001"); + workflow.setCustType("企业"); + workflow.setCustName("cipher-name"); + workflow.setIdNum("cipher-id"); + workflow.setRepayMethod("分期"); + workflow.setIsTradeConstruction("1"); + workflow.setModelOutputId(23L); + + ModelCorpOutputFields corpOutputFields = new ModelCorpOutputFields(); + corpOutputFields.setCalculateRate("4.95"); + + when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow); + when(modelCorpOutputFieldsMapper.selectById(23L)).thenReturn(corpOutputFields); + when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("上虞测试企业"); + when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("91330000123456789X"); + when(loanPricingSensitiveDisplayService.maskCustName("上虞测试企业")).thenReturn("上虞***企业"); + when(loanPricingSensitiveDisplayService.maskIdNum("91330000123456789X")).thenReturn("91*************89X"); + + LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("C20260415001"); + + assertEquals("分期", result.getModelCorpOutputFields().getRepayMethod()); + assertEquals("1", result.getModelCorpOutputFields().getIsTradeBuildEnt()); + assertEquals("4.95", result.getLoanPricingWorkflow().getLoanRate()); + } } diff --git a/ruoyi-ui/src/components/Breadcrumb/index.vue b/ruoyi-ui/src/components/Breadcrumb/index.vue index 84f4831..87e6b9e 100644 --- a/ruoyi-ui/src/components/Breadcrumb/index.vue +++ b/ruoyi-ui/src/components/Breadcrumb/index.vue @@ -1,7 +1,7 @@