15 Commits

79 changed files with 3953 additions and 361 deletions

2
.gitignore vendored
View File

@@ -59,3 +59,5 @@ audit.log
.DS_Store
*/.DS_Store
.codegraph/

View File

@@ -1,31 +0,0 @@
# AGENTS.md - AI Coding Assistant Guide
## GIT
- git提交时使用中文添加描述
- 无视`.DS_Store`
## AGENT
- 不开启subagent
## 文档
- 如果是前后端开发任务,根据设计文档产出实施计划时,输出两份执行文档,一份为后端的实施计划,一份为前端的实施计划
- 每一次改动都需要留下实施文档,记录修改的内容
- 每次写设计文档的时候,都要检查一下保存路径是否正确
## 测试
- 开发完成后必须执行与本次改动直接对应的验证步骤,完成验证后才能结束本次任务
- 如果是接口开发完成,先重启后端进程,确保最新代码已经生效,再调用接口进行测试
- 接口测试时必须覆盖多种情况,至少包含正常场景、必填/参数错误场景、分支场景;如接口逻辑包含状态、类型、金额、期限等关键条件,需要分别验证对应分支
- 每次调用外部接口进行测试或联调时,必须在后端日志中完整输出请求 URL、请求参数和返回参数
- 如果是前端页面开发完成,必须启动前端页面并调用浏览器检查功能是否正常,确认页面展示、交互流程、接口联动和关键提示信息符合预期
- 测试结束后,自动结束测试时开启的前后端进程
## 开发
- 在开发前端的时候不需要使用git worktree直接在当前分支进行开发
## 方案规范
- 当需要你给出方案时,必须符合以下规范
- 不允许给出兼容性或补丁性的方案
- 不允许过度设计,保持最短路径实现,且不能违反上一条要求
- 不允许自行给出我提供的需求以外的方案,例如一些兜底和降级方案,这可能导致业务逻辑偏移问题
- 必须确保方案的逻辑正确,必须经过全链路的逻辑验证

View File

@@ -309,7 +309,7 @@ GET /loanPricing/workflow/20250119143025123
"midPerFinMan": "false",
"midPerEtc": "true",
"bpMid": "-15",
"totoalBpRelevance": "-50",
"totalBpRelevance": "-50",
"applyAmt": "500000",
"bpLoanAmount": "0",
"loanPurpose": "consumer",
@@ -325,7 +325,7 @@ GET /loanPricing/workflow/20250119143025123
"interestOverdue": "false",
"cardOverdue": "false",
"bpGreyOverdue": "0",
"totoalBpRisk": "0",
"totalBpRisk": "0",
"totalBp": "-80",
"calculateRate": "2.65"
},
@@ -364,7 +364,7 @@ GET /loanPricing/workflow/20250119143025123
"isGreenLoan": "false",
"isTechEnt": "true",
"bpEntType": "-25",
"totoalBpRelevance": "-55",
"totalBpRelevance": "-55",
"loanTerm": "36",
"bpLoanTerm": "0",
"applyAmt": "1000000",
@@ -377,7 +377,7 @@ GET /loanPricing/workflow/20250119143025123
"interestOverdue": "false",
"cardOverdue": "false",
"bpGreyOverdue": "0",
"totoalBpRisk": "0",
"totalBpRisk": "0",
"totalBp": "-85",
"calculateRate": "2.60"
}

View File

@@ -0,0 +1,34 @@
# 上虞利率定价系统操作手册生成实施记录
## 基本信息
- 日期2026-05-09
- 任务:生成系统操作文档,覆盖主要业务流程、用户管理、部门管理,并在合适位置加入真实页面截图
- 产物:`doc/上虞利率定价系统操作手册-2026-05-09.docx`
## 实施内容
- 梳理前端路由、菜单 SQL、业务流程组件和系统管理页面确认操作手册覆盖范围。
- 临时启动前端开发服务,复用本机 63310 后端服务,通过真实浏览器获取页面截图。
- 截图覆盖登录页、流程列表、客户类型选择、客户号查询、新增个人定价流程、流程详情、用户管理、部门管理。
- 使用 `python-docx` 生成 Word 操作手册,内容包含:
- 文档说明与角色范围
- 登录与页面布局
- 利率定价主要业务流程
- 用户管理操作说明
- 部门管理操作说明
- 日常使用注意事项
## 验证情况
- 已确认前端服务可访问:`http://localhost:8080`
- 已通过 `/login/test` 获取测试登录令牌并进入真实页面截图。
- 已抽查关键截图显示正常,流程详情截图已改为从列表真实记录进入,避免使用无效流水号。
- 已生成 Word 文件:`doc/上虞利率定价系统操作手册-2026-05-09.docx`
- DOCX 渲染检查尝试使用文档技能提供的 `render_docx.py`,当前机器缺少 `soffice`,无法完成逐页 PNG 渲染;截图已内嵌在 Word 文档中。
- 已使用 macOS Quick Look 生成首屏缩略图进行抽查,封面、标题、表格和首张截图显示正常。
## 临时文件
- 截图目录、生成脚本、Playwright 临时依赖均位于 `output/` 下。
- 本次收尾时已清理 `output/`,避免误提交临时文件。

View File

@@ -0,0 +1,33 @@
# 2026-05-11 外部查询接口 GET 参数调用修复实施记录
## 实施内容
- 修复客户号查询客户内码的个人、企业两个外部接口调用方式。
- `LoanPricingCustomerMapService` 构建请求地址时先移除同名 `appCode``cust_id` 参数,再通过 GET query param 追加配置中的公共 `appCode` 和真实客户号。
- 修复历史贷款记录查询外部接口调用方式。
- `LoanRateHistoryService` 构建请求地址时先移除同名 `appCode``cust_isn` 参数,再通过 GET query param 追加配置中的公共 `appCode` 和真实客户内码。
- 调整 profile 外部地址配置。
- `application-pro.yml``application-dev.yml``application-uat.yml` 新增同一个 `loan-pricing-external.app-code` 配置项。
- 生产 profile 三条查询 URL 仅保留接口地址,不再在 URL 中写 `appCode` 或空业务参数。
- 补充服务层单元测试。
- 覆盖个人客户映射、企业客户映射、历史贷款记录三条接口最终均按 GET query param 生成公共 `appCode` 和各自业务参数。
- 测试文件位于 `*/src/test/`,按仓库 `.gitignore` 规则不纳入提交范围,仅用于本地验证。
## 涉及文件
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapService.java`
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanRateHistoryService.java`
- `ruoyi-admin/src/main/resources/application-pro.yml`
- `ruoyi-admin/src/main/resources/application-dev.yml`
- `ruoyi-admin/src/main/resources/application-uat.yml`
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapServiceTest.java`
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanRateHistoryServiceTest.java`
## 验证记录
- 已执行:`mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest,LoanRateHistoryServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 结果:通过,`Tests run: 9, Failures: 0, Errors: 0, Skipped: 0`
- 日志验证:
- 个人客户映射最终请求 URL 为 `http://mock/personal?appCode=abc&cust_id=P001`
- 企业客户映射最终请求 URL 为 `http://mock/corporate?appCode=abc&cust_id=C001`
- 历史贷款记录最终请求 URL 为 `http://mock/history?appCode=abc&cust_isn=81033011438`

View File

@@ -0,0 +1,19 @@
# 2026-05-11 上虞利率定价字段口径调整实施计划记录
## 修改内容
- 新增后端实施计划:`docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-backend-plan.md`
- 新增前端实施计划:`docs/superpowers/plans/2026-05-11-shangyu-pricing-field-adjustment-frontend-plan.md`
- 后端计划覆盖创建 DTO、流程实体、模型入参、转换器、服务层校验、SQL schema 和后端测试。
- 前端计划覆盖个人/企业新增弹窗、业务种类选项、抵质押类型选项、`couponRate` 条件必填、静态断言、构建和 Playwright 真实页面验证。
## 范围说明
- 计划依据:`docs/superpowers/specs/2026-05-11-shangyu-pricing-field-adjustment-design.md`
- 对公 `businessType` 上传模型这一条已按用户确认从本次实施范围排除;计划只覆盖 `couponRate` 的模型入参新增。
- 本次仅产出实施计划,未进入业务代码实现。
## 待验证
- 计划需通过计划审查后再进入实施。
- 后续实现完成后需要补充 `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md`,记录真实代码改动、测试命令和页面验证结果。

View File

@@ -0,0 +1,76 @@
# 上虞利率定价字段调整实施记录
## 基本信息
- 日期2026-05-11
- 范围:上虞利率定价个人/企业新增链路、服务端校验、模型入参、表结构脚本
- 目标:按已确认需求调整业务种类、抵质押类型、存单票面利率字段,以及对私新增入口字段剔除
## 修改内容
### 后端
- 个人新增 DTO
- 业务种类调整为 `新增/存量新增/存量转贷`
- 移除 `loanPurpose``bizProof` 新增入口字段。
- 新增 `couponRate`
- 企业新增 DTO
- 业务种类调整为 `新增/存量新增/存量转贷`
- 企业抵押类型调整为 `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`
- 企业质押类型调整为 `存单质押/股权质押/其他质押`
- 新增 `couponRate`
- 流程实体和模型入参:
- `LoanPricingWorkflow` 新增 `couponRate`
- `ModelInvokeDTO` 新增 `couponRate`,未增加 `businessType` 模型入参。
- 转换器:
- 个人/企业新增 DTO 均映射 `couponRate`
- 个人新增 DTO 不再映射 `loanPurpose``bizProof`
- 服务校验:
- 业务种类仅允许 `新增/存量新增/存量转贷`
-`存量转贷` 要求历史贷款合同。
- 抵押/质押时要求选择抵质押类型。
- 对私/对公按客户类型和担保方式校验各自抵质押类型。
- `质押 + 存单质押` 时要求填写 `couponRate`
- SQL
- 新增 `sql/add_coupon_rate_20260511.sql`
- 同步更新 `loan_pricing_workflow` 建表脚本中的 `coupon_rate` 字段。
### 前端
- 个人新增弹窗:
- 业务种类调整为 `新增/存量新增/存量转贷`
- 移除 `贷款用途``是否有经营佐证`
- 抵押类型调整为 `一线/一类/二类/三类`
- 质押类型调整为 `存单质押/其他质押`
- `质押 + 存单质押` 时显示并必填 `存单票面利率`
- 企业新增弹窗:
- 业务种类调整为 `新增/存量新增/存量转贷`
- 抵押类型调整为 `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`
- 质押类型调整为 `存单质押/股权质押/其他质押`
- `质押 + 存单质押` 时显示并必填 `存单票面利率`
- 共同逻辑:
-`存量转贷` 触发历史贷款合同查询。
- 非存单质押提交时清理 `couponRate`
## 验证结果
- 后端单元测试:
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 结果通过23 个测试全部成功。
- 前端静态断言:
- `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && npm --prefix ruoyi-ui run test:business-type-history-rate'`
- 结果:通过。
- 前端生产构建:
- `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'`
- 结果:构建通过,仅存在既有包体积 warning。
- 真实页面验证:
- 使用 Playwright 打开 `http://localhost:1024/index`
- 使用 `/login/test` 获取登录 token 后访问真实流程列表页面。
- 个人新增弹窗验证:已移除 `贷款用途/是否有经营佐证`;业务种类仅 `存量转贷` 触发历史利率逻辑;个人抵押/质押选项正确;`存单质押``couponRate` 显示并进入必填校验。
- 企业新增弹窗验证:抵押/质押选项正确;`存单质押``couponRate` 显示并进入必填校验;业务种类仅 `存量转贷` 触发历史利率逻辑。
- 验证后已关闭 Playwright 浏览器会话;本次未新启动前后端进程。
## 注意事项
- 控制台中的 `sockjs-node` 报错来自本地 dev-server HMR 连接内网地址失败,不影响本次页面功能验证。
- 表单校验 warning 来自验证时故意触发必填校验。

View File

@@ -0,0 +1,20 @@
# 登录页背景图替换实施记录
## 修改时间
2026-05-12
## 修改内容
- 将登录页背景资源 `ruoyi-ui/src/assets/images/login-background.jpg` 替换为上虞农商银行“心乐为新未来”宣传图。
- 保持登录页现有样式引用不变,继续由 `login.vue``.login` 背景图样式加载该资源。
## 涉及文件
- `ruoyi-ui/src/assets/images/login-background.jpg`
## 验证情况
- 已确认 `ruoyi-ui/src/assets/images/login-background.jpg` 与原始图片 SHA256 一致。
- 已使用 Node 14.21.3 执行 `npm --prefix ruoyi-ui run build:prod`,构建成功;仅存在项目原有包体积 warning。
- 已使用 browser-use 打开真实登录页 `http://localhost:9527/login`,确认新背景图已渲染,账号、密码和登录按钮显示正常。

View File

@@ -0,0 +1,18 @@
# 登录页标题文案移除实施记录
## 修改时间
2026-05-12
## 修改内容
- 移除登录框顶部标题展示,不再显示“上虞利率定价系统”。
## 涉及文件
- `ruoyi-ui/src/views/login.vue`
## 验证情况
- 已检查登录页模板,确认登录框内不再渲染标题节点。
- 已使用 browser-use 打开 `http://localhost:9527/login` 进行实际页面验证,确认页面跳转到登录页后不再出现“上虞利率定价系统”,账号和密码输入项仍正常显示。

View File

@@ -0,0 +1,57 @@
# 流程列表角色数据权限实施记录
## 修改日期
2026-05-12
## 需求范围
- 仅控制 `GET /loanPricing/workflow/list` 流程列表接口。
- 超级管理员 `user_id=1`、启用角色名为“管理员”或角色标识为 `headAdmin` 的用户可查看全部流程。
- 非管理员用户只能查看 `loan_pricing_workflow.create_by` 精确等于当前登录人 `昵称-柜员号` 的流程。
- 列表页“创建者”查询参数继续保留,但只按 `create_by` 中的柜员号部分进行模糊匹配。
## 修改内容
- `LoanPricingWorkflow` 增加非表字段 `dataScopeCreateBy`,专用于后端内部数据权限精确过滤。
- `LoanPricingWorkflowServiceImpl.selectLoanPricingPage` 增加流程列表数据权限裁剪:
- 管理员不写入 `dataScopeCreateBy`
- 非管理员写入 `dataScopeCreateBy = nickName + "-" + username`
- 前端传入 `createBy` 时仍保留原查询参数,但不能扩大非管理员可见范围。
- `LoanPricingWorkflowMapper.xml` 增加 `lpw.create_by = #{query.dataScopeCreateBy}` 精确权限条件。
- `LoanPricingWorkflowMapper.xml``createBy` 查询调整为 `SUBSTRING_INDEX(lpw.create_by, '-', -1) LIKE ...`,即只按柜员号模糊匹配。
- 补充 `LoanPricingWorkflowServiceImplTest``LoanPricingWorkflowMapperXmlTest`,覆盖管理员、业务管理员、客户经理、越权创建者查询参数和 XML 条件。
## 验证记录
- 单元测试通过:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingWorkflowMapperXmlTest -Dsurefire.failIfNoSpecifiedTests=false test
```
- 后端打包通过:
```bash
mvn -pl ruoyi-admin -am clean package -DskipTests
```
- API 验证通过:
- `admin/admin123` 查询流程列表返回全量数据,包含测试行和 `若依-admin` 历史行。
- `8929999/123456` 业务管理员查询流程列表返回全量数据。
- `8920001/123456` 客户经理查询流程列表只返回本人创建的测试行。
- 客户经理按 `createBy=8920001` 查询可返回本人测试行。
- 客户经理按昵称 `createBy=测试客户经理` 查询返回 0 条。
- 客户经理按其他柜员号 `createBy=admin` 查询返回 0 条。
- browser-use 真实页面验证通过:
- 管理员登录真实流程列表页,页面显示 `共 40 条`,可见本人测试行和 `若依-admin` 历史行。
- 客户经理登录真实流程列表页,页面只显示本人创建的测试行。
- 客户经理在“创建者”输入 `admin` 后页面显示暂无数据。
- 客户经理在“创建者”输入 `8920001` 后页面重新显示本人测试行。
## 验证数据与清理
- 因当前开发库创建流程接口依赖的模型输出表缺少 `coupon_rate` 字段,无法通过页面新增生成验证流程。
- 本次验证使用一条临时 SQL 测试数据:`serial_num = ROLE_SCOPE_20260512_001``create_by = 测试客户经理-8920001`
- 验证结束后已删除该临时数据,回查剩余数量为 0。

View File

@@ -0,0 +1,32 @@
# 贷款定价单脚本部署改造实施记录
## 保存路径检查
- 参考脚本:`/Users/wkc/Desktop/ccdi/ccdi/deploy/ccdi_function.sh`
- 新增脚本保存路径:`bin/prod/loan_pricing_function.sh`
- 实施记录保存路径:`doc/implementation-report-2026-05-13-loan-pricing-function-script.md`
## 修改内容
- 新增 `loan_pricing_function.sh`,按 `ccdi_function.sh``deploy``restart``stop` 三命令结构改造为贷款定价可用脚本。
- 按贷款定价现有生产目录约定调整:
- 后端 Jar`backend/ruoyi-admin.jar`
- 前端静态目录:`frontend/dist/`
- 日志目录:`logs/backend-console.log`
- PID 文件:`run/backend.pid`
- 临时目录:`tmp/loan-pricing-function/`
- 备份目录:`backup/YYYYMMDDHHMMSS/`
- 按贷款定价运行参数调整:
- Java 默认目录:`/home/webapp/env/java`
- 后端进程标记:`-Dloan.pricing.home=<脚本目录>`
- Spring Profile`uat`
- 后端端口:`63310`
- 上线包结构固定为根层包含:
- `ruoyi-admin.jar`
- `dist.zip`
- 前端 `dist.zip` 解压后必须包含 `dist/index.html`,部署时写入 `frontend/dist/`
- 默认保持参考脚本的启动后持续输出日志行为,并支持 `FOLLOW_LOGS=0` 供自动化验证跳过持续日志输出。
## 验证结果
- 已执行 `sh -n bin/prod/loan_pricing_function.sh`,语法校验通过。
- 已在临时目录构造 `backend/``frontend/dist/`、根层发布 zip 和假 Java 进程,验证 `deploy` 可完成备份、替换、启动和日志落盘。
- 已验证 `stop` 可停止脚本标记的后端进程并清理 PID 文件。
- 验证过程中产生的临时测试目录已删除,未新增仓库内测试文件。

View File

@@ -0,0 +1,22 @@
# 浏览器页签标题调整实施记录
## 修改时间
2026-05-15
## 修改内容
- 将前端浏览器页签标题从“上虞利率定价系统”调整为“贷款利率定价系统”。
- 同步更新开发、测试、生产环境的 `VUE_APP_TITLE` 配置,确保本地运行和打包产物标题一致。
## 涉及文件
- `ruoyi-ui/.env.development`
- `ruoyi-ui/.env.staging`
- `ruoyi-ui/.env.production`
## 验证情况
- 已通过源码检索确认 `ruoyi-ui` 中页面标题配置已统一为“贷款利率定价系统”。
- 已使用 browser-use 打开 `http://localhost:9527/login` 进行真实页面验证,浏览器标签页标题与 `document.title` 均为“贷款利率定价系统”。
- 验证时仅启动前端服务;因本地后端 `localhost:63310` 未启动,验证码接口代理返回 `ECONNREFUSED`,不影响本次页签标题验证。

View File

@@ -0,0 +1,32 @@
# 利率前端两位小数展示实施记录
## 修改时间
- 2026-05-15
## 修改范围
- `ruoyi-ui/src/utils/rate.js`
- `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
## 修改内容
- 新增 `formatRate` 前端格式化方法,统一将可解析的利率数值展示为小数点后两位。
- 利率定价流程列表的测算利率、执行利率改为通过 `formatRate` 展示。
- 个人/企业流程详情的基准利率、最终测算利率、执行利率初始展示值、历史贷款利率改为两位小数展示。
- 模型输出中的基准利率、测算利率、历史利率、产品最低利率下限、平滑幅度、参考利率、最终测算利率、派生率改为两位小数展示。
- 历史贷款合同选择弹窗和新增流程弹窗中的历史贷款利率展示改为两位小数。
## 影响说明
- 本次仅调整前端展示格式,不改后端接口、数据库字段和模型调用逻辑。
- 历史贷款利率在新增弹窗中仅格式化展示,表单内部仍保留接口返回的原始值。
## 验证
- 已执行 `source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm run build:prod`,构建通过,仅存在资源体积 warning。
- 已启动后端 `http://localhost:63310` 和前端 `http://localhost:9527/`,通过 browser-use 打开真实页面验证。
- 流程列表接口原始返回中存在 `calculateRate = 3.932` 的数据,流程列表页面 `测算利率(%)` 展示为 `3.93`
- 流程列表页面 `执行利率(%)` 展示为 `3.88``6.18``-`,已确认非空利率均为小数点后两位。
- 流程详情页模型输出中,`finalCalculateRate = 3.732` 对应页面展示为 `3.73`,其他利率字段也按两位小数展示。

View File

@@ -0,0 +1,74 @@
# 流程列表支行管理员数据权限实施记录
## 修改日期
2026-05-18
## 需求范围
-`loan_pricing_workflow` 新增 `dept_id`,保存新创建流程的创建人机构号。
- `GET /loanPricing/workflow/list` 增加支行管理员数据权限:
- 超级管理员、角色名“管理员”或角色标识 `headAdmin` 查看全部流程。
- 角色名“支行管理员”或角色标识 `branchAdmin` 查看本人机构及下级机构创建的流程。
- 其他用户继续只查看本人 `create_by` 精确匹配的流程。
- 不回填历史流程数据,历史 `dept_id` 为空的数据不纳入支行管理员机构权限。
- 本次无前端代码改动。
## 修改内容
- 新增 `sql/add_workflow_dept_id_20260518.sql`,为流程表增加 `dept_id` 字段和 `idx_dept_id` 索引。
- 同步更新流程表建表脚本中的 `dept_id` 字段和索引定义。
- `LoanPricingWorkflow` 增加表字段 `deptId` 和列表内部权限字段 `dataScopeDeptId`
- `LoanPricingWorkflowServiceImpl.createLoanPricing` 在插入前写入当前登录人的 `deptId`
- `LoanPricingWorkflowServiceImpl.selectLoanPricingPage` 增加支行管理员分支:
- `headAdmin` 不加权限过滤。
- `branchAdmin` 写入当前登录人的 `dataScopeDeptId`
- 客户经理继续写入 `dataScopeCreateBy`
- `LoanPricingWorkflowMapper.xml` 增加基于 `lpw.dept_id``sys_dept.ancestors` 的本机构及下级机构过滤。
- 创建者查询参数仍按 `SUBSTRING_INDEX(lpw.create_by, '-', -1)` 只模糊匹配柜员号,并与数据权限条件取交集。
## 验证记录
- 单元测试通过:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingWorkflowMapperXmlTest -Dsurefire.failIfNoSpecifiedTests=false test
```
- 后端打包通过:
```bash
mvn -pl ruoyi-admin -am clean package -DskipTests
```
- 已在开发库执行:
```bash
mysql ... loan-pricing < sql/add_workflow_dept_id_20260518.sql
```
- 数据库回查确认 `loan_pricing_workflow.dept_id``idx_dept_id` 已存在。
- API 验证通过:
- `8929999/headAdmin` 查询临时流程返回 6 条,包含全部测试数据。
- `8920100/branchAdmin` 查询临时流程返回 4 条,仅包含本机构、本人和下级机构数据。
- `8920001/客户经理` 查询临时流程返回 1 条,仅包含本人创建数据。
- `8920100/branchAdmin` 使用其他支行创建者 `8920201` 作为查询条件时返回 0 条,创建者查询参数不能扩大数据权限。
- 真实创建接口验证通过:
- `8920100/branchAdmin` 调用个人流程创建接口后,新流程落库 `dept_id=101``create_by=测试支行管理员-8920100`
- browser-use 真实页面验证通过:
- 支行管理员登录真实流程列表页,页面显示 `共 4 条`
- 页面只显示 `BRANCH_SCOPE_20260518_CREATE``BRANCH_SCOPE_20260518_SELF``BRANCH_SCOPE_20260518_SAME``BRANCH_SCOPE_20260518_CHILD`
- 页面未显示其他支行 `BRANCH_SCOPE_20260518_OTHER` 和普通客户经理本人 `BRANCH_SCOPE_20260518_MANAGER`
## 测试数据保留
- 按复测要求,本次验证保留测试用户和测试流程数据,不做清理。
- 保留临时用户:
- `8920100`,昵称 `测试支行管理员`,角色 `branchAdmin`,机构 `101`
- 保留流程数据:
- `BRANCH_SCOPE_20260518_SELF`,机构 `101`,创建者 `测试支行管理员-8920100`
- `BRANCH_SCOPE_20260518_SAME`,机构 `101`,创建者 `同支行客户经理-8920101`
- `BRANCH_SCOPE_20260518_CHILD`,机构 `103`,创建者 `下级客户经理-8920103`
- `BRANCH_SCOPE_20260518_OTHER`,机构 `102`,创建者 `其他支行客户经理-8920201`
- `BRANCH_SCOPE_20260518_MANAGER`,机构 `100`,创建者 `测试客户经理-8920001`
- `BRANCH_SCOPE_20260518_CREATE`,机构 `101`,创建者 `测试支行管理员-8920100`,由真实新增接口创建。

View File

@@ -0,0 +1,19 @@
# 流程列表客户内码字段实施记录
## 修改内容
- 后端流程列表查询 `selectWorkflowPageWithRates` 增加 `lpw.cust_isn AS custIsn`,确保列表接口返回客户内码。
- 流程列表返回对象 `LoanPricingWorkflowListVO` 增加 `custIsn` 字段,承接接口返回值。
- 前端流程列表页新增“客户内码”表格列,字段绑定 `custIsn`,支持超长内容 tooltip 展示。
## 影响范围
- 仅影响利率定价流程列表 `/loanPricing/workflow/list` 的返回字段和页面展示。
- 不修改新增流程、详情页、筛选条件和数据库结构。
## 验证记录
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowListVOTest -Dsurefire.failIfNoSpecifiedTests=false test` 通过。
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 && node tests/customer-map-selection.test.js` 通过。
- 使用真实后端接口 `/loanPricing/workflow/list?pageNum=1&pageSize=3` 验证返回 `custIsn`,前三条返回值为 `81000529053``81000791269``81000769824`
- 使用 browser-use 打开真实流程列表页 `http://localhost:1024/index`,确认表头包含“客户内码”,前三条列表行客户内码展示为 `81000529053``81000791269``81000769824`

View File

@@ -0,0 +1,22 @@
# 流程列表最终测算利率展示实施记录
## 修改内容
- 流程列表联表查询中,企业客户分支由 `model_corp_output_fields.calculate_rate` 改为 `model_corp_output_fields.final_calculate_rate`
- 个人客户分支保持读取 `model_retail_output_fields.final_calculate_rate`
- 前端流程列表列名由“测算利率(%)”调整为“最终测算利率(%)”,继续复用列表接口字段 `calculateRate` 展示,避免扩大接口字段变更范围。
## 影响范围
- 仅影响利率定价流程列表 `/loanPricing/workflow/list` 的利率来源和列名展示。
- 不修改详情页、新增流程、执行利率和数据库结构。
## 验证记录
- 已更新 `LoanPricingWorkflowMapperXmlTest`,约束个人、企业流程列表均取 `final_calculate_rate`
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowMapperXmlTest,LoanPricingWorkflowListVOTest -Dsurefire.failIfNoSpecifiedTests=false test` 通过。
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 && node tests/customer-map-selection.test.js` 通过。
- 使用 Node 静态断言确认流程列表列名为“最终测算利率(%)”,并且不再展示旧列名“测算利率(%)”。
- `mvn -pl ruoyi-admin -am clean package -DskipTests` 打包通过,并重启本地 63310 后端。
- 使用真实后端接口 `/loanPricing/workflow/list?pageNum=1&pageSize=3` 验证返回值,前三条 `calculateRate` 分别为 `3.732``6.05``3.732`
- 使用 browser-use 打开真实流程列表页 `http://localhost:1024/index`,确认列名为“最终测算利率(%)”,前三条页面展示值为 `3.73``6.05``3.73`,旧列名未出现。

View File

@@ -0,0 +1,28 @@
# 流程列表列宽连续展示实施记录
## 修改时间
- 2026-05-18
## 修改范围
- `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
## 修改内容
- 将流程列表表格字段改为最小列宽布局,宽屏下自动铺满容器,窄屏下通过表格横向滚动查看。
- 加宽“业务方流水号”“客户内码”“客户名称”“创建者”等关键字段最小列宽,避免当前业务数据被省略或换行截断。
- 将关键字段列的 `show-overflow-tooltip` 省略展示方式改为最小列宽完整展示,并覆盖 Element UI 默认省略号样式。
- 将“操作”列固定在右侧,并通过最小列宽布局让主体列自动铺满,避免固定列与主体列之间出现中间空白断开。
## 影响说明
- 本次仅调整流程列表前端表格展示,不修改接口、后端查询逻辑、数据库结构和权限逻辑。
- 字段内容较长时优先按最小列宽完整展示,页面可通过横向滚动查看完整列表。
## 验证
- 已执行 `source ~/.nvm/nvm.sh && nvm use 14.21.3 && npm run build:prod`,前端生产构建通过,仅存在既有资源体积 warning。
- 已复用本机前端 `http://localhost:1024/` 与后端 `http://localhost:63310` 进行真实页面验证。
- 已通过 browser-use 打开真实流程列表页 `http://localhost:1024/loanPricing/workflow` 验证页面标题为“贷款利率定价系统”,页面非空且无浏览器 error/warn 日志。
- 在真实页面验证流程列表:
-`1707x517` 视口下,表格主体 `scrollWidth=2020``clientWidth=1607`,确认窄屏仍可横向滚动。
-`2167x542` 视口下,表格主体 `scrollWidth=2067``clientWidth=2067`,确认宽屏下表格自动铺满容器。
- “操作”列固定在表格右侧,固定列宽度约 `112px`
- 固定“操作”列左侧间距 `gapBeforeFixed=0`,右侧尾部间距 `trailingGapAfterFixed=0`,未出现中间或尾部大块空白。
- “业务方流水号”“客户内码”“客户名称”“创建者”单元格样式为 `text-overflow: clip``overflow: hidden``white-space: nowrap`,当前列表关键字段 `BRANCH_SCOPE_20260518_MANAGER``测试支行管理员-8920100` 等完整展示且未使用省略号截断。

View File

@@ -0,0 +1,50 @@
# sys_user_role 角色关系 SQL 生成记录
## 修改日期
2026-05-20
## 需求范围
- 根据 `/Users/wkc/Downloads/892.xlsx``Sheet1` 生成 `sys_user_role` 插入语句。
- Excel 字段使用规则:`柜员号` 匹配 `sys_user.user_name` 后取 `sys_user.user_id``类型` 匹配 `sys_role.role_name` 后取 `sys_role.role_id`
- 未将 Excel 柜员号直接写入 `sys_user_role.user_id`
- 本次只生成 SQL 文件,未执行写入数据库。
## 输出文件
- `sql/insert_sys_user_role_892_20260519.sql`
## 数据统计
- Excel 有效数据行257 行。
- 管理员4 行。
- 支行管理员37 行。
- 客户经理213 行。
- Excel 中 3 个重复柜员号同时出现客户经理和支行管理员,本次按支行管理员优先处理,不插入客户经理角色。
## 重复柜员号
- `8922557`: 裘朝山 / 892220 / 客户经理 / Excel 第 26 行、裘朝山 / 892220 / 支行管理员 / Excel 第 117 行
- `8922667`: 徐华源 / 892080 / 客户经理 / Excel 第 66 行、徐华源 / 892080 / 支行管理员 / Excel 第 121 行
- `8923504`: 陈俊杰 / 892170 / 客户经理 / Excel 第 28 行、陈俊杰 / 892170 / 支行管理员 / Excel 第 118 行
- 处理规则:以上重复柜员号只保留在支行管理员插入语句中。
## 数据库映射确认
- 已查询开发库 `sys_role`,当前有效角色为:
- `管理员` -> `role_id=100`, `role_key=headAdmin`
- `客户经理` -> `role_id=101`, `role_key=common`
- `支行管理员` -> `role_id=102`, `role_key=branchAdmin`
- SQL 文件不硬编码 `role_id`,执行时按 `sys_role.role_name` 动态匹配,避免不同环境角色自增 ID 不一致。
- SQL 使用 `INSERT IGNORE`,重复执行不会重复写入相同 `(user_id, role_id)`
- SQL 不创建临时表,已按 `管理员``支行管理员``客户经理` 拆成三条独立插入语句,便于分角色执行和核对影响范围。
## 验证记录
- 已读取 Excel 并校验 `类型` 只包含 `管理员``支行管理员``客户经理`
- 已确认 `sys_user_role` 表结构为 `user_id``role_id` 联合主键。
- 已对开发库做不写入目标表的干跑校验257 行角色均可匹配,当前库 Excel 柜员号匹配到的有效用户数为 0。
- 批量执行该 SQL 前需先导入 Excel 对应用户,否则三条插入语句因匹配不到 `sys_user.user_name` 不会写入关系。
- 已按要求移除临时表建表、临时表插入和临时表删除语句。
- 已在事务内执行 SQL 脚本并 `ROLLBACK`,确认三条插入语句语法通过且未落库。

View File

@@ -0,0 +1,38 @@
# 对公余值覆盖字段实施记录
## 背景
对公新增流程需要在“贷款信息”分组补充“余值覆盖”开关,字段名为 `resCover`,提交到后端和模型调用时使用 `0/1` 值。
## 修改内容
1. 前端企业新增弹窗 `CorporateCreateDialog.vue`
- 在“贷款信息”分组新增“余值覆盖”开关。
- 表单默认值为 `false`
- 提交时转换为 `resCover: '1'``resCover: '0'`
2. 后端字段链路
- `CorporateLoanPricingCreateDTO` 增加 `resCover`
- `LoanPricingWorkflow` 增加 `resCover`,对应数据库字段 `res_cover`
- `LoanPricingConverter` 将 DTO 字段写入流程实体。
- `ModelInvokeDTO` 增加 `resCover`
- `LoanPricingModelService` 在企业模型调用前将 `resCover` 归一化为 `0/1`
3. 页面展示
- 企业流程详情页展示“余值覆盖”。
4. SQL
- 新增 `sql/add_res_cover_20260522.sql`
- 同步更新 `sql/loan_pricing_workflow.sql``sql/loan_pricing_schema_20260328.sql``sql/loan_pricing_prod_init_20260331.sql`
## 验证结果
- 已执行前端字段静态断言:`npm run test:corporate-create-input-params`,通过。
- 已执行前端详情字段静态断言:`npm run test:corporate-display-fields`,通过。
- 已执行后端单测:`mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`,通过。
- 已执行后端打包:`mvn -pl ruoyi-admin -am -DskipTests package`,通过。
- 已执行开发库 SQL`sql/add_res_cover_20260522.sql`,回查 `loan_pricing_workflow.res_cover``varchar(10)`
- 已启动真实前端页面,通过浏览器打开企业新增弹窗,确认“贷款信息”分组中展示“余值覆盖”开关,且位置在“企业标识”分组之前。
- 已用临时后端端口发起企业创建接口,响应中 `resCover``1`,后端外部模型调用日志确认请求参数包含 `"resCover":"1"`
- 接口验证产生的测试流程数据已清理,回查 `cust_isn = 'RES_COVER_TEST_20260522'` 的流程记录数为 `0`
- 测试时启动的前端和临时后端进程已关闭。

View File

@@ -0,0 +1,14 @@
# 用户管理新增弹窗宽度前端实施记录
## 修改内容
- 将用户管理“添加或修改用户配置对话框”的宽度从 `600px` 调整为页面宽度的 `80%`
- 修改范围仅限前端页面 `ruoyi-ui/src/views/system/user/index.vue`,不涉及后端接口、数据结构或权限逻辑。
## 验证记录
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && npm run build:prod`
- 结果:通过;仅保留项目既有的 webpack 体积提示。
- 真实页面验证:
- 前端地址:`http://localhost:1024/system/user`
- 登录用户:`admin`
- 点击用户管理页面“新增”后打开 `添加用户` 弹窗。
- 浏览器视口宽度为 `1067px`,弹窗实际宽度为 `853.33px`,宽度比例为 `0.7998`,符合页面宽度 `80%` 的要求。

View File

@@ -0,0 +1,26 @@
# 流程列表编辑功能后端实施记录
## 修改内容
- 在利率定价流程接口新增编辑查询接口:`GET /loanPricing/workflow/{serialNum}/edit`
- 新增个人流程编辑接口:`PUT /loanPricing/workflow/{serialNum}/personal`
- 新增企业流程编辑接口:`PUT /loanPricing/workflow/{serialNum}/corporate`
- 编辑接口按当前登录用户的 `昵称-柜员号` 校验创建者,只允许流程创建者编辑。
- 编辑时保持原业务方流水号、客户类型、创建者、创建时间和创建人部门,只覆盖表单字段。
- 编辑保存后重新调用模型服务;已有模型输出记录时覆盖原模型输出,并保持流程关联。
## 验证记录
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 结果:通过。
- 覆盖:创建者编辑、非创建者拒绝、客户类型不匹配拒绝、编辑数据解密返回、重新测算覆盖模型结果。
- `mvn -pl ruoyi-loan-pricing -am test`
- 结果:通过。
- 覆盖:利率定价模块现有单测和本次新增单测。
- `mvn -pl ruoyi-admin -am -DskipTests package`
- 结果:通过,重新打包 `ruoyi-admin/target/ruoyi-admin.jar` 用于真实接口验证。
- 真实接口验证:
- 创建临时个人流程 `20260525110739953`,创建者为 `若依-admin`
- 创建者调用 `GET /loanPricing/workflow/20260525110739953/edit` 成功返回原始客户名称 `编辑测试客户` 和原始证件号 `330103199901019999`
- 创建者通过页面编辑提交后,编辑详情接口返回 `applyAmt=120000`,并保持原 `serialNum``custType``createBy``createTime``deptId`
- 非创建者 `8929999` 调用编辑详情接口返回 `只有创建者可以编辑该流程`
- 非创建者 `8929999` 调用个人更新接口返回 `只有创建者可以编辑该流程`
- 验证完成后已按精确流水号删除临时流程和关联 `model_retail_output_fields` 记录,清理后计数均为 0。

View File

@@ -0,0 +1,24 @@
# 流程列表编辑功能前端实施记录
## 修改内容
- 在流程列表操作列新增“编辑”按钮。
- 编辑按钮只在 `row.createBy` 等于当前登录用户 `nickName-name` 时展示。
- 点击编辑后直接查询流程编辑数据,并按客户类型打开个人或企业弹窗,不再进入客户类型选择和客户号选择流程。
- 个人和企业新增弹窗复用为新增/编辑双模式:
- 新增模式继续调用原新增接口。
- 编辑模式显示编辑标题、回显原始数据,并调用对应更新接口。
- 编辑回显时跳过担保方式和抵质押类型监听中的清空逻辑,避免抵质押字段被初始化过程误清除。
## 验证记录
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && node tests/workflow-index-refresh.test.js && node tests/personal-create-input-params.test.js && node tests/corporate-create-input-params.test.js`
- 结果:通过。
- 覆盖:操作列编辑按钮、创建者展示控制、编辑数据查询、个人/企业弹窗编辑模式和更新接口调用。
- `source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && npm run build:prod`
- 结果:通过;仅保留项目既有的 webpack 体积提示。
- 真实页面验证:
- 前端地址:`http://localhost:1024/loanPricing/workflow`
- 创建者 `admin` 登录后,临时流水 `20260525110739953` 操作列显示“查看”和“编辑”。
- 点击“编辑”直接打开 `编辑个人利率定价流程` 弹窗,回显客户内码 `EDITTEST20260525`、客户名称 `编辑测试客户`、证件号 `330103199901019999`、担保方式 `信用`、申请金额 `100000`、借款期限 `3`、业务种类 `新增`
- 将申请金额改为 `120000` 后提交,页面提示“编辑成功”,列表刷新后该流水申请金额变为 `120000`
- 非创建者 `8929999` 登录后,同一流水仍可查看,但操作列只显示“查看”,不显示“编辑”。
- 浏览器控制台无相关 error仅出现登录和表单过程中的 `async-validator` 校验 warning。

View File

@@ -0,0 +1,28 @@
# 取消贷款定价页面脱敏展示后端实施记录
## 需求范围
- 取消贷款定价流程页面展示层的客户名称、证件号码脱敏效果。
- 列表、详情、模型输出基本信息返回完整展示值。
- 保留流程表客户名称、证件号码存储加密,以及创建、编辑、模型调用链路中的加解密逻辑。
## 实施内容
- 调整 `LoanPricingWorkflowServiceImpl` 分页列表返回逻辑,客户名称解密后直接返回。
- 调整 `LoanPricingWorkflowServiceImpl` 详情返回逻辑,流程主信息中的客户名称、证件号码解密后直接返回。
- 取消个人、企业模型输出基本信息中的客户名称、证件号码返回前脱敏处理。
- 删除不再使用的贷款定价专用展示脱敏服务及对应单元测试。
- 更新工作流服务单元测试,验证列表、详情、个人模型输出、企业模型输出均返回完整展示值。
## 未改动内容
- 未修改数据库表结构和数据迁移脚本。
- 未修改 `SensitiveFieldCryptoService` 存储加解密逻辑。
- 未修改登录、密码传输加密逻辑。
- 未修改通用 `@Sensitive` 脱敏机制。
- 未修改前端业务代码。
## 验证计划
- 执行 `LoanPricingWorkflowServiceImplTest`,确认返回展示值与原加解密边界符合预期。
- 启动真实前后端页面,进入贷款定价流程列表与详情页,确认页面展示完整客户名称和证件号码。

Binary file not shown.

View File

@@ -0,0 +1,647 @@
# Shangyu Pricing Field Adjustment Backend Implementation Plan
> **For agentic workers:** Follow this repository's `AGENTS.md`: do not invoke `using-superpowers` or subagents during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Update the backend create-flow contract so `businessType`, `couponRate`, personal removed fields, and customer-type-specific collateral options match the approved Shangyu pricing spec.
**Architecture:** Keep the existing workflow creation API and entity flow. Add `couponRate` to the DTO/entity/model path, tighten service-layer validation in `LoanPricingWorkflowServiceImpl`, remove personal create dependencies on `loanPurpose` and `bizProof`, and update SQL schema files in place.
**Tech Stack:** Spring Boot, RuoYi, MyBatis Plus, Lombok, Jakarta Validation, JUnit 5, Mockito, MySQL SQL files.
---
## File Structure
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java`
- Change `businessType` values to `新增/存量新增/存量转贷`.
- Remove personal create DTO dependency on `loanPurpose` and `bizProof`.
- Add `couponRate`.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java`
- Change `businessType` values to `新增/存量新增/存量转贷`.
- Expand `collType` values for corporate mortgage and pledge paths.
- Add `couponRate`.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
- Update `businessType` comment and add persisted `couponRate`.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java`
- Add `couponRate` only. Do not add `businessType`; the user excluded that model-input branch from this scope.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
- Map `couponRate`.
- Stop mapping removed personal fields from the personal create DTO.
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- Update business type validation.
- Add collateral-option validation by `custType + guarType`.
- Add required `couponRate` validation for `质押 + 存单质押`.
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- Add validation coverage and update existing create tests to set valid `businessType`.
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- Remove old personal `loanPurpose` DTO expectations so Maven test compilation remains valid after DTO removal.
- Keep `loanTerm/loanLoop` model assertions.
- Create Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java`
- Confirm personal removed fields no longer drive conversion and `couponRate` maps.
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
- Confirm `couponRate` is copied into `ModelInvokeDTO` via the model invocation path.
- Create: `sql/add_coupon_rate_20260511.sql`
- Migration for existing databases.
- Modify: `sql/loan_pricing_workflow.sql`
- Add `coupon_rate`.
- Modify: `sql/loan_pricing_schema_20260328.sql`
- Add `coupon_rate` to `loan_pricing_workflow`.
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
- Add `coupon_rate` to production init schema.
## Task 1: Add Failing Backend Contract Tests
**Files:**
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- Create Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
- [ ] **Step 1: Update existing create tests with valid business type**
In `LoanPricingWorkflowServiceImplTest`, every test that calls `createLoanPricing` must set:
```java
workflow.setBusinessType("新增");
```
For tests using mortgage or pledge, also set an allowed `collType` for the matching `custType`.
- [ ] **Step 2: Update old personal parameter tests**
In `LoanPricingModelServicePersonalParamsTest`, replace the old DTO field test:
Add the static import if it is not present:
```java
import static org.junit.jupiter.api.Assertions.assertNull;
```
```java
@Test
void shouldRemoveLoanPurposeAndKeepLoanTermInPersonalCreateDto() throws NoSuchFieldException {
assertThrows(NoSuchFieldException.class,
() -> PersonalLoanPricingCreateDTO.class.getDeclaredField("loanPurpose"));
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanTerm"));
}
```
Replace the old converter test that called `dto.setLoanPurpose(...)`:
```java
@Test
void shouldMapLoanTermWithoutLoanPurposeFromPersonalDto() {
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
dto.setCustName("张三");
dto.setGuarType("信用");
dto.setApplyAmt("100000");
dto.setBusinessType("新增");
dto.setLoanTerm("3");
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
assertNull(workflow.getLoanPurpose());
assertNull(workflow.getBizProof());
assertEquals("3", workflow.getLoanTerm());
}
```
In `shouldInvokePersonalModelWithExpectedParams`, remove:
```java
workflow.setLoanPurpose("business");
workflow.setBizProof("true");
```
and remove these argument matcher clauses:
```java
&& Objects.equals("business", dto.getLoanPurpose())
&& Objects.equals("1", dto.getBizProof())
```
Keep the existing `loanTerm`, `loanLoop`, `collThirdParty`, and `collType` assertions.
- [ ] **Step 3: Add failing business type and collateral validation tests**
Add tests like:
```java
@Test
void shouldRejectOldBusinessType() {
LoanPricingWorkflow workflow = validPersonalWorkflow();
workflow.setBusinessType("新客");
ServiceException ex = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("业务种类必须是:新增、存量新增、存量转贷之一", ex.getMessage());
}
@Test
void shouldRequireHistoryRateForStockTransfer() {
LoanPricingWorkflow workflow = validCorporateWorkflow();
workflow.setBusinessType("存量转贷");
workflow.setLoanRateHistory(null);
ServiceException ex = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("请选择历史贷款合同", ex.getMessage());
}
@Test
void shouldRequireCouponRateForCertificatePledge() {
LoanPricingWorkflow workflow = validPersonalWorkflow();
workflow.setGuarType("质押");
workflow.setCollType("存单质押");
workflow.setCouponRate(null);
ServiceException ex = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("存单票面利率不能为空", ex.getMessage());
}
```
Add helper methods:
```java
private LoanPricingWorkflow validPersonalWorkflow() {
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setCustType("个人");
workflow.setCustIsn("P001");
workflow.setCustName("张三");
workflow.setIdNum("330102199001011234");
workflow.setGuarType("信用");
workflow.setApplyAmt("100000");
workflow.setBusinessType("新增");
return workflow;
}
private LoanPricingWorkflow validCorporateWorkflow() {
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setCustType("企业");
workflow.setCustIsn("C001");
workflow.setCustName("测试企业");
workflow.setIdNum("91330100MA0000000X");
workflow.setGuarType("信用");
workflow.setApplyAmt("1000000");
workflow.setBusinessType("新增");
return workflow;
}
```
- [ ] **Step 4: Add failing allowed-option tests**
Add focused tests:
```java
@Test
void shouldAllowPersonalMortgageLineType() {
LoanPricingWorkflow workflow = validPersonalWorkflow();
workflow.setGuarType("抵押");
workflow.setCollType("一线");
when(sensitiveFieldCryptoService.encrypt(any())).thenAnswer(invocation -> invocation.getArgument(0));
loanPricingWorkflowService.createLoanPricing(workflow);
verify(loanPricingWorkflowMapper).insert(any());
}
@Test
void shouldRejectCorporateMortgageTypeForPersonalMortgage() {
LoanPricingWorkflow workflow = validPersonalWorkflow();
workflow.setGuarType("抵押");
workflow.setCollType("排污权抵押");
ServiceException ex = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("个人抵押抵质押类型必须是:一线、一类、二类、三类之一", ex.getMessage());
}
@Test
void shouldAllowCorporatePledgeEquityType() {
LoanPricingWorkflow workflow = validCorporateWorkflow();
workflow.setGuarType("质押");
workflow.setCollType("股权质押");
when(sensitiveFieldCryptoService.encrypt(any())).thenAnswer(invocation -> invocation.getArgument(0));
loanPricingWorkflowService.createLoanPricing(workflow);
verify(loanPricingWorkflowMapper).insert(any());
}
```
- [ ] **Step 5: Add failing converter test**
Create `LoanPricingConverterTest`:
```java
package com.ruoyi.loanpricing.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import org.junit.jupiter.api.Test;
class LoanPricingConverterTest {
@Test
void shouldMapCouponRateForPersonalWorkflow() {
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("P001");
dto.setGuarType("质押");
dto.setApplyAmt("100000");
dto.setBusinessType("新增");
dto.setCouponRate("2.15");
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
assertEquals("2.15", workflow.getCouponRate());
assertNull(workflow.getLoanPurpose());
assertNull(workflow.getBizProof());
}
@Test
void shouldMapCouponRateForCorporateWorkflow() {
CorporateLoanPricingCreateDTO dto = new CorporateLoanPricingCreateDTO();
dto.setCustIsn("C001");
dto.setGuarType("质押");
dto.setApplyAmt("1000000");
dto.setBusinessType("新增");
dto.setCouponRate("2.35");
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
assertEquals("2.35", workflow.getCouponRate());
}
}
```
- [ ] **Step 6: Add failing model DTO test**
In `LoanPricingModelServiceTest`, add a test or extend the existing argument-captor test to assert:
```java
assertEquals("2.15", capturedModelInvokeDto.getCouponRate());
```
The workflow used by that test must set:
```java
loanPricingWorkflow.setCouponRate("2.15");
loanPricingWorkflow.setBusinessType("新增");
```
- [ ] **Step 7: Run backend tests and confirm failure**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: FAIL because `couponRate` and new validation are not implemented yet.
## Task 2: Implement DTO, Entity, Converter, and Model DTO Fields
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.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/dto/ModelInvokeDTO.java`
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
- [ ] **Step 1: Update personal create DTO**
In `PersonalLoanPricingCreateDTO`:
- Remove the `loanPurpose` field and its validation annotations.
- Remove the `bizProof` field.
- Change `businessType` annotations to:
```java
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "新增", allowableValues = {"新增", "存量新增", "存量转贷"})
@NotBlank(message = "业务种类不能为空")
@Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一")
private String businessType;
```
- Add:
```java
@Schema(description = "存单票面利率", example = "2.15")
private String couponRate;
```
- [ ] **Step 2: Update corporate create DTO**
In `CorporateLoanPricingCreateDTO`:
```java
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "新增", allowableValues = {"新增", "存量新增", "存量转贷"})
@NotBlank(message = "业务种类不能为空")
@Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一")
private String businessType;
```
Change `collType` annotation to include the combined corporate option set:
```java
@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押", "存单质押", "股权质押", "其他质押"})
@Pattern(regexp = "^(一类|二类|三类|四类|排污权抵押|设备等其他不动产抵押|存单质押|股权质押|其他质押)$", message = "抵质押类型不符合当前客户类型和担保方式")
private String collType;
```
Add:
```java
@Schema(description = "存单票面利率", example = "2.35")
private String couponRate;
```
- [ ] **Step 3: Update workflow entity**
In `LoanPricingWorkflow`:
```java
/** 业务种类: 新增/存量新增/存量转贷 */
private String businessType;
/** 存单票面利率 */
private String couponRate;
```
Keep `loanPurpose` and `bizProof` on the entity for historical rows and existing schema compatibility; they are no longer populated by personal create DTO conversion.
- [ ] **Step 4: Update model DTO**
In `ModelInvokeDTO`, add:
```java
/**
* 存单票面利率
*/
private String couponRate;
```
Do not add `businessType` in this task.
- [ ] **Step 5: Update converter**
In personal conversion, remove:
```java
entity.setLoanPurpose(dto.getLoanPurpose());
entity.setBizProof(dto.getBizProof());
```
Add for both personal and corporate conversion:
```java
entity.setCouponRate(dto.getCouponRate());
```
- [ ] **Step 6: Run targeted converter and model tests**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: personal parameter, converter, and model field tests PASS; service validation tests may still fail until Task 3.
- [ ] **Step 7: Commit field and converter changes**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java \
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java
git commit -m "调整上虞利率定价后端字段"
```
## Task 3: Implement Service-Layer Validation
**Files:**
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
- [ ] **Step 1: Replace business type validation**
Update `validateBusinessTypeAndHistoryRate`:
```java
private void validateBusinessTypeAndHistoryRate(LoanPricingWorkflow workflow) {
if (!StringUtils.hasText(workflow.getBusinessType())) {
throw new ServiceException("业务种类不能为空");
}
if (!isOneOf(workflow.getBusinessType(), "新增", "存量新增", "存量转贷")) {
throw new ServiceException("业务种类必须是:新增、存量新增、存量转贷之一");
}
if ("存量转贷".equals(workflow.getBusinessType())
&& !StringUtils.hasText(workflow.getLoanRateHistory())) {
throw new ServiceException("请选择历史贷款合同");
}
}
```
- [ ] **Step 2: Add collateral and coupon validators**
Add calls before insert:
```java
validateBusinessTypeAndHistoryRate(loanPricingWorkflow);
validateCollateralType(loanPricingWorkflow);
validateCouponRate(loanPricingWorkflow);
```
Add helper methods:
```java
private void validateCouponRate(LoanPricingWorkflow workflow) {
if ("质押".equals(workflow.getGuarType())
&& "存单质押".equals(workflow.getCollType())
&& !StringUtils.hasText(workflow.getCouponRate())) {
throw new ServiceException("存单票面利率不能为空");
}
}
private void validateCollateralType(LoanPricingWorkflow workflow) {
if (!"抵押".equals(workflow.getGuarType()) && !"质押".equals(workflow.getGuarType())) {
return;
}
if (!StringUtils.hasText(workflow.getCollType())) {
throw new ServiceException("请选择抵质押类型");
}
if ("个人".equals(workflow.getCustType()) && "抵押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "一线", "一类", "二类", "三类")) {
throw new ServiceException("个人抵押抵质押类型必须是:一线、一类、二类、三类之一");
}
if ("个人".equals(workflow.getCustType()) && "质押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "存单质押", "其他质押")) {
throw new ServiceException("个人质押抵质押类型必须是:存单质押、其他质押之一");
}
if ("企业".equals(workflow.getCustType()) && "抵押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押")) {
throw new ServiceException("企业抵押抵质押类型必须是:一类、二类、三类、四类、排污权抵押、设备等其他不动产抵押之一");
}
if ("企业".equals(workflow.getCustType()) && "质押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "存单质押", "股权质押", "其他质押")) {
throw new ServiceException("企业质押抵质押类型必须是:存单质押、股权质押、其他质押之一");
}
}
private boolean isOneOf(String value, String... allowedValues) {
for (String allowedValue : allowedValues) {
if (allowedValue.equals(value)) {
return true;
}
}
return false;
}
```
- [ ] **Step 3: Run service tests**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: PASS.
- [ ] **Step 4: Run backend targeted suite**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: PASS.
- [ ] **Step 5: Commit validation changes**
```bash
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java \
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java
git commit -m "增加上虞利率定价创建校验"
```
## Task 4: Update SQL Schema Files
**Files:**
- Create: `sql/add_coupon_rate_20260511.sql`
- Modify: `sql/loan_pricing_workflow.sql`
- Modify: `sql/loan_pricing_schema_20260328.sql`
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
- [ ] **Step 1: Create migration SQL**
Create `sql/add_coupon_rate_20260511.sql`:
```sql
-- 上虞利率定价存单票面利率字段
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率' AFTER `loan_rate_history`;
```
- [ ] **Step 2: Update standalone workflow schema**
In `sql/loan_pricing_workflow.sql`, add after `loan_rate_history`:
```sql
`coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率',
```
- [ ] **Step 3: Update bundled schema files**
Apply the same column to the `loan_pricing_workflow` table definitions in:
- `sql/loan_pricing_schema_20260328.sql`
- `sql/loan_pricing_prod_init_20260331.sql`
- [ ] **Step 4: Verify SQL references**
Run:
```bash
rg -n "coupon_rate|存单票面利率" sql
```
Expected: `coupon_rate` appears in the migration and all three schema/init files.
- [ ] **Step 5: Commit SQL changes**
```bash
git add sql/add_coupon_rate_20260511.sql \
sql/loan_pricing_workflow.sql \
sql/loan_pricing_schema_20260328.sql \
sql/loan_pricing_prod_init_20260331.sql
git commit -m "新增存单票面利率数据库字段"
```
## Task 5: Backend Verification and Record
**Files:**
- Create or Modify: `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md`
- [ ] **Step 1: Run backend verification**
Run:
```bash
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: PASS.
- [ ] **Step 2: Check staged-independent status**
Run:
```bash
git status --short
```
Expected: only intended backend and SQL files are modified or staged for this implementation slice. Existing unrelated files such as `AGENTS.md`, `.DS_Store`, and operation-manual docs must stay out of these commits.
- [ ] **Step 3: Update implementation record**
Add backend notes to `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md`:
```markdown
## 后端实现
- 调整个人/企业创建 DTO 的业务种类口径为 `新增/存量新增/存量转贷`
- 新增 `couponRate` 存单票面利率字段,并贯通流程保存和模型入参。
- 取消对私创建链路对 `loanPurpose``bizProof` 的依赖。
- 增加服务层校验,覆盖业务种类、历史贷款利率、抵质押类型和存单票面利率。
- 新增 `coupon_rate` 数据库字段及 schema/init SQL。
## 后端验证
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingConverterTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
```
- [ ] **Step 4: Commit backend record**
```bash
git add doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md
git commit -m "记录上虞字段调整后端实现"
```

View File

@@ -0,0 +1,485 @@
# Shangyu Pricing Field Adjustment Frontend Implementation Plan
> **For agentic workers:** Follow this repository's `AGENTS.md`: do not invoke `using-superpowers` or subagents during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Update the personal and corporate workflow create dialogs so the displayed fields, dynamic options, required `couponRate`, and submitted payload match the approved Shangyu pricing spec.
**Architecture:** Keep the existing Vue 2 create-dialog components and static Node assertion tests. Update each dialog in place, using computed properties for customer-type-specific collateral options and the `质押 + 存单质押` coupon-rate condition; verify with static tests, production build, and a real Playwright browser check.
**Tech Stack:** Vue 2, Element UI, RuoYi request wrapper, Node static tests, npm scripts, nvm-controlled frontend runtime, Playwright real browser verification.
---
## File Structure
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- Change `businessType` options.
- Remove `loanPurpose` and `bizProof` UI, form fields, rules, reset values, and submit payload handling.
- Change personal `collateralTypeOptions`.
- Add `couponRate` conditional field and submit cleanup.
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Change `businessType` options.
- Change corporate `collateralTypeOptions`.
- Add `couponRate` conditional field and submit cleanup.
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- Update business-type assertions from `新客` to `新增`.
- Add `couponRate` assertions.
- Modify: `ruoyi-ui/tests/personal-create-input-params.test.js`
- Assert personal removed fields are absent.
- Assert personal collateral options match the new spec.
- Modify: `ruoyi-ui/tests/corporate-create-input-params.test.js`
- Assert corporate collateral options match the new spec.
- Modify: `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md`
- Add frontend implementation and Playwright verification notes after execution.
## Task 1: Update Frontend Static Assertions First
**Files:**
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- Modify: `ruoyi-ui/tests/personal-create-input-params.test.js`
- Modify: `ruoyi-ui/tests/corporate-create-input-params.test.js`
- [ ] **Step 1: Update business-type assertions**
In `business-type-history-rate.test.js`, replace old assertions for `新客` with:
```js
;[
['个人新增弹窗', personalCreate],
['企业新增弹窗', corporateCreate]
].forEach(([name, source]) => {
assert(source.includes('label="业务种类"'), `${name} 缺少业务种类`)
assert(source.includes('value="新增"'), `${name} 缺少新增选项`)
assert(source.includes('value="存量新增"'), `${name} 缺少存量新增选项`)
assert(source.includes('value="存量转贷"'), `${name} 缺少存量转贷选项`)
assert(!source.includes('value="新客"'), `${name} 仍保留旧业务种类 新客`)
assert(source.includes("if (value === '存量转贷')"), `${name} 未保持仅存量转贷查询历史合同`)
})
```
- [ ] **Step 2: Add coupon-rate assertions**
In the same test file, add:
```js
;[
['个人新增弹窗', personalCreate],
['企业新增弹窗', corporateCreate]
].forEach(([name, source]) => {
assert(source.includes('label="存单票面利率"'), `${name} 缺少存单票面利率字段`)
assert(source.includes('prop="couponRate"'), `${name} 缺少 couponRate prop`)
assert(source.includes('isCertificatePledge'), `${name} 缺少存单质押条件计算`)
assert(source.includes("this.form.guarType === '质押'") && source.includes("this.form.collType === '存单质押'"), `${name} couponRate 条件不正确`)
assert(source.includes('delete data.couponRate'), `${name} 未在非适用条件删除 couponRate`)
assert(source.includes('存单票面利率不能为空'), `${name} 缺少 couponRate 必填提示`)
})
```
This shared `business-type-history-rate.test.js` is expected to remain failing until both personal and corporate dialogs are updated. Do not use it as a passing checkpoint after only the personal dialog is changed.
- [ ] **Step 3: Update personal-field assertions**
In `personal-create-input-params.test.js`, replace old required assertions for `loanPurpose` and `bizProof` with absence checks:
```js
assert(
!personalCreateDialog.includes('label="贷款用途"') &&
!personalCreateDialog.includes('prop="loanPurpose"') &&
!personalCreateDialog.includes('loanPurpose:'),
'个人新增弹窗不应继续保留贷款用途字段'
)
assert(
!personalCreateDialog.includes('label="是否有经营佐证"') &&
!personalCreateDialog.includes('prop="bizProof"') &&
!personalCreateDialog.includes('bizProof:'),
'个人新增弹窗不应继续保留是否有经营佐证字段'
)
```
Update personal collateral assertion:
```js
assert(
personalCreateDialog.includes("return ['一线', '一类', '二类', '三类']") &&
personalCreateDialog.includes("return ['存单质押', '其他质押']"),
'个人新增弹窗抵质押类型选项未按新口径动态切换'
)
```
- [ ] **Step 4: Update corporate collateral assertions**
In `corporate-create-input-params.test.js`, replace old collateral assertion with:
```js
assert(
corporateCreateDialog.includes("return ['一类', '二类', '三类', '四类', '排污权抵押', '设备等其他不动产抵押']") &&
corporateCreateDialog.includes("return ['存单质押', '股权质押', '其他质押']"),
'企业新增弹窗抵质押类型选项未按新口径动态切换'
)
```
- [ ] **Step 5: Run frontend static tests and confirm failure**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params'
```
Expected: FAIL because the UI is not updated yet.
## Task 2: Update Personal Create Dialog
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
- [ ] **Step 1: Change business type options**
Replace the first option:
```vue
<el-option label="新增" value="新增"/>
<el-option label="存量新增" value="存量新增"/>
<el-option label="存量转贷" value="存量转贷"/>
```
- [ ] **Step 2: Remove personal fields from template**
Delete the `贷款用途` form item and its surrounding column. Delete the `是否有经营佐证` form item. Keep `借款期限(年)` and `循环功能`.
If a row becomes single-column, leave it as a normal `el-row` with one `el-col :span="12"`; do not restructure the whole dialog.
- [ ] **Step 3: Remove personal fields from data, rules, and submit payload**
Remove these from `form` initial state and `reset()`:
```js
loanPurpose: undefined,
bizProof: false,
```
Remove `loanPurpose` from `rules`.
Remove this submit conversion:
```js
bizProof: this.form.bizProof ? '1' : '0',
```
Keep:
```js
loanLoop: this.form.loanLoop ? '1' : '0'
```
- [ ] **Step 4: Update personal collateral options**
Change `collateralTypeOptions()` to:
```js
collateralTypeOptions() {
if (this.form.guarType === '抵押') {
return ['一线', '一类', '二类', '三类']
}
if (this.form.guarType === '质押') {
return ['存单质押', '其他质押']
}
return []
}
```
- [ ] **Step 5: Add coupon-rate field**
Add under the collateral row:
```vue
<el-col :span="12" v-if="isCertificatePledge">
<el-form-item label="存单票面利率" prop="couponRate">
<el-input v-model="form.couponRate" placeholder="请输入存单票面利率"/>
</el-form-item>
</el-col>
```
If the row already has two columns, put this field in the same `抵质押信息` area and keep the existing `900px` dialog.
- [ ] **Step 6: Add computed condition and validator**
Add:
```js
isCertificatePledge() {
return this.form.guarType === '质押' && this.form.collType === '存单质押'
}
```
Add a validator in `data()`:
```js
const validateCouponRate = (rule, value, callback) => {
if (this.isCertificatePledge && !value) {
callback(new Error('存单票面利率不能为空'))
return
}
callback()
}
```
Add rule:
```js
couponRate: [
{validator: validateCouponRate, trigger: "blur"}
]
```
- [ ] **Step 7: Add coupon-rate state cleanup**
Add `couponRate: undefined` to `form` initial state and `reset()`.
Add watcher:
```js
'form.collType'() {
this.resetCouponRateIfNotCertificatePledge()
}
```
Add method:
```js
resetCouponRateIfNotCertificatePledge() {
if (!this.isCertificatePledge) {
this.form.couponRate = undefined
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate(['couponRate'])
}
})
}
}
```
Call it at the end of `resetCollateralFields()`.
- [ ] **Step 8: Add submit cleanup and front-end guard**
In `submitForm`, before `this.submitting = true`, add:
```js
if (this.isCertificatePledge && !this.form.couponRate) {
this.$modal.msgWarning("存单票面利率不能为空")
return
}
```
After collateral handling:
```js
if (!this.isCertificatePledge) {
delete data.couponRate
}
```
- [ ] **Step 9: Run personal static tests**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:personal-create-input-params'
```
Expected: PASS. The shared `test:business-type-history-rate` is intentionally not run here because it also asserts corporate changes that are implemented in Task 3.
- [ ] **Step 10: Commit personal frontend changes**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue \
ruoyi-ui/tests/personal-create-input-params.test.js
git commit -m "调整上虞对私新增字段口径"
```
## Task 3: Update Corporate Create Dialog
**Files:**
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
- Modify: `ruoyi-ui/tests/corporate-create-input-params.test.js`
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
- [ ] **Step 1: Change business type options**
Use:
```vue
<el-option label="新增" value="新增"/>
<el-option label="存量新增" value="存量新增"/>
<el-option label="存量转贷" value="存量转贷"/>
```
- [ ] **Step 2: Update corporate collateral options**
Change `collateralTypeOptions()` to:
```js
collateralTypeOptions() {
if (this.form.guarType === '抵押') {
return ['一类', '二类', '三类', '四类', '排污权抵押', '设备等其他不动产抵押']
}
if (this.form.guarType === '质押') {
return ['存单质押', '股权质押', '其他质押']
}
return []
}
```
- [ ] **Step 3: Add coupon-rate field and state**
Mirror the personal dialog implementation:
```vue
<el-col :span="12" v-if="isCertificatePledge">
<el-form-item label="存单票面利率" prop="couponRate">
<el-input v-model="form.couponRate" placeholder="请输入存单票面利率"/>
</el-form-item>
</el-col>
```
Add:
```js
couponRate: undefined
```
to initial `form` and `reset()`.
- [ ] **Step 4: Add computed condition, validator, watcher, and submit cleanup**
Use the same names as personal dialog:
```js
isCertificatePledge() {
return this.form.guarType === '质押' && this.form.collType === '存单质押'
}
```
Use the same `validateCouponRate`, `couponRate` rule, `form.collType` watcher, `resetCouponRateIfNotCertificatePledge`, front-end guard, and:
```js
if (!this.isCertificatePledge) {
delete data.couponRate
}
```
- [ ] **Step 5: Run corporate static tests**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:corporate-create-input-params && npm --prefix ruoyi-ui run test:business-type-history-rate'
```
Expected: PASS.
- [ ] **Step 6: Commit corporate frontend changes**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue \
ruoyi-ui/tests/corporate-create-input-params.test.js \
ruoyi-ui/tests/business-type-history-rate.test.js
git commit -m "调整上虞对公新增字段口径"
```
## Task 4: Frontend Build and Real Page Verification
**Files:**
- Modify: `doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md`
- [ ] **Step 1: Run all related static tests**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params'
```
Expected: PASS.
- [ ] **Step 2: Run production build**
Run:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'
```
Expected: PASS and `ruoyi-ui/dist` generated.
- [ ] **Step 3: Start the local app stack**
Use the repository's existing startup scripts if available. If a frontend dev server is needed, run it with Node controlled by `nvm`:
```bash
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run dev'
```
Record the PID and URL. If the backend also needs restart, use the existing repository backend restart script rather than inventing a new start path.
- [ ] **Step 4: Use Playwright on the real page**
Open the actual local workflow page, not a prototype. Verify:
- Personal create dialog:
- `业务种类` shows `新增/存量新增/存量转贷`.
- `新客` is absent.
- `贷款用途` is absent.
- `是否有经营佐证` is absent.
- `抵押` shows `一线/一类/二类/三类`.
- `质押 + 存单质押` shows `存单票面利率`.
- Leaving `存单票面利率` empty blocks submit.
- Corporate create dialog:
- `业务种类` shows `新增/存量新增/存量转贷`.
- `抵押` shows `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`.
- `质押` shows `存单质押/股权质押/其他质押`.
- `质押 + 存单质押` shows `存单票面利率`.
- Leaving `存单票面利率` empty blocks submit.
- History-rate behavior:
- `存量转贷` triggers historical-contract query.
- `新增` and `存量新增` do not trigger historical-contract query.
- [ ] **Step 5: Stop test processes**
Stop any frontend/backend processes started in this task. Verify with:
```bash
ps -ef | rg 'vue-cli-service|ruoyi-admin|RuoYiApplication'
```
Expected: no leftover processes from this test run.
- [ ] **Step 6: Update implementation record**
Append:
```markdown
## 前端实现
- 调整个人/企业新增弹窗业务种类选项为 `新增/存量新增/存量转贷`
- 个人新增弹窗剔除 `loanPurpose``bizProof`
- 按客户类型和担保方式调整抵质押类型选项。
- 新增 `质押 + 存单质押` 下的 `couponRate` 存单票面利率字段、必填校验和提交清理。
## 前端验证
- `npm --prefix ruoyi-ui run test:business-type-history-rate`
- `npm --prefix ruoyi-ui run test:personal-create-input-params`
- `npm --prefix ruoyi-ui run test:corporate-create-input-params`
- `npm --prefix ruoyi-ui run build:prod`
- Playwright 真实页面验证:通过
```
- [ ] **Step 7: Commit frontend verification record**
```bash
git add doc/implementation-report-2026-05-11-shangyu-pricing-field-adjustment.md
git commit -m "记录上虞字段调整前端验证"
```

View File

@@ -0,0 +1,231 @@
# 上虞利率定价字段口径调整设计
## 背景
当前上虞利率定价新增流程已经具备个人、企业两个新增弹窗,并已有业务种类、历史贷款利率和历史贷款合同选择链路。本次需求是在现有链路上调整字段口径:
- 前端通用业务种类改为 `新增/存量新增/存量转贷`,仅 `存量转贷` 查询历史贷款合同。
- `质押 + 存单质押` 时新增 `couponRate` 存单票面利率,客户经理填写后上传。
- 上虞对私剔除 `loanPurpose` 贷款用途和 `bizProof` 是否有经营佐证。
- 上虞对私、对公按客户类型和担保方式分别调整 `collType` 可选值。
用户已确认本次按页面、创建接口、服务校验、流程保存、模型调用参数全链路同步调整。用户也确认 `couponRate``质押 + 存单质押` 时必须填写。对公 `businessType` 上传模型接口这一条用户已明确要求忽略,因此本设计不新增、不验证 `businessType` 模型入参链路;本次模型入参调整只覆盖 `couponRate`
## 范围
### 本次包含
- 调整个人和企业新增弹窗的 `businessType` 选项。
- 保持 `businessType=存量转贷` 才查询历史贷款合同。
- 新增 `couponRate` 条件展示、必填校验、提交和模型入参传递。
- 移除个人新增弹窗中的 `loanPurpose``bizProof`
- 后端取消个人创建 DTO 对 `loanPurpose``bizProof` 的必填依赖。
- 按个人/企业和抵押/质押分别校验 `collType` 可选值。
- 新增或更新数据库字段定义,使 `couponRate` 能保存到流程表。
- 更新直接相关的前端静态断言和后端单元测试。
### 本次不包含
- 不重做历史贷款合同查询接口。
- 不改历史贷款合同选择弹窗结构。
- 不新增字典配置或字段配置化能力。
- 不处理旧历史数据回填。
- 不新增或验证对公 `businessType` 上传模型逻辑;该条按用户确认忽略。
## 推荐方案
采用全链路最短路径同步方案。
页面负责字段展示、条件清空和提交前整理。后端负责创建 DTO 接收、统一业务校验、流程实体保存和模型入参传递。SQL 同步补充 `coupon_rate` 字段,保证新环境和已有环境都能落库。
不采用只改前端的方式,因为直接调用创建接口会绕过 `couponRate` 必填和 `collType` 口径校验。不采用配置化字段规则,因为本次口径明确,配置化会超出需求范围。
## 前端设计
### 通用业务种类
个人新增弹窗和企业新增弹窗的 `businessType` 选项统一为:
- `新增`
- `存量新增`
- `存量转贷`
交互规则:
- 选择 `存量转贷` 时,沿用现有客户内码查询历史贷款合同逻辑。
- 选择 `新增``存量新增` 时,清空历史合同选择和 `loanRateHistory`
-`存量转贷` 提交前不上传 `loanRateHistory`
### 存单票面利率
个人和企业新增弹窗都新增 `couponRate` 输入框,字段名称展示为 `存单票面利率`
展示和提交规则:
- 仅当 `guarType=质押``collType=存单质押` 时展示。
- 展示时必填。
- 切换担保方式或抵质押类型后,如果不再满足 `质押 + 存单质押`,清空 `couponRate`
- 不满足条件时提交前不上传 `couponRate`
### 上虞对私字段调整
个人新增弹窗移除:
- `贷款用途 loanPurpose`
- `是否有经营佐证 bizProof`
个人提交数据不再包含这两个字段。个人的 `loanLoop``collThirdParty` 等既有字段保持现有处理方式。
个人 `collType` 选项:
- `guarType=抵押``一线/一类/二类/三类`
- `guarType=质押``存单质押/其他质押`
### 上虞对公字段调整
企业 `collType` 选项:
- `guarType=抵押``一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`
- `guarType=质押``存单质押/股权质押/其他质押`
企业其他既有上传字段保持现状。
## 后端设计
### DTO 和实体
`PersonalLoanPricingCreateDTO`
- `businessType` 允许值改为 `新增/存量新增/存量转贷`
- 移除 `loanPurpose` 必填和枚举校验。
- 移除 `bizProof` 业务依赖。
- 新增 `couponRate`
`CorporateLoanPricingCreateDTO`
- `businessType` 允许值改为 `新增/存量新增/存量转贷`
- 新增 `couponRate`
- `collType` 允许值覆盖对公抵押和质押新口径。
`LoanPricingWorkflow`
- `businessType` 注释更新为 `新增/存量新增/存量转贷`
- 新增 `couponRate` 字段,对应数据库 `coupon_rate`
- 个人剔除字段不再作为本次创建链路必需字段。
`ModelInvokeDTO`
- 新增 `couponRate`,用于模型调用参数。
- 不新增或调整 `businessType` 模型入参;该条按用户确认不纳入本次范围。
`LoanPricingConverter`
- 个人和企业创建 DTO 都映射 `couponRate`
- 个人转换不再依赖 `loanPurpose``bizProof`
### 服务层校验
创建流程统一校验放在 `LoanPricingWorkflowServiceImpl`,确保绕过前端直接调用接口也会被拦截。
校验规则:
- `businessType` 必填,只允许 `新增/存量新增/存量转贷`
- `businessType=存量转贷` 时,`loanRateHistory` 必填。
- `guarType=质押``collType=存单质押` 时,`couponRate` 必填。
- `custType=个人``guarType=抵押` 时,`collType` 只允许 `一线/一类/二类/三类`
- `custType=个人``guarType=质押` 时,`collType` 只允许 `存单质押/其他质押`
- `custType=企业``guarType=抵押` 时,`collType` 只允许 `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`
- `custType=企业``guarType=质押` 时,`collType` 只允许 `存单质押/股权质押/其他质押`
当担保方式不是抵押或质押时,不要求 `collType``couponRate`
## 数据库设计
流程表新增字段:
```sql
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率' AFTER `loan_rate_history`;
```
需要同步更新:
- 当前迁移 SQL。
- `sql/loan_pricing_workflow.sql`
- `sql/loan_pricing_schema_20260328.sql`
- `sql/loan_pricing_prod_init_20260331.sql`
已有历史数据不回填,`coupon_rate` 允许为空。
## 数据流
1. 用户进入利率定价列表,选择新增。
2. 用户选择个人或企业客户,并通过客户号映射选择客户内码。
3. 打开对应新增弹窗。
4. 用户选择业务种类。
5. 若业务种类为 `存量转贷`,前端使用客户内码查询历史贷款合同并回填 `loanRateHistory`
6. 用户选择担保方式和抵质押类型。
7. 若选择 `质押 + 存单质押`,前端展示并要求填写 `couponRate`
8. 前端提交前清理不适用字段。
9. 后端创建接口接收 DTO。
10. 转换器映射为 `LoanPricingWorkflow`
11. 服务层做业务种类、历史利率、抵质押类型和 `couponRate` 校验。
12. 流程表保存。
13. 模型调用前复制流程字段到 `ModelInvokeDTO`,带出 `couponRate``businessType` 模型入参不在本次设计范围内。
## 错误处理
- 业务种类为空:提示 `请选择业务种类` 或后端返回 `业务种类不能为空`
- 业务种类为旧值或非法值:后端返回 `业务种类必须是:新增、存量新增、存量转贷之一`
- `存量转贷` 未选择历史贷款合同:提示或返回 `请选择历史贷款合同`
- `质押 + 存单质押` 未填写 `couponRate`:提示或返回 `存单票面利率不能为空`
- `collType` 不符合当前客户类型和担保方式:后端返回对应可选值错误。
## 测试设计
### 前端静态断言
更新或新增前端测试脚本,覆盖:
- 个人和企业新增弹窗都包含 `新增/存量新增/存量转贷`
- 个人和企业新增弹窗不再包含旧选项 `新客`
- 个人新增弹窗不再展示 `贷款用途``是否有经营佐证`
- 个人抵押选项为 `一线/一类/二类/三类`
- 个人质押选项包含 `存单质押/其他质押`
- 企业抵押选项为 `一类/二类/三类/四类/排污权抵押/设备等其他不动产抵押`
- 企业质押选项为 `存单质押/股权质押/其他质押`
- `couponRate` 仅在 `质押 + 存单质押` 条件下展示、必填、提交。
- 非适用条件提交前删除 `couponRate`
### 后端单元测试
更新 `LoanPricingWorkflowServiceImplTest` 或新增直接相关测试,覆盖:
- `businessType=新增` 通过。
- `businessType=新客` 拒绝。
- `businessType=存量转贷` 且缺少 `loanRateHistory` 拒绝。
- `guarType=质押``collType=存单质押` 且缺少 `couponRate` 拒绝。
- 个人抵押允许 `一线/一类/二类/三类`,拒绝对公专属抵押值。
- 企业抵押允许 `排污权抵押/设备等其他不动产抵押`
- 企业质押允许 `股权质押/其他质押`
### 实际页面验证
实现完成后按项目规则验证:
- 使用 `nvm` 控制前端 Node 版本。
- 启动前端页面并用 Playwright 打开真实页面,不使用 prototype。
- 验证个人新增弹窗字段移除、业务种类选项、抵押/质押选项和 `couponRate` 条件展示。
- 验证企业新增弹窗业务种类选项、抵押/质押选项和 `couponRate` 条件展示。
- 验证 `存量转贷` 仍会触发历史合同查询,`新增/存量新增` 不触发。
- 测试结束关闭本次启动的前后端进程。
## 验收标准
- 页面字段和选项与本设计一致。
- 创建接口不能接受旧业务种类 `新客`
- `质押 + 存单质押` 未填 `couponRate` 时前后端都不能提交成功。
- 不适用 `couponRate` 时不会上传该字段。
- `couponRate` 能保存到流程表并进入模型调用参数。
- 对私不再要求或上传 `loanPurpose``bizProof`
- 前端静态断言、后端单元测试和真实页面验证通过。

BIN
ruoyi-admin/.DS_Store vendored

Binary file not shown.

View File

@@ -82,6 +82,9 @@ model:
personal-url: http://localhost:63310/rate/pricing/mock/invokeModel/personal
corporate-url: http://localhost:63310/rate/pricing/mock/invokeModel/corporate
loan-pricing-external:
app-code: 1a89fa84abda480ba93ed73fd01ffd07
customer-map:
personal-url: http://localhost:63310/rate/pricing/mock/customer-map/personal
corporate-url: http://localhost:63310/rate/pricing/mock/customer-map/corporate

View File

@@ -82,12 +82,15 @@ model:
personal-url: http://64.202.32.40:8083/api/service/interface/invokeService/syllcs
corporate-url: http://64.202.32.40:8083/api/service/interface/invokeService/sydgllcs
loan-pricing-external:
app-code: 1a89fa84abda480ba93ed73fd01ffd07
customer-map:
personal-url: http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_lilvcesuan_ind_idmapno?appCode=1a89fa84abda480ba93ed73fd01ffd07&cust_id=
corporate-url: http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_lilvcesuan_ent_idmapno?appCode=1a89fa84abda480ba93ed73fd01ffd07&cust_id=
personal-url: http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_lilvcesuan_ind_idmapno
corporate-url: http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_lilvcesuan_ent_idmapno
loan-rate-history:
url: http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_loan_rate_history?appCode=1a89fa84abda480ba93ed73fd01ffd07
url: http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_loan_rate_history
security:
password-transfer:

View File

@@ -82,6 +82,9 @@ model:
personal-url: http://localhost:63310/rate/pricing/mock/invokeModel/personal
corporate-url: http://localhost:63310/rate/pricing/mock/invokeModel/corporate
loan-pricing-external:
app-code: 1a89fa84abda480ba93ed73fd01ffd07
customer-map:
personal-url: http://localhost:63310/rate/pricing/mock/customer-map/personal
corporate-url: http://localhost:63310/rate/pricing/mock/customer-map/corporate

View File

@@ -69,6 +69,46 @@ public class LoanPricingWorkflowController extends BaseController
return success(result);
}
/**
* 查询利率定价流程编辑数据
*/
@Operation(summary = "查询利率定价流程编辑数据")
@GetMapping("/{serialNum}/edit")
public AjaxResult getEditInfo(
@Parameter(description = "业务方流水号")
@PathVariable("serialNum") String serialNum)
{
return success(loanPricingWorkflowService.selectEditableLoanPricingBySerialNum(serialNum));
}
/**
* 编辑个人客户利率定价流程
*/
@Operation(summary = "编辑个人客户利率定价流程")
@Log(title = "个人客户利率定价流程", businessType = BusinessType.UPDATE)
@PutMapping("/{serialNum}/personal")
public AjaxResult updatePersonal(
@Parameter(description = "业务方流水号")
@PathVariable("serialNum") String serialNum,
@Validated @RequestBody PersonalLoanPricingCreateDTO dto)
{
return success(loanPricingWorkflowService.updatePersonalLoanPricing(serialNum, dto));
}
/**
* 编辑企业客户利率定价流程
*/
@Operation(summary = "编辑企业客户利率定价流程")
@Log(title = "企业客户利率定价流程", businessType = BusinessType.UPDATE)
@PutMapping("/{serialNum}/corporate")
public AjaxResult updateCorporate(
@Parameter(description = "业务方流水号")
@PathVariable("serialNum") String serialNum,
@Validated @RequestBody CorporateLoanPricingCreateDTO dto)
{
return success(loanPricingWorkflowService.updateCorporateLoanPricing(serialNum, dto));
}
/**
* 查询个人客户号映射
*/

View File

@@ -41,9 +41,9 @@ public class CorporateLoanPricingCreateDTO implements Serializable {
@NotBlank(message = "申请金额不能为空")
private String applyAmt;
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"", "存量新增", "存量转贷"})
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"", "存量新增", "存量转贷"})
@NotBlank(message = "业务种类不能为空")
@Pattern(regexp = "^(新|存量新增|存量转贷)$", message = "业务种类必须是:新、存量新增、存量转贷之一")
@Pattern(regexp = "^(新|存量新增|存量转贷)$", message = "业务种类必须是:新、存量新增、存量转贷之一")
private String businessType;
@Schema(description = "历史贷款利率", example = "3.65")
@@ -56,16 +56,22 @@ public class CorporateLoanPricingCreateDTO implements Serializable {
@NotBlank(message = "贷款期限不能为空")
private String loanTerm;
@Schema(description = "余值覆盖", example = "0")
private String resCover;
@Schema(description = "绿色贷款", example = "0")
private String isGreenLoan;
@Schema(description = "贸易和建筑业企业标识", example = "0")
private String isTradeBuildEnt;
@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "四类", "其他", "存单质押"})
@Pattern(regexp = "^(一类|二类|三类|四类|其他|存单质押)$", message = "抵质押类型必须是:一类、二类、三类、四类、其他、存单质押之一")
@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押", "存单质押", "股权质押", "其他质押"})
@Pattern(regexp = "^(一类|二类|三类|四类|排污权抵押|设备等其他不动产抵押|存单质押|股权质押|其他质押)$", message = "抵质押类型必须是:一类、二类、三类、四类、排污权抵押、设备等其他不动产抵押、存单质押、股权质押、其他质押之一")
private String collType;
@Schema(description = "抵质押物是否三方所有", example = "0")
private String collThirdParty;
@Schema(description = "存单票面利率", example = "2.15")
private String couponRate;
}

View File

@@ -158,6 +158,17 @@ public class ModelInvokeDTO {
*/
private String loanRateHistory;
/**
* 存单票面利率
*/
private String couponRate;
/**
* 余值覆盖(非必填)
* 可选值0/1
*/
private String resCover;
// /**
// * 贷款利率(必填)
// */

View File

@@ -41,14 +41,9 @@ public class PersonalLoanPricingCreateDTO implements Serializable {
@NotBlank(message = "申请金额不能为空")
private String applyAmt;
@Schema(description = "贷款用途", requiredMode = Schema.RequiredMode.REQUIRED, example = "business", allowableValues = {"consumer", "business"})
@NotBlank(message = "贷款用途不能为空")
@Pattern(regexp = "^(consumer|business)$", message = "贷款用途必须是consumer、business之一")
private String loanPurpose;
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"新客", "存量新增", "存量转贷"})
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"新增", "存量新增", "存量转贷"})
@NotBlank(message = "业务种类不能为空")
@Pattern(regexp = "^(新|存量新增|存量转贷)$", message = "业务种类必须是:新、存量新增、存量转贷之一")
@Pattern(regexp = "^(新|存量新增|存量转贷)$", message = "业务种类必须是:新、存量新增、存量转贷之一")
private String businessType;
@Schema(description = "历史贷款利率", example = "3.65")
@@ -58,15 +53,15 @@ public class PersonalLoanPricingCreateDTO implements Serializable {
@NotBlank(message = "借款期限不能为空")
private String loanTerm;
@Schema(description = "是否有经营佐证", example = "1")
private String bizProof;
@Schema(description = "循环功能", example = "0")
private String loanLoop;
@Schema(description = "抵质押类型", example = "一类")
@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一线", "一类", "二类", "三类", "存单质押", "其他质押"})
private String collType;
@Schema(description = "抵质押物是否三方所有", example = "0")
private String collThirdParty;
@Schema(description = "存单票面利率", example = "2.15")
private String couponRate;
}

View File

@@ -103,12 +103,18 @@ public class LoanPricingWorkflow implements Serializable
/** 贷款用途: consumer-消费/business-经营 */
private String loanPurpose;
/** 业务种类: 新/存量新增/存量转贷 */
/** 业务种类: 新/存量新增/存量转贷 */
private String businessType;
/** 历史贷款利率 */
private String loanRateHistory;
/** 存单票面利率 */
private String couponRate;
/** 余值覆盖: 0/1 */
private String resCover;
/** 是否有经营佐证: true/false */
private String bizProof;
@@ -143,10 +149,21 @@ public class LoanPricingWorkflow implements Serializable
/** 是否普惠小微借款人: true/false */
private String isInclusiveFinance;
/** 创建人部门ID */
private Long deptId;
/** 创建者 */
@TableField(fill = FieldFill.INSERT)
private String createBy;
/** 列表数据权限创建者过滤条件 */
@TableField(exist = false)
private String dataScopeCreateBy;
/** 列表数据权限部门过滤条件 */
@TableField(exist = false)
private Long dataScopeDeptId;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@TableField(fill = FieldFill.INSERT)

View File

@@ -88,7 +88,8 @@ public class ModelCorpOutputFields {
// BP_企业客户类别
private String bpEntType;
// TOTAL_BP_关联度
private String totoalBpRelevance;
@TableField("totoal_bp_relevance")
private String totalBpRelevance;
// 贷款期限
private String loanTerm;
// BP_贷款期限
@@ -112,7 +113,8 @@ public class ModelCorpOutputFields {
// BP_灰名单与逾期
private String bpGreyOverdue;
// TOTAL_BP_风险度
private String totoalBpRisk;
@TableField("totoal_bp_risk")
private String totalBpRisk;
// 浮动BP
private String totalBp;
// 测算利率

View File

@@ -116,8 +116,9 @@ public class ModelRetailOutputFields {
// BP_中间业务
private String bpMid;
// TOTAL_BP_关联度注意原字段名拼写错误totoalBpRelevance已保留原拼写
private String totoalBpRelevance;
// TOTAL_BP_关联度
@TableField("totoal_bp_relevance")
private String totalBpRelevance;
// 申请金额
private String applyAmt;
@@ -164,8 +165,9 @@ public class ModelRetailOutputFields {
// BP_灰名单与逾期
private String bpGreyOverdue;
// TOTAL_BP_风险度注意原字段名拼写错误totoalBpRisk已保留原拼写
private String totoalBpRisk;
// TOTAL_BP_风险度
@TableField("totoal_bp_risk")
private String totalBpRisk;
// 浮动BP
private String totalBp;

View File

@@ -9,6 +9,8 @@ public class LoanPricingWorkflowListVO
{
private String serialNum;
private String custIsn;
private String custName;
private String custType;

View File

@@ -34,6 +34,32 @@ public interface ILoanPricingWorkflowService
*/
public LoanPricingWorkflow createCorporateLoanPricing(CorporateLoanPricingCreateDTO dto);
/**
* 查询利率定价流程编辑数据
*
* @param serialNum 业务方流水号
* @return 编辑用流程数据
*/
public LoanPricingWorkflow selectEditableLoanPricingBySerialNum(String serialNum);
/**
* 编辑个人客户利率定价流程
*
* @param serialNum 业务方流水号
* @param dto 个人客户编辑DTO
* @return 更新后的流程数据
*/
public LoanPricingWorkflow updatePersonalLoanPricing(String serialNum, PersonalLoanPricingCreateDTO dto);
/**
* 编辑企业客户利率定价流程
*
* @param serialNum 业务方流水号
* @param dto 企业客户编辑DTO
* @return 更新后的流程数据
*/
public LoanPricingWorkflow updateCorporateLoanPricing(String serialNum, CorporateLoanPricingCreateDTO dto);
/**
* 查询利率定价流程列表
*

View File

@@ -6,7 +6,9 @@ import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
import java.net.URI;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@@ -30,6 +32,9 @@ public class LoanPricingCustomerMapService
@Value("${customer-map.corporate-url}")
private String corporateUrl;
@Value("${loan-pricing-external.app-code:}")
private String appCode;
public LoanPricingCustomerMapService()
{
this(new RestTemplate());
@@ -41,10 +46,16 @@ public class LoanPricingCustomerMapService
}
LoanPricingCustomerMapService(RestTemplate restTemplate, String personalUrl, String corporateUrl)
{
this(restTemplate, personalUrl, corporateUrl, null);
}
LoanPricingCustomerMapService(RestTemplate restTemplate, String personalUrl, String corporateUrl, String appCode)
{
this.restTemplate = restTemplate;
this.personalUrl = personalUrl;
this.corporateUrl = corporateUrl;
this.appCode = appCode;
}
public List<CustomerMapRecordVO> queryPersonal(String custId)
@@ -64,15 +75,12 @@ public class LoanPricingCustomerMapService
{
throw new ServiceException("客户号不能为空");
}
URI uri = UriComponentsBuilder.fromHttpUrl(url)
.queryParam("cust_id", normalizedCustId)
.build()
.encode()
.toUri();
URI uri = buildGetParamUri(url, "cust_id", normalizedCustId);
Map<String, String> requestParams = buildRequestParamLog("cust_id", normalizedCustId);
CustomerMapResponse response = restTemplate.getForObject(uri, CustomerMapResponse.class);
log.info("后端外部接口调用完成\n请求URL{}\n请求参数\n{}\n返回参数\n{}",
uri,
HttpUtils.formatLogValue(Collections.singletonMap("cust_id", normalizedCustId)),
HttpUtils.formatLogValue(requestParams),
HttpUtils.formatLogValue(response));
if (response == null)
{
@@ -85,6 +93,34 @@ public class LoanPricingCustomerMapService
return response.getData() == null ? Collections.emptyList() : response.getData();
}
private URI buildGetParamUri(String url, String paramName, String paramValue)
{
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url)
.replaceQueryParam("appCode")
.replaceQueryParam(paramName);
String normalizedAppCode = StringUtils.trimWhitespace(appCode);
if (StringUtils.hasText(normalizedAppCode))
{
builder.queryParam("appCode", normalizedAppCode);
}
return builder.queryParam(paramName, paramValue)
.build()
.encode()
.toUri();
}
private Map<String, String> buildRequestParamLog(String paramName, String paramValue)
{
Map<String, String> requestParams = new LinkedHashMap<>();
String normalizedAppCode = StringUtils.trimWhitespace(appCode);
if (StringUtils.hasText(normalizedAppCode))
{
requestParams.put("appCode", normalizedAppCode);
}
requestParams.put(paramName, paramValue);
return requestParams;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
static class CustomerMapResponse

View File

@@ -40,6 +40,14 @@ public class LoanPricingModelService {
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
public void invokeModelAsync(Long workflowId) {
invokeModelAndSave(workflowId, false);
}
public void reinvokeModelAndOverwrite(Long workflowId) {
invokeModelAndSave(workflowId, true);
}
private void invokeModelAndSave(Long workflowId, boolean overwriteModelOutput) {
LoanPricingWorkflow loanPricingWorkflow = loanPricingWorkflowMapper.selectById(workflowId);
if (Objects.isNull(loanPricingWorkflow)){
log.error("未找到对应的流程信息,未调用模型服务");
@@ -68,26 +76,43 @@ public class LoanPricingModelService {
if (loanPricingWorkflow.getCustType().equals("个人")){
// 个人模型
ModelRetailOutputFields modelRetailOutputFields = modelService.invokePersonalModel(modelInvokeDTO);
modelRetailOutputFieldsMapper.insert(modelRetailOutputFields);
if (overwriteModelOutput && Objects.nonNull(loanPricingWorkflow.getModelOutputId()))
{
modelRetailOutputFields.setId(loanPricingWorkflow.getModelOutputId());
modelRetailOutputFieldsMapper.updateById(modelRetailOutputFields);
}
else
{
modelRetailOutputFieldsMapper.insert(modelRetailOutputFields);
}
log.info("个人模型调用成功");
LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow();
workflowToUpdate.setId(loanPricingWorkflow.getId());
workflowToUpdate.setModelOutputId(modelRetailOutputFields.getId());
loanPricingWorkflowMapper.updateById(workflowToUpdate);
log.info("更新流程信息成功");
updateWorkflowModelOutputId(loanPricingWorkflow, modelRetailOutputFields.getId());
}else if (loanPricingWorkflow.getCustType().equals("企业")){
// 企业模型
ModelCorpOutputFields modelCorpOutputFields = modelService.invokeCorporateModel(modelInvokeDTO);
modelCorpOutputFieldsMapper.insert(modelCorpOutputFields);
if (overwriteModelOutput && Objects.nonNull(loanPricingWorkflow.getModelOutputId()))
{
modelCorpOutputFields.setId(loanPricingWorkflow.getModelOutputId());
modelCorpOutputFieldsMapper.updateById(modelCorpOutputFields);
}
else
{
modelCorpOutputFieldsMapper.insert(modelCorpOutputFields);
}
log.info("企业模型调用成功");
LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow();
workflowToUpdate.setId(loanPricingWorkflow.getId());
workflowToUpdate.setModelOutputId(modelCorpOutputFields.getId());
loanPricingWorkflowMapper.updateById(workflowToUpdate);
log.info("更新流程信息成功");
updateWorkflowModelOutputId(loanPricingWorkflow, modelCorpOutputFields.getId());
}
}
private void updateWorkflowModelOutputId(LoanPricingWorkflow loanPricingWorkflow, Long modelOutputId)
{
LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow();
workflowToUpdate.setId(loanPricingWorkflow.getId());
workflowToUpdate.setModelOutputId(modelOutputId);
loanPricingWorkflowMapper.updateById(workflowToUpdate);
log.info("更新流程信息成功");
}
private void normalizePersonalModelInvokeDTO(ModelInvokeDTO modelInvokeDTO)
{
modelInvokeDTO.setBizProof(toZeroOne(modelInvokeDTO.getBizProof()));
@@ -98,6 +123,7 @@ public class LoanPricingModelService {
private void normalizeCorporateModelInvokeDTO(ModelInvokeDTO modelInvokeDTO)
{
modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty()));
modelInvokeDTO.setResCover(toZeroOne(modelInvokeDTO.getResCover()));
modelInvokeDTO.setIsGreenLoan(toZeroOne(modelInvokeDTO.getIsGreenLoan()));
modelInvokeDTO.setIsTradeBuildEnt(toZeroOne(modelInvokeDTO.getIsTradeBuildEnt()));
}

View File

@@ -1,46 +0,0 @@
package com.ruoyi.loanpricing.service;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@Service
public class LoanPricingSensitiveDisplayService
{
public String maskCustName(String custName)
{
if (!StringUtils.hasText(custName))
{
return custName;
}
if (custName.contains("公司") && custName.length() > 4)
{
return custName.substring(0, 2) + "*".repeat(custName.length() - 4) + custName.substring(custName.length() - 2);
}
if (custName.length() == 1)
{
return custName;
}
return custName.substring(0, 1) + "*".repeat(custName.length() - 1);
}
public String maskIdNum(String idNum)
{
if (!StringUtils.hasText(idNum))
{
return idNum;
}
if (idNum.startsWith("91") && idNum.length() == 18)
{
return idNum.substring(0, 2) + "*".repeat(13) + idNum.substring(idNum.length() - 3);
}
if (idNum.matches("\\d{17}[\\dXx]"))
{
return idNum.substring(0, 4) + "*".repeat(8) + idNum.substring(idNum.length() - 4);
}
if (idNum.length() > 5)
{
return idNum.substring(0, 2) + "*".repeat(idNum.length() - 5) + idNum.substring(idNum.length() - 3);
}
return "*".repeat(idNum.length());
}
}

View File

@@ -6,7 +6,9 @@ import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.loanpricing.domain.vo.HistoryLoanContractVO;
import java.net.URI;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@@ -24,6 +26,9 @@ public class LoanRateHistoryService
@Value("${loan-rate-history.url}")
private String historyUrl;
@Value("${loan-pricing-external.app-code:}")
private String appCode;
public LoanRateHistoryService()
{
this(new RestTemplate());
@@ -35,9 +40,15 @@ public class LoanRateHistoryService
}
LoanRateHistoryService(RestTemplate restTemplate, String historyUrl)
{
this(restTemplate, historyUrl, null);
}
LoanRateHistoryService(RestTemplate restTemplate, String historyUrl, String appCode)
{
this.restTemplate = restTemplate;
this.historyUrl = historyUrl;
this.appCode = appCode;
}
public List<HistoryLoanContractVO> query(String custIsn)
@@ -47,15 +58,12 @@ public class LoanRateHistoryService
{
throw new ServiceException("客户内码不能为空");
}
URI uri = UriComponentsBuilder.fromHttpUrl(historyUrl)
.queryParam("cust_isn", normalizedCustIsn)
.build()
.encode()
.toUri();
URI uri = buildGetParamUri(historyUrl, "cust_isn", normalizedCustIsn);
Map<String, String> requestParams = buildRequestParamLog("cust_isn", normalizedCustIsn);
HistoryLoanContractResponse response = restTemplate.getForObject(uri, HistoryLoanContractResponse.class);
log.info("后端外部接口调用完成\n请求URL{}\n请求参数\n{}\n返回参数\n{}",
uri,
HttpUtils.formatLogValue(Collections.singletonMap("cust_isn", normalizedCustIsn)),
HttpUtils.formatLogValue(requestParams),
HttpUtils.formatLogValue(response));
if (response == null)
{
@@ -68,6 +76,34 @@ public class LoanRateHistoryService
return response.getData() == null ? Collections.emptyList() : response.getData();
}
private URI buildGetParamUri(String url, String paramName, String paramValue)
{
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url)
.replaceQueryParam("appCode")
.replaceQueryParam(paramName);
String normalizedAppCode = StringUtils.trimWhitespace(appCode);
if (StringUtils.hasText(normalizedAppCode))
{
builder.queryParam("appCode", normalizedAppCode);
}
return builder.queryParam(paramName, paramValue)
.build()
.encode()
.toUri();
}
private Map<String, String> buildRequestParamLog(String paramName, String paramValue)
{
Map<String, String> requestParams = new LinkedHashMap<>();
String normalizedAppCode = StringUtils.trimWhitespace(appCode);
if (StringUtils.hasText(normalizedAppCode))
{
requestParams.put("appCode", normalizedAppCode);
}
requestParams.put(paramName, paramValue);
return requestParams;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
static class HistoryLoanContractResponse

View File

@@ -1,9 +1,15 @@
package com.ruoyi.loanpricing.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
@@ -15,7 +21,6 @@ import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
import com.ruoyi.loanpricing.service.ILoanPricingWorkflowService;
import com.ruoyi.loanpricing.service.LoanPricingSensitiveDisplayService;
import com.ruoyi.loanpricing.service.LoanPricingModelService;
import com.ruoyi.loanpricing.service.SensitiveFieldCryptoService;
import com.ruoyi.loanpricing.util.LoanPricingConverter;
@@ -38,6 +43,14 @@ import java.util.Objects;
@Service
public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowService
{
private static final String WORKFLOW_ADMIN_ROLE_NAME = "管理员";
private static final String WORKFLOW_ADMIN_ROLE_KEY = "headAdmin";
private static final String WORKFLOW_BRANCH_ADMIN_ROLE_NAME = "支行管理员";
private static final String WORKFLOW_BRANCH_ADMIN_ROLE_KEY = "branchAdmin";
@Resource
private LoanPricingWorkflowMapper loanPricingWorkflowMapper;
@@ -53,10 +66,6 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
@Resource
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
@Resource
private LoanPricingSensitiveDisplayService loanPricingSensitiveDisplayService;
/**
* 发起利率定价流程
*
@@ -67,6 +76,7 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
public LoanPricingWorkflow createLoanPricing(LoanPricingWorkflow loanPricingWorkflow)
{
validateBusinessTypeAndHistoryRate(loanPricingWorkflow);
validateCollateralTypeAndCouponRate(loanPricingWorkflow);
// 自动生成业务方流水号(时间戳)
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
@@ -83,6 +93,7 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
loanPricingWorkflow.setRunType("1");
}
loanPricingWorkflow.setDeptId(SecurityUtils.getDeptId());
loanPricingWorkflow.setCustName(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getCustName()));
loanPricingWorkflow.setIdNum(sensitiveFieldCryptoService.encrypt(loanPricingWorkflow.getIdNum()));
loanPricingWorkflowMapper.insert(loanPricingWorkflow);
@@ -95,10 +106,10 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
if (!StringUtils.hasText(workflow.getBusinessType())) {
throw new ServiceException("业务种类不能为空");
}
if (!"".equals(workflow.getBusinessType())
if (!"".equals(workflow.getBusinessType())
&& !"存量新增".equals(workflow.getBusinessType())
&& !"存量转贷".equals(workflow.getBusinessType())) {
throw new ServiceException("业务种类必须是:新、存量新增、存量转贷之一");
throw new ServiceException("业务种类必须是:新、存量新增、存量转贷之一");
}
if ("存量转贷".equals(workflow.getBusinessType())
&& !StringUtils.hasText(workflow.getLoanRateHistory())) {
@@ -106,6 +117,57 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
}
}
private void validateCollateralTypeAndCouponRate(LoanPricingWorkflow workflow) {
if (!"抵押".equals(workflow.getGuarType()) && !"质押".equals(workflow.getGuarType())) {
return;
}
if (!StringUtils.hasText(workflow.getCollType())) {
throw new ServiceException("请选择抵质押类型");
}
if ("个人".equals(workflow.getCustType())) {
validatePersonalCollateralType(workflow);
}
if ("企业".equals(workflow.getCustType())) {
validateCorporateCollateralType(workflow);
}
if ("质押".equals(workflow.getGuarType())
&& "存单质押".equals(workflow.getCollType())
&& !StringUtils.hasText(workflow.getCouponRate())) {
throw new ServiceException("存单票面利率不能为空");
}
}
private void validatePersonalCollateralType(LoanPricingWorkflow workflow) {
if ("抵押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "一线", "一类", "二类", "三类")) {
throw new ServiceException("个人抵押抵质押类型必须是:一线、一类、二类、三类之一");
}
if ("质押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "存单质押", "其他质押")) {
throw new ServiceException("个人质押抵质押类型必须是:存单质押、其他质押之一");
}
}
private void validateCorporateCollateralType(LoanPricingWorkflow workflow) {
if ("抵押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押")) {
throw new ServiceException("企业抵押抵质押类型必须是:一类、二类、三类、四类、排污权抵押、设备等其他不动产抵押之一");
}
if ("质押".equals(workflow.getGuarType())
&& !isOneOf(workflow.getCollType(), "存单质押", "股权质押", "其他质押")) {
throw new ServiceException("企业质押抵质押类型必须是:存单质押、股权质押、其他质押之一");
}
}
private boolean isOneOf(String value, String... allowedValues) {
for (String allowedValue : allowedValues) {
if (allowedValue.equals(value)) {
return true;
}
}
return false;
}
/**
* 发起个人客户利率定价流程
*
@@ -132,6 +194,97 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
return createLoanPricing(entity);
}
@Override
public LoanPricingWorkflow selectEditableLoanPricingBySerialNum(String serialNum)
{
LoanPricingWorkflow workflow = selectWorkflowBySerialNum(serialNum);
assertCurrentUserIsCreator(workflow);
workflow.setCustName(sensitiveFieldCryptoService.decrypt(workflow.getCustName()));
workflow.setIdNum(sensitiveFieldCryptoService.decrypt(workflow.getIdNum()));
return workflow;
}
@Override
@Transactional(rollbackFor = Exception.class)
public LoanPricingWorkflow updatePersonalLoanPricing(String serialNum, PersonalLoanPricingCreateDTO dto)
{
LoanPricingWorkflow editingWorkflow = LoanPricingConverter.toEntity(dto);
return updateLoanPricing(serialNum, editingWorkflow, "个人");
}
@Override
@Transactional(rollbackFor = Exception.class)
public LoanPricingWorkflow updateCorporateLoanPricing(String serialNum, CorporateLoanPricingCreateDTO dto)
{
LoanPricingWorkflow editingWorkflow = LoanPricingConverter.toEntity(dto);
return updateLoanPricing(serialNum, editingWorkflow, "企业");
}
private LoanPricingWorkflow updateLoanPricing(String serialNum, LoanPricingWorkflow editingWorkflow, String expectedCustType)
{
LoanPricingWorkflow existingWorkflow = selectWorkflowBySerialNum(serialNum);
assertCurrentUserIsCreator(existingWorkflow);
if (!expectedCustType.equals(existingWorkflow.getCustType()))
{
throw new ServiceException("客户类型不匹配,不能编辑该流程");
}
editingWorkflow.setId(existingWorkflow.getId());
editingWorkflow.setSerialNum(existingWorkflow.getSerialNum());
editingWorkflow.setCustType(existingWorkflow.getCustType());
editingWorkflow.setModelOutputId(existingWorkflow.getModelOutputId());
validateBusinessTypeAndHistoryRate(editingWorkflow);
validateCollateralTypeAndCouponRate(editingWorkflow);
String encryptedCustName = sensitiveFieldCryptoService.encrypt(editingWorkflow.getCustName());
String encryptedIdNum = sensitiveFieldCryptoService.encrypt(editingWorkflow.getIdNum());
LambdaUpdateWrapper<LoanPricingWorkflow> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(LoanPricingWorkflow::getId, existingWorkflow.getId())
.set(LoanPricingWorkflow::getCustIsn, editingWorkflow.getCustIsn())
.set(LoanPricingWorkflow::getCustName, encryptedCustName)
.set(LoanPricingWorkflow::getIdType, editingWorkflow.getIdType())
.set(LoanPricingWorkflow::getIdNum, encryptedIdNum)
.set(LoanPricingWorkflow::getGuarType, editingWorkflow.getGuarType())
.set(LoanPricingWorkflow::getApplyAmt, editingWorkflow.getApplyAmt())
.set(LoanPricingWorkflow::getBusinessType, editingWorkflow.getBusinessType())
.set(LoanPricingWorkflow::getLoanRateHistory, editingWorkflow.getLoanRateHistory())
.set(LoanPricingWorkflow::getCouponRate, editingWorkflow.getCouponRate())
.set(LoanPricingWorkflow::getLoanTerm, editingWorkflow.getLoanTerm())
.set(LoanPricingWorkflow::getCollType, editingWorkflow.getCollType())
.set(LoanPricingWorkflow::getCollThirdParty, editingWorkflow.getCollThirdParty())
.set(LoanPricingWorkflow::getLoanLoop, editingWorkflow.getLoanLoop())
.set(LoanPricingWorkflow::getResCover, editingWorkflow.getResCover())
.set(LoanPricingWorkflow::getRepayMethod, editingWorkflow.getRepayMethod())
.set(LoanPricingWorkflow::getIsGreenLoan, editingWorkflow.getIsGreenLoan())
.set(LoanPricingWorkflow::getIsTradeBuildEnt, editingWorkflow.getIsTradeBuildEnt())
.set(LoanPricingWorkflow::getUpdateBy, buildCurrentCreateBy(SecurityUtils.getLoginUser()))
.set(LoanPricingWorkflow::getUpdateTime, new Date());
loanPricingWorkflowMapper.update(null, updateWrapper);
loanPricingModelService.reinvokeModelAndOverwrite(existingWorkflow.getId());
return selectEditableLoanPricingBySerialNum(serialNum);
}
private LoanPricingWorkflow selectWorkflowBySerialNum(String serialNum)
{
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LoanPricingWorkflow::getSerialNum, serialNum);
LoanPricingWorkflow workflow = loanPricingWorkflowMapper.selectOne(wrapper);
if (workflow == null)
{
throw new ServiceException("记录不存在");
}
return workflow;
}
private void assertCurrentUserIsCreator(LoanPricingWorkflow workflow)
{
String currentCreateBy = buildCurrentCreateBy(SecurityUtils.getLoginUser());
if (!currentCreateBy.equals(workflow.getCreateBy()))
{
throw new ServiceException("只有创建者可以编辑该流程");
}
}
/**
* 查询利率定价流程列表
*
@@ -157,10 +310,9 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
@Override
public IPage<LoanPricingWorkflowListVO> selectLoanPricingPage(Page<LoanPricingWorkflowListVO> page, LoanPricingWorkflow loanPricingWorkflow)
{
IPage<LoanPricingWorkflowListVO> pageResult = loanPricingWorkflowMapper.selectWorkflowPageWithRates(page, loanPricingWorkflow);
pageResult.getRecords().forEach(row -> row.setCustName(
loanPricingSensitiveDisplayService.maskCustName(
sensitiveFieldCryptoService.decrypt(row.getCustName()))));
LoanPricingWorkflow scopedQuery = applyWorkflowListDataScope(loanPricingWorkflow);
IPage<LoanPricingWorkflowListVO> pageResult = loanPricingWorkflowMapper.selectWorkflowPageWithRates(page, scopedQuery);
pageResult.getRecords().forEach(row -> row.setCustName(sensitiveFieldCryptoService.decrypt(row.getCustName())));
return pageResult;
}
@@ -180,8 +332,8 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
LoanPricingWorkflow loanPricingWorkflow = loanPricingWorkflowMapper.selectOne(wrapper);
String plainCustName = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getCustName());
String plainIdNum = sensitiveFieldCryptoService.decrypt(loanPricingWorkflow.getIdNum());
loanPricingWorkflow.setCustName(loanPricingSensitiveDisplayService.maskCustName(plainCustName));
loanPricingWorkflow.setIdNum(loanPricingSensitiveDisplayService.maskIdNum(plainIdNum));
loanPricingWorkflow.setCustName(plainCustName);
loanPricingWorkflow.setIdNum(plainIdNum);
loanPricingWorkflowVO.setLoanPricingWorkflow(loanPricingWorkflow);
if (Objects.nonNull(loanPricingWorkflow.getModelOutputId())){
@@ -189,7 +341,6 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
ModelRetailOutputFields modelRetailOutputFields = modelRetailOutputFieldsMapper.selectById(loanPricingWorkflow.getModelOutputId());
if (Objects.nonNull(modelRetailOutputFields))
{
maskModelRetailOutputBasicInfo(modelRetailOutputFields);
loanPricingWorkflow.setLoanRate(modelRetailOutputFields.getFinalCalculateRate());
}
loanPricingWorkflowVO.setModelRetailOutputFields(modelRetailOutputFields);
@@ -198,7 +349,6 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
ModelCorpOutputFields modelCorpOutputFields = modelCorpOutputFieldsMapper.selectById(loanPricingWorkflow.getModelOutputId());
if (Objects.nonNull(modelCorpOutputFields))
{
maskModelCorpOutputBasicInfo(modelCorpOutputFields);
loanPricingWorkflow.setLoanRate(modelCorpOutputFields.getFinalCalculateRate());
}
loanPricingWorkflowVO.setModelCorpOutputFields(modelCorpOutputFields);
@@ -240,20 +390,60 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
return wrapper;
}
private void maskModelRetailOutputBasicInfo(ModelRetailOutputFields modelRetailOutputFields)
private LoanPricingWorkflow applyWorkflowListDataScope(LoanPricingWorkflow query)
{
modelRetailOutputFields.setCustName(
loanPricingSensitiveDisplayService.maskCustName(modelRetailOutputFields.getCustName()));
modelRetailOutputFields.setIdNum(
loanPricingSensitiveDisplayService.maskIdNum(modelRetailOutputFields.getIdNum()));
LoanPricingWorkflow scopedQuery = query == null ? new LoanPricingWorkflow() : query;
LoginUser loginUser = SecurityUtils.getLoginUser();
if (canViewAllWorkflows(loginUser))
{
return scopedQuery;
}
if (canViewBranchWorkflows(loginUser))
{
scopedQuery.setDataScopeDeptId(loginUser.getDeptId() == null ? -1L : loginUser.getDeptId());
}
else
{
scopedQuery.setDataScopeCreateBy(buildCurrentCreateBy(loginUser));
}
return scopedQuery;
}
private void maskModelCorpOutputBasicInfo(ModelCorpOutputFields modelCorpOutputFields)
private boolean canViewAllWorkflows(LoginUser loginUser)
{
modelCorpOutputFields.setCustName(
loanPricingSensitiveDisplayService.maskCustName(modelCorpOutputFields.getCustName()));
modelCorpOutputFields.setIdNum(
loanPricingSensitiveDisplayService.maskIdNum(modelCorpOutputFields.getIdNum()));
SysUser user = loginUser.getUser();
if (user.isAdmin())
{
return true;
}
List<SysRole> roles = user.getRoles();
if (roles == null)
{
return false;
}
return roles.stream().anyMatch(role -> role != null
&& UserConstants.ROLE_NORMAL.equals(role.getStatus())
&& (WORKFLOW_ADMIN_ROLE_NAME.equals(role.getRoleName())
|| WORKFLOW_ADMIN_ROLE_KEY.equals(role.getRoleKey())));
}
private boolean canViewBranchWorkflows(LoginUser loginUser)
{
List<SysRole> roles = loginUser.getUser().getRoles();
if (roles == null)
{
return false;
}
return roles.stream().anyMatch(role -> role != null
&& UserConstants.ROLE_NORMAL.equals(role.getStatus())
&& (WORKFLOW_BRANCH_ADMIN_ROLE_NAME.equals(role.getRoleName())
|| WORKFLOW_BRANCH_ADMIN_ROLE_KEY.equals(role.getRoleKey())));
}
private String buildCurrentCreateBy(LoginUser loginUser)
{
SysUser user = loginUser.getUser();
return user.getNickName() + "-" + loginUser.getUsername();
}
/**

View File

@@ -28,14 +28,13 @@ public class LoanPricingConverter {
entity.setIdNum(dto.getIdNum());
entity.setGuarType(dto.getGuarType());
entity.setApplyAmt(dto.getApplyAmt());
entity.setLoanPurpose(dto.getLoanPurpose());
entity.setBusinessType(dto.getBusinessType());
entity.setLoanRateHistory(dto.getLoanRateHistory());
entity.setCouponRate(dto.getCouponRate());
entity.setLoanTerm(dto.getLoanTerm());
entity.setCollType(dto.getCollType());
entity.setCollThirdParty(dto.getCollThirdParty());
// 映射个人特有字段
entity.setBizProof(dto.getBizProof());
entity.setLoanLoop(dto.getLoanLoop());
return entity;
}
@@ -58,6 +57,8 @@ public class LoanPricingConverter {
entity.setApplyAmt(dto.getApplyAmt());
entity.setBusinessType(dto.getBusinessType());
entity.setLoanRateHistory(dto.getLoanRateHistory());
entity.setCouponRate(dto.getCouponRate());
entity.setResCover(dto.getResCover());
entity.setRepayMethod(dto.getRepayMethod());
entity.setCollType(dto.getCollType());
entity.setCollThirdParty(dto.getCollThirdParty());

View File

@@ -37,7 +37,7 @@
"isGreenLoan": "1",
"isTechEnt": "1",
"bpEntType": "7.5",
"totoalBpRelevance": "9.2",
"totalBpRelevance": "9.2",
"bpLoanTerm": "3.3",
"applyAmt": "5000000.00",
"bpLoanAmount": "5.8",
@@ -48,7 +48,7 @@
"prinOverdue": "0",
"interestOverdue": "0",
"bpGreyOverdue": "0",
"totoalBpRisk": "1.2",
"totalBpRisk": "1.2",
"totalBp": "48.2",
"baseLoanRate": "3.45",
"calculateRate": "3.932",

View File

@@ -35,7 +35,7 @@
"midPerFinMan": "5000.00",
"midPerEtc": "180.00",
"bpMid": "45",
"totoalBpRelevance": "90",
"totalBpRelevance": "90",
"applyAmt": "200000.00",
"bpLoanAmount": "60",
"loanPurpose": "个人消费",
@@ -51,7 +51,7 @@
"interestOverdue": "否",
"cardOverdue": "否",
"bpGreyOverdue": "98",
"totoalBpRisk": "95",
"totalBpRisk": "95",
"totalBp": "350",
"calculateRate": "6.15",
"loanRateHistory": "6.40",

View File

@@ -7,13 +7,14 @@
<select id="selectWorkflowPageWithRates" resultType="com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO">
SELECT
lpw.serial_num AS serialNum,
lpw.cust_isn AS custIsn,
lpw.cust_name AS custName,
lpw.cust_type AS custType,
lpw.guar_type AS guarType,
lpw.apply_amt AS applyAmt,
CASE
WHEN lpw.cust_type = '个人' THEN mr.final_calculate_rate
WHEN lpw.cust_type = '企业' THEN mc.calculate_rate
WHEN lpw.cust_type = '企业' THEN mc.final_calculate_rate
ELSE NULL
END AS calculateRate,
lpw.execute_rate AS executeRate,
@@ -23,8 +24,20 @@
LEFT JOIN model_retail_output_fields mr ON lpw.model_output_id = mr.id
LEFT JOIN model_corp_output_fields mc ON lpw.model_output_id = mc.id
<where>
<if test="query != null and query.dataScopeCreateBy != null and query.dataScopeCreateBy != ''">
AND lpw.create_by = #{query.dataScopeCreateBy}
</if>
<if test="query != null and query.dataScopeDeptId != null">
AND lpw.dept_id IN (
SELECT dept_id
FROM sys_dept
WHERE del_flag = '0'
AND (dept_id = #{query.dataScopeDeptId}
OR find_in_set(#{query.dataScopeDeptId}, ancestors))
)
</if>
<if test="query != null and query.createBy != null and query.createBy != ''">
AND lpw.create_by LIKE CONCAT('%', #{query.createBy}, '%')
AND SUBSTRING_INDEX(lpw.create_by, '-', -1) LIKE CONCAT('%', #{query.createBy}, '%')
</if>
<if test="query != null and query.custIsn != null and query.custIsn != ''">
AND lpw.cust_isn LIKE CONCAT('%', #{query.custIsn}, '%')

View File

@@ -10,9 +10,11 @@ class LoanPricingWorkflowListVOTest
void shouldExposeCalculateRateAndExecuteRateFields()
{
LoanPricingWorkflowListVO vo = new LoanPricingWorkflowListVO();
vo.setCustIsn("CUST001");
vo.setCalculateRate("6.15");
vo.setExecuteRate("5.80");
assertEquals("CUST001", vo.getCustIsn());
assertEquals("6.15", vo.getCalculateRate());
assertEquals("5.80", vo.getExecuteRate());
}

View File

@@ -17,6 +17,10 @@ class LoanPricingWorkflowMapperXmlTest
String xml = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
assertTrue(xml.contains("WHEN lpw.cust_type = '个人' THEN mr.final_calculate_rate"));
assertTrue(xml.contains("WHEN lpw.cust_type = '企业' THEN mc.calculate_rate"));
assertTrue(xml.contains("WHEN lpw.cust_type = '企业' THEN mc.final_calculate_rate"));
assertTrue(xml.contains("lpw.create_by = #{query.dataScopeCreateBy}"));
assertTrue(xml.contains("lpw.dept_id IN"));
assertTrue(xml.contains("find_in_set(#{query.dataScopeDeptId}, ancestors)"));
assertTrue(xml.contains("SUBSTRING_INDEX(lpw.create_by, '-', -1) LIKE CONCAT('%', #{query.createBy}, '%')"));
}
}

View File

@@ -18,6 +18,7 @@ import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
@@ -46,24 +47,28 @@ class LoanPricingModelServicePersonalParamsTest {
private LoanPricingModelService loanPricingModelService;
@Test
void shouldContainLoanPurposeAndLoanTermInPersonalCreateDto() throws NoSuchFieldException {
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanPurpose"));
void shouldRemoveLoanPurposeAndKeepLoanTermInPersonalCreateDto() throws NoSuchFieldException {
assertThrows(NoSuchFieldException.class,
() -> PersonalLoanPricingCreateDTO.class.getDeclaredField("loanPurpose"));
assertThrows(NoSuchFieldException.class,
() -> PersonalLoanPricingCreateDTO.class.getDeclaredField("bizProof"));
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanTerm"));
}
@Test
void shouldMapLoanPurposeAndLoanTermFromPersonalDto() {
void shouldNotMapLoanPurposeAndBizProofFromPersonalDto() {
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
dto.setCustName("张三");
dto.setGuarType("信用");
dto.setApplyAmt("100000");
dto.setLoanPurpose("business");
dto.setBusinessType("新增");
dto.setLoanTerm("3");
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
assertEquals("business", workflow.getLoanPurpose());
assertNull(workflow.getLoanPurpose());
assertNull(workflow.getBizProof());
assertEquals("3", workflow.getLoanTerm());
}
@@ -87,9 +92,7 @@ class LoanPricingModelServicePersonalParamsTest {
workflow.setIdNum("cipher-id");
workflow.setGuarType("信用");
workflow.setApplyAmt("100000");
workflow.setLoanPurpose("business");
workflow.setLoanTerm("3");
workflow.setBizProof("true");
workflow.setLoanLoop("false");
workflow.setCollThirdParty("true");
workflow.setCollType("一类");
@@ -115,9 +118,7 @@ class LoanPricingModelServicePersonalParamsTest {
&& Objects.equals("110101199001011234", dto.getIdNum())
&& Objects.equals("信用", dto.getGuarType())
&& Objects.equals("100000", dto.getApplyAmt())
&& Objects.equals("business", dto.getLoanPurpose())
&& Objects.equals("3", dto.getLoanTerm())
&& Objects.equals("1", dto.getBizProof())
&& Objects.equals("0", dto.getLoanLoop())
&& Objects.equals("1", dto.getCollThirdParty())
&& Objects.equals("一类", dto.getCollType())));

View File

@@ -29,6 +29,7 @@ class LoanPricingModelServiceTest
assertEquals("张三", context.modelService.personalRequest.getCustName());
assertEquals("110101199001011234", context.modelService.personalRequest.getIdNum());
assertEquals("2.15", context.modelService.personalRequest.getCouponRate());
assertEquals(1, context.modelService.personalCalls.get());
assertEquals(0, context.modelService.corporateCalls.get());
assertEquals(1, context.retailInsertCount.get());
@@ -56,24 +57,46 @@ class LoanPricingModelServiceTest
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
workflow.setRepayMethod("分期");
workflow.setResCover("true");
workflow.setIsGreenLoan("true");
workflow.setIsTradeBuildEnt("false");
workflow.setCollThirdParty("true");
workflow.setCouponRate("2.35");
TestContext context = createContext(workflow);
context.service.invokeModelAsync(3L);
ModelInvokeDTO request = context.modelService.corporateRequest;
assertEquals("分期", request.getRepayMethod());
assertEquals("1", request.getResCover());
assertEquals("1", request.getIsGreenLoan());
assertEquals("0", request.getIsTradeBuildEnt());
assertEquals("1", request.getCollThirdParty());
assertEquals("2.35", request.getCouponRate());
assertEquals(0, context.modelService.personalCalls.get());
assertEquals(1, context.modelService.corporateCalls.get());
assertEquals(0, context.retailInsertCount.get());
assertEquals(1, context.corpInsertCount.get());
}
@Test
void shouldOverwriteExistingCorporateModelOutputWhenReinvokingModel() throws Exception
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setId(4L);
workflow.setModelOutputId(88L);
workflow.setCustType("企业");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
TestContext context = createContext(workflow);
context.service.reinvokeModelAndOverwrite(4L);
assertEquals(1, context.modelService.corporateCalls.get());
assertEquals(0, context.corpInsertCount.get());
assertEquals(1, context.corpUpdateCount.get());
}
private static LoanPricingWorkflow personalWorkflow(Long id)
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
@@ -81,6 +104,7 @@ class LoanPricingModelServiceTest
workflow.setCustType("个人");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
workflow.setCouponRate("2.15");
return workflow;
}
@@ -92,15 +116,17 @@ class LoanPricingModelServiceTest
context.workflow = workflow;
context.updatedWorkflow = new AtomicReference<>();
context.retailInsertCount = new AtomicInteger();
context.retailUpdateCount = new AtomicInteger();
context.corpInsertCount = new AtomicInteger();
context.corpUpdateCount = new AtomicInteger();
setField(context.service, "modelService", context.modelService);
setField(context.service, "loanPricingWorkflowMapper",
workflowMapper(context.workflow, context.updatedWorkflow));
setField(context.service, "modelRetailOutputFieldsMapper",
insertCountingMapper(ModelRetailOutputFieldsMapper.class, context.retailInsertCount));
savingMapper(ModelRetailOutputFieldsMapper.class, context.retailInsertCount, context.retailUpdateCount));
setField(context.service, "modelCorpOutputFieldsMapper",
insertCountingMapper(ModelCorpOutputFieldsMapper.class, context.corpInsertCount));
savingMapper(ModelCorpOutputFieldsMapper.class, context.corpInsertCount, context.corpUpdateCount));
setField(context.service, "sensitiveFieldCryptoService", new TestSensitiveFieldCryptoService());
return context;
}
@@ -122,7 +148,7 @@ class LoanPricingModelServiceTest
});
}
private static <T> T insertCountingMapper(Class<T> mapperClass, AtomicInteger insertCount)
private static <T> T savingMapper(Class<T> mapperClass, AtomicInteger insertCount, AtomicInteger updateCount)
{
return proxy(mapperClass, (proxy, method, args) -> {
if ("insert".equals(method.getName()))
@@ -130,6 +156,11 @@ class LoanPricingModelServiceTest
insertCount.incrementAndGet();
return 1;
}
if ("updateById".equals(method.getName()))
{
updateCount.incrementAndGet();
return 1;
}
return defaultValue(method);
});
}
@@ -168,7 +199,9 @@ class LoanPricingModelServiceTest
private LoanPricingWorkflow workflow;
private AtomicReference<LoanPricingWorkflow> updatedWorkflow;
private AtomicInteger retailInsertCount;
private AtomicInteger retailUpdateCount;
private AtomicInteger corpInsertCount;
private AtomicInteger corpUpdateCount;
}
private static class CapturingModelService extends ModelService

View File

@@ -1,24 +0,0 @@
package com.ruoyi.loanpricing.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class LoanPricingSensitiveDisplayServiceTest
{
private final LoanPricingSensitiveDisplayService displayService = new LoanPricingSensitiveDisplayService();
@Test
void shouldMaskPersonalNameAndIdNum()
{
assertEquals("张*", displayService.maskCustName("张三"));
assertEquals("1101********1234", displayService.maskIdNum("110101199001011234"));
}
@Test
void shouldMaskCorporateNameAndCreditCode()
{
assertEquals("测试****公司", displayService.maskCustName("测试科技有限公司"));
assertEquals("91*************00X", displayService.maskIdNum("91110000100000000X"));
}
}

View File

@@ -1,6 +1,8 @@
package com.ruoyi.loanpricing.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
@@ -9,9 +11,17 @@ import static org.mockito.Mockito.when;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
@@ -20,16 +30,19 @@ import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
import com.ruoyi.loanpricing.service.LoanPricingSensitiveDisplayService;
import com.ruoyi.loanpricing.service.LoanPricingModelService;
import com.ruoyi.loanpricing.service.SensitiveFieldCryptoService;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Collections;
import java.util.Objects;
@@ -52,12 +65,21 @@ class LoanPricingWorkflowServiceImplTest
@Mock
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
@Mock
private LoanPricingSensitiveDisplayService loanPricingSensitiveDisplayService;
@InjectMocks
private LoanPricingWorkflowServiceImpl loanPricingWorkflowService;
@BeforeEach
void setUpLoginUser()
{
setLoginUser(1L, "admin", "若依", role(1L, "超级管理员", "admin"));
}
@AfterEach
void clearLoginUser()
{
SecurityContextHolder.clearContext();
}
@Test
void shouldEncryptCustNameAndIdNumBeforeInsert()
{
@@ -65,6 +87,7 @@ class LoanPricingWorkflowServiceImplTest
workflow.setCustName("张三");
workflow.setIdNum("110101199001011234");
workflow.setCustIsn("CUST001");
workflow.setBusinessType("新增");
when(sensitiveFieldCryptoService.encrypt("张三")).thenReturn("cipher-name");
when(sensitiveFieldCryptoService.encrypt("110101199001011234")).thenReturn("cipher-id");
@@ -74,6 +97,7 @@ class LoanPricingWorkflowServiceImplTest
verify(loanPricingWorkflowMapper).insert(argThat((LoanPricingWorkflow entity) ->
Objects.equals("cipher-name", entity.getCustName())
&& Objects.equals("cipher-id", entity.getIdNum())
&& Objects.equals(100L, entity.getDeptId())
&& Objects.equals("CUST001", entity.getCustIsn())));
}
@@ -94,7 +118,7 @@ class LoanPricingWorkflowServiceImplTest
}
@Test
void shouldMaskCustNameWhenReturningPagedWorkflowList()
void shouldReturnPlainCustNameWhenReturningPagedWorkflowList()
{
LoanPricingWorkflowListVO row = new LoanPricingWorkflowListVO();
row.setCustName("cipher-name");
@@ -104,11 +128,104 @@ class LoanPricingWorkflowServiceImplTest
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(pageResult);
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(loanPricingSensitiveDisplayService.maskCustName("张三")).thenReturn("张*");
IPage<LoanPricingWorkflowListVO> result = loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow());
assertEquals("*", result.getRecords().get(0).getCustName());
assertEquals("", result.getRecords().get(0).getCustName());
}
@Test
void shouldNotSetDataScopeCreateByForSuperAdminWhenReturningPagedWorkflowList()
{
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(emptyPageResult());
loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow());
ArgumentCaptor<LoanPricingWorkflow> queryCaptor = ArgumentCaptor.forClass(LoanPricingWorkflow.class);
verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture());
assertNull(queryCaptor.getValue().getDataScopeCreateBy());
assertNull(queryCaptor.getValue().getDataScopeDeptId());
}
@Test
void shouldNotSetDataScopeCreateByForBusinessAdminWhenReturningPagedWorkflowList()
{
setLoginUser(100L, "8929999", "测试管理员", role(100L, "管理员", "headAdmin"));
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(emptyPageResult());
loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow());
ArgumentCaptor<LoanPricingWorkflow> queryCaptor = ArgumentCaptor.forClass(LoanPricingWorkflow.class);
verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture());
assertNull(queryCaptor.getValue().getDataScopeCreateBy());
assertNull(queryCaptor.getValue().getDataScopeDeptId());
}
@Test
void shouldSetCurrentDeptForBranchAdminWhenReturningPagedWorkflowList()
{
setLoginUser(102L, 101L, "8920100", "测试支行管理员", role(102L, "支行管理员", "branchAdmin"));
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(emptyPageResult());
loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow());
ArgumentCaptor<LoanPricingWorkflow> queryCaptor = ArgumentCaptor.forClass(LoanPricingWorkflow.class);
verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture());
assertEquals(101L, queryCaptor.getValue().getDataScopeDeptId());
assertNull(queryCaptor.getValue().getDataScopeCreateBy());
}
@Test
void shouldKeepBranchAdminWithinDeptDataScopeWhenCreateByQueryIsProvided()
{
setLoginUser(102L, 101L, "8920100", "测试支行管理员", role(102L, "支行管理员", "branchAdmin"));
LoanPricingWorkflow query = new LoanPricingWorkflow();
query.setCreateBy("8920001");
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(emptyPageResult());
loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), query);
ArgumentCaptor<LoanPricingWorkflow> queryCaptor = ArgumentCaptor.forClass(LoanPricingWorkflow.class);
verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture());
assertEquals("8920001", queryCaptor.getValue().getCreateBy());
assertEquals(101L, queryCaptor.getValue().getDataScopeDeptId());
assertNull(queryCaptor.getValue().getDataScopeCreateBy());
}
@Test
void shouldSetCurrentCreateByForCustomerManagerWhenReturningPagedWorkflowList()
{
setLoginUser(101L, "8920001", "测试客户经理", role(101L, "客户经理", "common"));
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(emptyPageResult());
loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), new LoanPricingWorkflow());
ArgumentCaptor<LoanPricingWorkflow> queryCaptor = ArgumentCaptor.forClass(LoanPricingWorkflow.class);
verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture());
assertEquals("测试客户经理-8920001", queryCaptor.getValue().getDataScopeCreateBy());
}
@Test
void shouldKeepCustomerManagerWithinOwnDataScopeWhenCreateByQueryIsProvided()
{
setLoginUser(101L, "8920001", "测试客户经理", role(101L, "客户经理", "common"));
LoanPricingWorkflow query = new LoanPricingWorkflow();
query.setCreateBy("若依-admin");
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(emptyPageResult());
loanPricingWorkflowService.selectLoanPricingPage(new Page<>(1, 10), query);
ArgumentCaptor<LoanPricingWorkflow> queryCaptor = ArgumentCaptor.forClass(LoanPricingWorkflow.class);
verify(loanPricingWorkflowMapper).selectWorkflowPageWithRates(any(), queryCaptor.capture());
assertEquals("若依-admin", queryCaptor.getValue().getCreateBy());
assertEquals("测试客户经理-8920001", queryCaptor.getValue().getDataScopeCreateBy());
assertNull(queryCaptor.getValue().getDataScopeDeptId());
}
@Test
@@ -133,6 +250,270 @@ class LoanPricingWorkflowServiceImplTest
assertTrue(!sqlSegment.contains("cust_name"), sqlSegment);
}
@Test
void shouldRejectOldBusinessType()
{
LoanPricingWorkflow workflow = validWorkflow();
workflow.setBusinessType("新客");
ServiceException exception = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("业务种类必须是:新增、存量新增、存量转贷之一", exception.getMessage());
}
@Test
void shouldRequireHistoryRateForStockTransfer()
{
LoanPricingWorkflow workflow = validWorkflow();
workflow.setBusinessType("存量转贷");
ServiceException exception = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("请选择历史贷款合同", exception.getMessage());
}
@Test
void shouldRequireCouponRateForCertificatePledge()
{
LoanPricingWorkflow workflow = validWorkflow();
workflow.setCustType("企业");
workflow.setGuarType("质押");
workflow.setCollType("存单质押");
ServiceException exception = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(workflow));
assertEquals("存单票面利率不能为空", exception.getMessage());
}
@Test
void shouldAllowPersonalMortgageOneLineAndRejectOldOther()
{
LoanPricingWorkflow allowedWorkflow = validWorkflow();
allowedWorkflow.setCustType("个人");
allowedWorkflow.setGuarType("抵押");
allowedWorkflow.setCollType("一线");
loanPricingWorkflowService.createLoanPricing(allowedWorkflow);
LoanPricingWorkflow rejectedWorkflow = validWorkflow();
rejectedWorkflow.setCustType("个人");
rejectedWorkflow.setGuarType("抵押");
rejectedWorkflow.setCollType("其他");
ServiceException exception = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(rejectedWorkflow));
assertEquals("个人抵押抵质押类型必须是:一线、一类、二类、三类之一", exception.getMessage());
}
@Test
void shouldAllowCorporateMortgagePollutionRightAndPledgeEquity()
{
LoanPricingWorkflow mortgageWorkflow = validWorkflow();
mortgageWorkflow.setCustType("企业");
mortgageWorkflow.setGuarType("抵押");
mortgageWorkflow.setCollType("排污权抵押");
loanPricingWorkflowService.createLoanPricing(mortgageWorkflow);
LoanPricingWorkflow pledgeWorkflow = validWorkflow();
pledgeWorkflow.setCustType("企业");
pledgeWorkflow.setGuarType("质押");
pledgeWorkflow.setCollType("股权质押");
loanPricingWorkflowService.createLoanPricing(pledgeWorkflow);
LoanPricingWorkflow rejectedWorkflow = validWorkflow();
rejectedWorkflow.setCustType("企业");
rejectedWorkflow.setGuarType("质押");
rejectedWorkflow.setCollType("其他");
ServiceException exception = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.createLoanPricing(rejectedWorkflow));
assertEquals("企业质押抵质押类型必须是:存单质押、股权质押、其他质押之一", exception.getMessage());
}
@Test
void shouldCreatePersonalWorkflowWithCouponRateAndWithoutRemovedFields()
{
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
dto.setGuarType("质押");
dto.setApplyAmt("100000");
dto.setBusinessType("新增");
dto.setLoanTerm("3");
dto.setCollType("存单质押");
dto.setCouponRate("2.15");
loanPricingWorkflowService.createPersonalLoanPricing(dto);
verify(loanPricingWorkflowMapper).insert(argThat((LoanPricingWorkflow entity) ->
Objects.equals("个人", entity.getCustType())
&& Objects.equals("2.15", entity.getCouponRate())
&& Objects.isNull(entity.getLoanPurpose())
&& Objects.isNull(entity.getBizProof())));
}
@Test
void shouldCreateCorporateWorkflowWithCouponRateAndBusinessType()
{
CorporateLoanPricingCreateDTO dto = new CorporateLoanPricingCreateDTO();
dto.setCustIsn("CORP001");
dto.setGuarType("质押");
dto.setApplyAmt("1000000");
dto.setBusinessType("新增");
dto.setLoanTerm("3");
dto.setCollType("存单质押");
dto.setCouponRate("2.35");
dto.setResCover("1");
loanPricingWorkflowService.createCorporateLoanPricing(dto);
verify(loanPricingWorkflowMapper).insert(argThat((LoanPricingWorkflow entity) ->
Objects.equals("企业", entity.getCustType())
&& Objects.equals("新增", entity.getBusinessType())
&& Objects.equals("1", entity.getResCover())
&& Objects.equals("2.35", entity.getCouponRate())));
}
@Test
void shouldReturnEditableWorkflowWithPlainSensitiveFieldsForCreator()
{
LoanPricingWorkflow workflow = editableWorkflow("个人", "若依-admin");
workflow.setCustName("cipher-name");
workflow.setIdNum("cipher-id");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
LoanPricingWorkflow result = loanPricingWorkflowService.selectEditableLoanPricingBySerialNum("P20260525001");
assertEquals("张三", result.getCustName());
assertEquals("110101199001011234", result.getIdNum());
}
@Test
void shouldRejectEditableWorkflowWhenCurrentUserIsNotCreator()
{
LoanPricingWorkflow workflow = editableWorkflow("个人", "其他用户-8920001");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
ServiceException exception = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.selectEditableLoanPricingBySerialNum("P20260525001"));
assertEquals("只有创建者可以编辑该流程", exception.getMessage());
}
@Test
void shouldUpdatePersonalWorkflowAndReinvokeModelForCreator()
{
LoanPricingWorkflow workflow = editableWorkflow("个人", "若依-admin");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(sensitiveFieldCryptoService.encrypt("张三")).thenReturn("cipher-name-new");
when(sensitiveFieldCryptoService.encrypt("110101199001019999")).thenReturn("cipher-id-new");
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001019999");
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
dto.setCustName("张三");
dto.setIdType("身份证");
dto.setIdNum("110101199001019999");
dto.setGuarType("信用");
dto.setApplyAmt("200000");
dto.setBusinessType("新增");
dto.setLoanTerm("3");
dto.setLoanLoop("1");
loanPricingWorkflowService.updatePersonalLoanPricing("P20260525001", dto);
ArgumentCaptor<LambdaUpdateWrapper> updateWrapperCaptor = ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
verify(loanPricingWorkflowMapper).update(any(), updateWrapperCaptor.capture());
assertTrue(updateWrapperCaptor.getValue().getSqlSet().contains("apply_amt"));
verify(loanPricingModelService).reinvokeModelAndOverwrite(101L);
}
@Test
void shouldUpdateCorporateWorkflowAndReinvokeModelForCreator()
{
LoanPricingWorkflow workflow = editableWorkflow("企业", "若依-admin");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
when(sensitiveFieldCryptoService.encrypt("测试科技有限公司")).thenReturn("cipher-corp-name");
when(sensitiveFieldCryptoService.encrypt("91110000100000000X")).thenReturn("cipher-corp-id");
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("测试科技有限公司");
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("91110000100000000X");
CorporateLoanPricingCreateDTO dto = new CorporateLoanPricingCreateDTO();
dto.setCustIsn("CORP001");
dto.setCustName("测试科技有限公司");
dto.setIdType("统一社会信用代码");
dto.setIdNum("91110000100000000X");
dto.setGuarType("质押");
dto.setApplyAmt("300000");
dto.setBusinessType("新增");
dto.setLoanTerm("5");
dto.setCollType("存单质押");
dto.setCollThirdParty("1");
dto.setCouponRate("2.35");
dto.setResCover("1");
dto.setIsGreenLoan("1");
dto.setIsTradeBuildEnt("0");
loanPricingWorkflowService.updateCorporateLoanPricing("C20260525001", dto);
ArgumentCaptor<LambdaUpdateWrapper> updateWrapperCaptor = ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
verify(loanPricingWorkflowMapper).update(any(), updateWrapperCaptor.capture());
String sqlSet = updateWrapperCaptor.getValue().getSqlSet();
assertTrue(sqlSet.contains("res_cover"), sqlSet);
assertTrue(sqlSet.contains("is_green_loan"), sqlSet);
assertTrue(sqlSet.contains("is_trade_construction"), sqlSet);
assertTrue(sqlSet.contains("coll_third_party"), sqlSet);
verify(loanPricingModelService).reinvokeModelAndOverwrite(101L);
}
@Test
void shouldRejectUpdateWhenCustomerTypeDoesNotMatch()
{
LoanPricingWorkflow workflow = editableWorkflow("个人", "若依-admin");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
CorporateLoanPricingCreateDTO dto = new CorporateLoanPricingCreateDTO();
dto.setCustIsn("CORP001");
dto.setGuarType("信用");
dto.setApplyAmt("300000");
dto.setBusinessType("新增");
dto.setLoanTerm("5");
ServiceException exception = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.updateCorporateLoanPricing("P20260525001", dto));
assertEquals("客户类型不匹配,不能编辑该流程", exception.getMessage());
}
@Test
void shouldRejectUpdateWhenCurrentUserIsNotCreator()
{
LoanPricingWorkflow workflow = editableWorkflow("个人", "其他用户-8920001");
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
dto.setCustIsn("CUST001");
dto.setGuarType("信用");
dto.setApplyAmt("200000");
dto.setBusinessType("新增");
dto.setLoanTerm("3");
ServiceException exception = assertThrows(ServiceException.class,
() -> loanPricingWorkflowService.updatePersonalLoanPricing("P20260525001", dto));
assertEquals("只有创建者可以编辑该流程", exception.getMessage());
}
@Test
void shouldUseRetailModelOutputFinalCalculateRateForWorkflowDetail()
{
@@ -157,7 +538,7 @@ class LoanPricingWorkflowServiceImplTest
}
@Test
void shouldMaskCustNameAndIdNumWhenReturningWorkflowDetail()
void shouldReturnPlainCustNameAndIdNumWhenReturningWorkflowDetail()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("P20260328001");
@@ -168,17 +549,15 @@ class LoanPricingWorkflowServiceImplTest
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
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.getLoanPricingWorkflow().getCustName());
assertEquals("1101********1234", result.getLoanPricingWorkflow().getIdNum());
assertEquals("", result.getLoanPricingWorkflow().getCustName());
assertEquals("110101199001011234", result.getLoanPricingWorkflow().getIdNum());
}
@Test
void shouldMaskCustNameAndIdNumInRetailModelOutputBasicInfo()
void shouldReturnPlainCustNameAndIdNumInRetailModelOutputBasicInfo()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("P20260328001");
@@ -196,13 +575,11 @@ class LoanPricingWorkflowServiceImplTest
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());
assertEquals("", result.getModelRetailOutputFields().getCustName());
assertEquals("110101199001011234", result.getModelRetailOutputFields().getIdNum());
}
@Test
@@ -229,7 +606,7 @@ class LoanPricingWorkflowServiceImplTest
}
@Test
void shouldMaskCustNameAndIdNumInCorporateModelOutputBasicInfo()
void shouldReturnPlainCustNameAndIdNumInCorporateModelOutputBasicInfo()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setSerialNum("C20260328001");
@@ -247,12 +624,75 @@ class LoanPricingWorkflowServiceImplTest
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());
assertEquals("测试科技有限公司", result.getModelCorpOutputFields().getCustName());
assertEquals("91110000100000000X", result.getModelCorpOutputFields().getIdNum());
}
private LoanPricingWorkflow validWorkflow()
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setCustIsn("CUST001");
workflow.setCustType("个人");
workflow.setGuarType("信用");
workflow.setApplyAmt("100000");
workflow.setBusinessType("新增");
return workflow;
}
private LoanPricingWorkflow editableWorkflow(String custType, String createBy)
{
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
workflow.setId(101L);
workflow.setSerialNum("个人".equals(custType) ? "P20260525001" : "C20260525001");
workflow.setModelOutputId(201L);
workflow.setCustIsn("CUST001");
workflow.setCustType(custType);
workflow.setCustName("cipher-name");
workflow.setIdType("身份证");
workflow.setIdNum("cipher-id");
workflow.setGuarType("信用");
workflow.setApplyAmt("100000");
workflow.setBusinessType("新增");
workflow.setLoanTerm("3");
workflow.setCreateBy(createBy);
return workflow;
}
private Page<LoanPricingWorkflowListVO> emptyPageResult()
{
Page<LoanPricingWorkflowListVO> pageResult = new Page<>(1, 10);
pageResult.setRecords(Collections.emptyList());
return pageResult;
}
private void setLoginUser(Long userId, String username, String nickName, SysRole... roles)
{
setLoginUser(userId, 100L, username, nickName, roles);
}
private void setLoginUser(Long userId, Long deptId, String username, String nickName, SysRole... roles)
{
SysUser user = new SysUser();
user.setUserId(userId);
user.setDeptId(deptId);
user.setUserName(username);
user.setNickName(nickName);
user.setRoles(java.util.Arrays.asList(roles));
LoginUser loginUser = new LoginUser(userId, deptId, user, Collections.emptySet());
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
private SysRole role(Long roleId, String roleName, String roleKey)
{
SysRole role = new SysRole(roleId);
role.setRoleName(roleName);
role.setRoleKey(roleKey);
role.setStatus(UserConstants.ROLE_NORMAL);
return role;
}
}

View File

@@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 上虞利率定价系统
VUE_APP_TITLE = 贷款利率定价系统
# 开发环境配置
ENV = 'development'

View File

@@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 上虞利率定价系统
VUE_APP_TITLE = 贷款利率定价系统
# 生产环境配置
ENV = 'production'

View File

@@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 上虞利率定价系统
VUE_APP_TITLE = 贷款利率定价系统
BABEL_ENV = production

View File

@@ -17,6 +17,14 @@ export function getWorkflow(serialNum) {
})
}
// 查询利率定价流程编辑数据
export function getWorkflowEdit(serialNum) {
return request({
url: '/loanPricing/workflow/' + serialNum + '/edit',
method: 'get'
})
}
// 创建个人客户利率定价流程
export function createPersonalWorkflow(data) {
return request({
@@ -26,6 +34,15 @@ export function createPersonalWorkflow(data) {
})
}
// 编辑个人客户利率定价流程
export function updatePersonalWorkflow(serialNum, data) {
return request({
url: '/loanPricing/workflow/' + serialNum + '/personal',
method: 'put',
data: data
})
}
// 创建企业客户利率定价流程
export function createCorporateWorkflow(data) {
return request({
@@ -35,6 +52,15 @@ export function createCorporateWorkflow(data) {
})
}
// 编辑企业客户利率定价流程
export function updateCorporateWorkflow(serialNum, data) {
return request({
url: '/loanPricing/workflow/' + serialNum + '/corporate',
method: 'put',
data: data
})
}
// 查询个人客户号映射
export function queryPersonalCustomerMap(custId) {
return request({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -0,0 +1,14 @@
export function formatRate(value, emptyText = '-') {
if (value === null || value === undefined || value === '') {
return emptyText
}
const stringValue = String(value).trim()
if (stringValue === '') {
return emptyText
}
const numberValue = Number(stringValue)
if (Number.isNaN(numberValue)) {
return stringValue
}
return numberValue.toFixed(2)
}

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog title="新增企业利率定价流程" :visible.sync="dialogVisible" width="900px" append-to-body
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="900px" append-to-body
@close="handleClose">
<el-form ref="form" :model="form" :rules="rules" label-width="140px" class="workflow-create-form">
<!-- 基本信息 -->
@@ -59,12 +59,17 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="余值覆盖" prop="resCover">
<el-switch v-model="form.resCover"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="业务种类" prop="businessType">
<el-select v-model="form.businessType" placeholder="请选择业务种类" style="width: 100%" @change="handleBusinessTypeChange">
<el-option label="新" value="新"/>
<el-option label="新" value="新"/>
<el-option label="存量新增" value="存量新增"/>
<el-option label="存量转贷" value="存量转贷"/>
</el-select>
@@ -72,7 +77,7 @@
</el-col>
<el-col :span="12" v-if="isStockTransfer">
<el-form-item label="历史贷款利率" prop="loanRateHistory">
<el-input v-model="form.loanRateHistory" placeholder="请选择历史贷款合同" :readonly="true"/>
<el-input :value="formatRate(form.loanRateHistory, '')" placeholder="请选择历史贷款合同" :readonly="true"/>
</el-form-item>
</el-col>
</el-row>
@@ -107,6 +112,11 @@
<el-switch v-model="form.collThirdParty"/>
</el-form-item>
</el-col>
<el-col :span="12" v-if="isCertificatePledge">
<el-form-item label="存单票面利率" prop="couponRate">
<el-input v-model="form.couponRate" placeholder="请输入存单票面利率"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<history-contract-selector
@@ -123,8 +133,9 @@
</template>
<script>
import {createCorporateWorkflow, queryHistoryContracts} from "@/api/loanPricing/workflow"
import {createCorporateWorkflow, queryHistoryContracts, updateCorporateWorkflow} from "@/api/loanPricing/workflow"
import HistoryContractSelector from "./HistoryContractSelector"
import {formatRate} from "@/utils/rate"
export default {
name: "CorporateCreateDialog",
@@ -139,6 +150,10 @@ export default {
customerMap: {
type: Object,
default: null
},
editData: {
type: Object,
default: null
}
},
data() {
@@ -158,6 +173,14 @@ export default {
}
}
const validateCouponRate = (rule, value, callback) => {
if (this.isCertificatePledge && !value) {
callback(new Error('存单票面利率不能为空'))
return
}
callback()
}
// 贷款期限验证
const validateLoanTerm = (rule, value, callback) => {
if (!value && value !== 0) {
@@ -177,6 +200,7 @@ export default {
'1', '2', '3', '4', '5', '6', '7', '8', '9', '10'
],
submitting: false,
resettingForm: false,
showHistorySelector: false,
historyLoading: false,
historyContracts: [],
@@ -193,10 +217,12 @@ export default {
loanTerm: undefined,
businessType: undefined,
loanRateHistory: undefined,
resCover: false,
isGreenLoan: false,
isTradeBuildEnt: false,
collType: undefined,
collThirdParty: false
collThirdParty: false,
couponRate: undefined
},
rules: {
custIsn: [
@@ -230,6 +256,9 @@ export default {
],
collType: [
{required: true, message: "请选择抵质押类型", trigger: "change"}
],
couponRate: [
{validator: validateCouponRate, trigger: "blur"}
]
}
}
@@ -249,12 +278,21 @@ export default {
isStockTransfer() {
return this.form.businessType === '存量转贷'
},
isCertificatePledge() {
return this.form.guarType === '质押' && this.form.collType === '存单质押'
},
isEdit() {
return !!(this.editData && this.editData.serialNum)
},
dialogTitle() {
return this.isEdit ? '编辑企业利率定价流程' : '新增企业利率定价流程'
},
collateralTypeOptions() {
if (this.form.guarType === '抵押') {
return ['一类', '二类', '三类', '四类', '其他']
return ['一类', '二类', '三类', '四类', '排污权抵押', '设备等其他不动产抵押']
}
if (this.form.guarType === '质押') {
return ['存单质押', '其他']
return ['存单质押', '股权质押', '其他质押']
}
return []
}
@@ -266,15 +304,61 @@ export default {
}
},
'form.guarType'(val, oldVal) {
if (this.resettingForm) {
return
}
if (val !== oldVal) {
this.resetCollateralFields()
}
},
'form.collType'() {
if (this.resettingForm) {
return
}
this.resetCouponRateIfNotCertificatePledge()
}
},
methods: {
formatRate,
/** 表单重置 */
reset() {
this.form = {
this.resettingForm = true
this.form = this.buildForm()
this.submitting = false
this.showHistorySelector = false
this.historyLoading = false
this.historyContracts = []
this.selectedHistoryContract = null
this.$nextTick(() => {
this.resettingForm = false
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
},
buildForm() {
if (this.isEdit) {
return {
orgCode: this.editData.orgCode || '892000',
runType: this.editData.runType || '1',
custIsn: this.editData.custIsn,
custName: this.editData.custName,
idType: this.editData.idType,
idNum: this.editData.idNum,
guarType: this.editData.guarType,
applyAmt: this.editData.applyAmt,
loanTerm: this.editData.loanTerm,
businessType: this.editData.businessType,
loanRateHistory: this.editData.loanRateHistory,
resCover: this.isOne(this.editData.resCover),
isGreenLoan: this.isOne(this.editData.isGreenLoan),
isTradeBuildEnt: this.isOne(this.editData.isTradeBuildEnt),
collType: this.editData.collType,
collThirdParty: this.isOne(this.editData.collThirdParty),
couponRate: this.editData.couponRate
}
}
return {
orgCode: '892000',
runType: '1',
custIsn: this.customerMap ? this.customerMap.cust_isn : undefined,
@@ -286,21 +370,16 @@ export default {
loanTerm: undefined,
businessType: undefined,
loanRateHistory: undefined,
resCover: false,
isGreenLoan: false,
isTradeBuildEnt: false,
collType: undefined,
collThirdParty: false
collThirdParty: false,
couponRate: undefined
}
this.submitting = false
this.showHistorySelector = false
this.historyLoading = false
this.historyContracts = []
this.selectedHistoryContract = null
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
},
isOne(value) {
return value === true || value === 'true' || value === '1'
},
/** 对话框关闭处理 */
handleClose() {
@@ -315,12 +394,23 @@ export default {
resetCollateralFields() {
this.form.collType = undefined
this.form.collThirdParty = false
this.resetCouponRateIfNotCertificatePledge()
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate(['collType', 'collThirdParty'])
this.$refs.form.clearValidate(['collType', 'collThirdParty', 'couponRate'])
}
})
},
resetCouponRateIfNotCertificatePledge() {
if (!this.isCertificatePledge) {
this.form.couponRate = undefined
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate(['couponRate'])
}
})
}
},
handleBusinessTypeChange(value) {
this.clearHistoryContract()
if (value === '存量转贷') {
@@ -370,10 +460,15 @@ export default {
this.$modal.msgWarning("请选择历史贷款合同")
return
}
if (this.isCertificatePledge && !this.form.couponRate) {
this.$modal.msgWarning("存单票面利率不能为空")
return
}
this.submitting = true
// 转换开关值为字符串
const data = {
...this.form,
resCover: this.form.resCover ? '1' : '0',
isGreenLoan: this.form.isGreenLoan ? '1' : '0',
isTradeBuildEnt: this.form.isTradeBuildEnt ? '1' : '0'
}
@@ -386,9 +481,15 @@ export default {
if (!this.isStockTransfer) {
delete data.loanRateHistory
}
if (!this.isCertificatePledge) {
delete data.couponRate
}
createCorporateWorkflow(data).then(response => {
this.$modal.msgSuccess("新增成功")
const submitRequest = this.isEdit
? updateCorporateWorkflow(this.editData.serialNum, data)
: createCorporateWorkflow(data)
submitRequest.then(response => {
this.$modal.msgSuccess(this.isEdit ? "编辑成功" : "新增成功")
this.dialogVisible = false
this.$emit('success')
}).catch(error => {

View File

@@ -81,7 +81,8 @@
<el-descriptions-item label="担保方式">{{ detailData.guarType }}</el-descriptions-item>
<el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
<el-descriptions-item label="业务种类">{{ detailData.businessType || '-' }}</el-descriptions-item>
<el-descriptions-item label="历史贷款利率">{{ detailData.loanRateHistory || '-' }}</el-descriptions-item>
<el-descriptions-item label="历史贷款利率">{{ formatRate(detailData.loanRateHistory) }}</el-descriptions-item>
<el-descriptions-item label="余值覆盖">{{ formatBoolean(detailData.resCover) }}</el-descriptions-item>
<el-descriptions-item label="绿色贷款">{{ formatBoolean(detailData.isGreenLoan) }}</el-descriptions-item>
<el-descriptions-item label="贸易和建筑业企业">{{ formatBoolean(detailData.isTradeBuildEnt) }}</el-descriptions-item>
<el-descriptions-item label="抵质押类型">{{ detailData.collType || '-' }}</el-descriptions-item>
@@ -110,6 +111,7 @@
import {setExecuteRate} from "@/api/loanPricing/workflow"
import ModelOutputDisplay from "./ModelOutputDisplay.vue"
import BargainingPoolDisplay from "./BargainingPoolDisplay.vue"
import {formatRate} from "@/utils/rate"
export default {
name: "CorporateWorkflowDetail",
@@ -140,12 +142,13 @@ export default {
watch: {
'detailData.executeRate': {
handler(newVal) {
this.executeRateInput = newVal || ''
this.executeRateInput = formatRate(newVal, '')
},
immediate: true
}
},
methods: {
formatRate,
/** 格式化布尔值为中文 */
formatBoolean(value) {
if (value === 'true' || value === true || value === '1' || value === 1) return '是'
@@ -154,7 +157,7 @@ export default {
},
/** 获取基准利率 */
getBaseLoanRate() {
return this.corpOutput?.baseLoanRate || '-'
return this.formatRate(this.corpOutput?.baseLoanRate)
},
/** 获取浮动BP */
getTotalBp() {
@@ -162,7 +165,7 @@ export default {
},
/** 获取最终测算利率 */
getCalculateRate() {
return this.corpOutput?.finalCalculateRate || '-'
return this.formatRate(this.corpOutput?.finalCalculateRate)
},
/** 设定执行利率 */
handleSetExecuteRate() {

View File

@@ -18,7 +18,11 @@
<el-table-column label="历史贷款合同号" prop="loan_contract_history" align="center" :show-overflow-tooltip="true"/>
<el-table-column label="历史贷款担保方式" prop="guar_type_history" align="center"/>
<el-table-column label="历史贷款产品代码" prop="product_code_history" align="center"/>
<el-table-column label="历史贷款利率" prop="loan_rate_history" align="center"/>
<el-table-column label="历史贷款利率" prop="loan_rate_history" align="center">
<template slot-scope="scope">
<span>{{ formatRate(scope.row.loan_rate_history) }}</span>
</template>
</el-table-column>
<el-table-column label="历史贷款金额" prop="loan_amount_history" align="center"/>
<el-table-column label="历史贷款签订时间" prop="loan_sign_date_history" align="center" width="150"/>
</el-table>
@@ -30,6 +34,8 @@
</template>
<script>
import {formatRate} from "@/utils/rate"
export default {
name: "HistoryContractSelector",
props: {
@@ -63,6 +69,7 @@ export default {
}
},
methods: {
formatRate,
contractRadioValue(row, index) {
return row.loan_contract_history || `${row.cust_isn || ''}-${index}`
},

View File

@@ -13,7 +13,7 @@
<el-descriptions-item label="客户名称">{{ retailOutput.custName || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件类型">{{ retailOutput.idType || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ retailOutput.idNum || '-' }}</el-descriptions-item>
<el-descriptions-item label="基准利率"><span class="rate-value">{{ retailOutput.baseLoanRate || '-' }}</span> %</el-descriptions-item>
<el-descriptions-item label="基准利率"><span class="rate-value">{{ formatRate(retailOutput.baseLoanRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="灰黑名单客户">{{ formatOutputValue(retailOutput.greyBlackCust) }}</el-descriptions-item>
</el-descriptions>
</div>
@@ -22,12 +22,12 @@
<h4 class="section-title">测算结果</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="浮动BP"><span class="total-bp-value">{{ retailOutput.totalBp || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="测算利率"><span class="calculate-rate">{{ retailOutput.calculateRate || '-' }}</span> %</el-descriptions-item>
<el-descriptions-item label="历史利率">{{ retailOutput.loanRateHistory || '-' }}</el-descriptions-item>
<el-descriptions-item label="产品最低利率下限">{{ retailOutput.minRateProduct || '-' }}</el-descriptions-item>
<el-descriptions-item label="平滑幅度">{{ retailOutput.smoothRange || '-' }}</el-descriptions-item>
<el-descriptions-item label="参考利率"><span class="calculate-rate">{{ retailOutput.referenceRate || '-' }}</span> %</el-descriptions-item>
<el-descriptions-item label="最终测算利率"><span class="calculate-rate">{{ retailOutput.finalCalculateRate || '-' }}</span> %</el-descriptions-item>
<el-descriptions-item label="测算利率"><span class="calculate-rate">{{ formatRate(retailOutput.calculateRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="历史利率">{{ formatRate(retailOutput.loanRateHistory) }}</el-descriptions-item>
<el-descriptions-item label="产品最低利率下限">{{ formatRate(retailOutput.minRateProduct) }}</el-descriptions-item>
<el-descriptions-item label="平滑幅度">{{ formatRate(retailOutput.smoothRange) }}</el-descriptions-item>
<el-descriptions-item label="参考利率"><span class="calculate-rate">{{ formatRate(retailOutput.referenceRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="最终测算利率"><span class="calculate-rate">{{ formatRate(retailOutput.finalCalculateRate) }}</span> %</el-descriptions-item>
</el-descriptions>
</div>
@@ -49,7 +49,7 @@
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="存款年日均">{{ retailOutput.balanceAvg || '-' }}</el-descriptions-item>
<el-descriptions-item label="贷款年日均">{{ retailOutput.loanAvg || '-' }}</el-descriptions-item>
<el-descriptions-item label="派生率">{{ retailOutput.derivationRate || '-' }}</el-descriptions-item>
<el-descriptions-item label="派生率">{{ formatRate(retailOutput.derivationRate) }}</el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_贡献度"><span class="total-bp-value">{{ retailOutput.totalBpContribution || '-' }}</span></el-descriptions-item>
</el-descriptions>
</div>
@@ -70,7 +70,7 @@
<el-descriptions-item label="中间业务_个人_理财业务">{{ formatBoolean(retailOutput.midPerFinMan) }}</el-descriptions-item>
<el-descriptions-item label="中间业务_个人_etc">{{ formatBoolean(retailOutput.midPerEtc) }}</el-descriptions-item>
<el-descriptions-item label="BP_中间业务"><span class="bp-value">{{ retailOutput.bpMid || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_关联度"><span class="total-bp-value">{{ retailOutput.totoalBpRelevance || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_关联度"><span class="total-bp-value">{{ retailOutput.totalBpRelevance || '-' }}</span></el-descriptions-item>
</el-descriptions>
</div>
@@ -98,7 +98,7 @@
<el-descriptions-item label="利息逾期">{{ formatBoolean(retailOutput.interestOverdue) }}</el-descriptions-item>
<el-descriptions-item label="信用卡逾期">{{ formatBoolean(retailOutput.cardOverdue) }}</el-descriptions-item>
<el-descriptions-item label="BP_灰名单与逾期"><span class="bp-value">{{ retailOutput.bpGreyOverdue || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_风险度"><span class="total-bp-value">{{ retailOutput.totoalBpRisk || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_风险度"><span class="total-bp-value">{{ retailOutput.totalBpRisk || '-' }}</span></el-descriptions-item>
</el-descriptions>
</div>
</template>
@@ -112,7 +112,7 @@
<el-descriptions-item label="客户名称">{{ corpOutput.custName || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件类型">{{ corpOutput.idType || '-' }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ corpOutput.idNum || '-' }}</el-descriptions-item>
<el-descriptions-item label="基准利率"><span class="rate-value">{{ corpOutput.baseLoanRate || '-' }}</span> %</el-descriptions-item>
<el-descriptions-item label="基准利率"><span class="rate-value">{{ formatRate(corpOutput.baseLoanRate) }}</span> %</el-descriptions-item>
</el-descriptions>
</div>
@@ -120,12 +120,12 @@
<h4 class="section-title">测算结果</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="浮动BP"><span class="total-bp-value">{{ corpOutput.totalBp || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="测算利率"><span class="calculate-rate">{{ corpOutput.calculateRate || '-' }}</span> %</el-descriptions-item>
<el-descriptions-item label="历史利率">{{ corpOutput.loanRateHistory || '-' }}</el-descriptions-item>
<el-descriptions-item label="产品最低利率下限">{{ corpOutput.minRateProduct || '-' }}</el-descriptions-item>
<el-descriptions-item label="平滑幅度">{{ corpOutput.smoothRange || '-' }}</el-descriptions-item>
<el-descriptions-item label="参考利率"><span class="calculate-rate">{{ corpOutput.referenceRate || '-' }}</span> %</el-descriptions-item>
<el-descriptions-item label="最终测算利率"><span class="calculate-rate">{{ corpOutput.finalCalculateRate || '-' }}</span> %</el-descriptions-item>
<el-descriptions-item label="测算利率"><span class="calculate-rate">{{ formatRate(corpOutput.calculateRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="历史利率">{{ formatRate(corpOutput.loanRateHistory) }}</el-descriptions-item>
<el-descriptions-item label="产品最低利率下限">{{ formatRate(corpOutput.minRateProduct) }}</el-descriptions-item>
<el-descriptions-item label="平滑幅度">{{ formatRate(corpOutput.smoothRange) }}</el-descriptions-item>
<el-descriptions-item label="参考利率"><span class="calculate-rate">{{ formatRate(corpOutput.referenceRate) }}</span> %</el-descriptions-item>
<el-descriptions-item label="最终测算利率"><span class="calculate-rate">{{ formatRate(corpOutput.finalCalculateRate) }}</span> %</el-descriptions-item>
</el-descriptions>
</div>
@@ -145,7 +145,7 @@
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="存款年日均">{{ corpOutput.balanceAvg || '-' }}</el-descriptions-item>
<el-descriptions-item label="贷款年日均">{{ corpOutput.loanAvg || '-' }}</el-descriptions-item>
<el-descriptions-item label="派生率">{{ corpOutput.derivationRate || '-' }}</el-descriptions-item>
<el-descriptions-item label="派生率">{{ formatRate(corpOutput.derivationRate) }}</el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_贡献度"><span class="total-bp-value">{{ corpOutput.totalBpContribution || '-' }}</span></el-descriptions-item>
</el-descriptions>
</div>
@@ -164,7 +164,7 @@
<el-descriptions-item label="代发工资户数">{{ corpOutput.payroll || '-' }}</el-descriptions-item>
<el-descriptions-item label="存量贷款余额">{{ corpOutput.invLoanAmount || '-' }}</el-descriptions-item>
<el-descriptions-item label="BP_代发工资"><span class="bp-value">{{ corpOutput.bpPayroll || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_关联度"><span class="total-bp-value">{{ corpOutput.totoalBpRelevance || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_关联度"><span class="total-bp-value">{{ corpOutput.totalBpRelevance || '-' }}</span></el-descriptions-item>
</el-descriptions>
</div>
@@ -200,7 +200,7 @@
<el-descriptions-item label="本金逾期">{{ formatBoolean(corpOutput.prinOverdue) }}</el-descriptions-item>
<el-descriptions-item label="利息逾期">{{ formatBoolean(corpOutput.interestOverdue) }}</el-descriptions-item>
<el-descriptions-item label="BP_灰名单与逾期"><span class="bp-value">{{ corpOutput.bpGreyOverdue || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_风险度"><span class="total-bp-value">{{ corpOutput.totoalBpRisk || '-' }}</span></el-descriptions-item>
<el-descriptions-item label="TOTAL_BP_风险度"><span class="total-bp-value">{{ corpOutput.totalBpRisk || '-' }}</span></el-descriptions-item>
</el-descriptions>
</div>
</template>
@@ -209,6 +209,8 @@
</template>
<script>
import {formatRate} from "@/utils/rate"
export default {
name: "ModelOutputDisplay",
props: {
@@ -226,6 +228,7 @@ export default {
}
},
methods: {
formatRate,
/** 格式化布尔值为中文 */
formatBoolean(value) {
if (value === 'true' || value === true || value === '1' || value === 1) return '是'

View File

@@ -1,5 +1,5 @@
<template>
<el-dialog title="新增个人利率定价流程" :visible.sync="dialogVisible" width="900px" append-to-body
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="900px" append-to-body
@close="handleClose">
<el-form ref="form" :model="form" :rules="rules" label-width="140px" class="workflow-create-form">
<!-- 基本信息 -->
@@ -52,14 +52,6 @@
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="贷款用途" prop="loanPurpose">
<el-select v-model="form.loanPurpose" placeholder="请选择贷款用途" style="width: 100%">
<el-option label="消费" value="consumer"/>
<el-option label="经营" value="business"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="借款期限(年)" prop="loanTerm">
<el-select v-model="form.loanTerm" placeholder="请选择借款期限" style="width: 100%">
@@ -72,7 +64,7 @@
<el-col :span="12">
<el-form-item label="业务种类" prop="businessType">
<el-select v-model="form.businessType" placeholder="请选择业务种类" style="width: 100%" @change="handleBusinessTypeChange">
<el-option label="新" value="新"/>
<el-option label="新" value="新"/>
<el-option label="存量新增" value="存量新增"/>
<el-option label="存量转贷" value="存量转贷"/>
</el-select>
@@ -80,16 +72,11 @@
</el-col>
<el-col :span="12" v-if="isStockTransfer">
<el-form-item label="历史贷款利率" prop="loanRateHistory">
<el-input v-model="form.loanRateHistory" placeholder="请选择历史贷款合同" :readonly="true"/>
<el-input :value="formatRate(form.loanRateHistory, '')" placeholder="请选择历史贷款合同" :readonly="true"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="是否有经营佐证" prop="bizProof">
<el-switch v-model="form.bizProof"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="循环功能" prop="loanLoop">
<el-switch v-model="form.loanLoop"/>
@@ -112,6 +99,11 @@
<el-switch v-model="form.collThirdParty"/>
</el-form-item>
</el-col>
<el-col :span="12" v-if="isCertificatePledge">
<el-form-item label="存单票面利率" prop="couponRate">
<el-input v-model="form.couponRate" placeholder="请输入存单票面利率"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<history-contract-selector
@@ -128,8 +120,9 @@
</template>
<script>
import {createPersonalWorkflow, queryHistoryContracts} from "@/api/loanPricing/workflow"
import {createPersonalWorkflow, queryHistoryContracts, updatePersonalWorkflow} from "@/api/loanPricing/workflow"
import HistoryContractSelector from "./HistoryContractSelector"
import {formatRate} from "@/utils/rate"
export default {
name: "PersonalCreateDialog",
@@ -144,6 +137,10 @@ export default {
customerMap: {
type: Object,
default: null
},
editData: {
type: Object,
default: null
}
},
data() {
@@ -163,11 +160,20 @@ export default {
}
}
const validateCouponRate = (rule, value, callback) => {
if (this.isCertificatePledge && !value) {
callback(new Error('存单票面利率不能为空'))
return
}
callback()
}
return {
loanTermOptions: [
'1', '2', '3', '4', '5', '6'
],
submitting: false,
resettingForm: false,
showHistorySelector: false,
historyLoading: false,
historyContracts: [],
@@ -181,14 +187,13 @@ export default {
idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined,
guarType: undefined,
applyAmt: undefined,
loanPurpose: undefined,
loanTerm: undefined,
businessType: undefined,
loanRateHistory: undefined,
bizProof: false,
loanLoop: false,
collType: undefined,
collThirdParty: false
collThirdParty: false,
couponRate: undefined
},
rules: {
custIsn: [
@@ -211,9 +216,6 @@ export default {
applyAmt: [
{required: true, validator: validateApplyAmt, trigger: "blur"}
],
loanPurpose: [
{required: true, message: "请选择贷款用途", trigger: "change"}
],
loanTerm: [
{required: true, message: "请选择借款期限", trigger: "change"}
],
@@ -222,6 +224,12 @@ export default {
],
loanRateHistory: [
{required: true, message: "请选择历史贷款合同", trigger: "change"}
],
collType: [
{required: true, message: "请选择抵质押类型", trigger: "change"}
],
couponRate: [
{validator: validateCouponRate, trigger: "blur"}
]
}
}
@@ -241,12 +249,21 @@ export default {
isStockTransfer() {
return this.form.businessType === '存量转贷'
},
isCertificatePledge() {
return this.form.guarType === '质押' && this.form.collType === '存单质押'
},
isEdit() {
return !!(this.editData && this.editData.serialNum)
},
dialogTitle() {
return this.isEdit ? '编辑个人利率定价流程' : '新增个人利率定价流程'
},
collateralTypeOptions() {
if (this.form.guarType === '抵押') {
return ['一类', '二类', '三类', '四类', '其他']
return ['一线', '一类', '二类', '三类']
}
if (this.form.guarType === '质押') {
return ['存单质押', '其他']
return ['存单质押', '其他质押']
}
return []
}
@@ -258,15 +275,59 @@ export default {
}
},
'form.guarType'(val, oldVal) {
if (this.resettingForm) {
return
}
if (val !== oldVal) {
this.resetCollateralFields()
}
},
'form.collType'() {
if (this.resettingForm) {
return
}
this.resetCouponRateIfNotCertificatePledge()
}
},
methods: {
formatRate,
/** 表单重置 */
reset() {
this.form = {
this.resettingForm = true
this.form = this.buildForm()
this.submitting = false
this.showHistorySelector = false
this.historyLoading = false
this.historyContracts = []
this.selectedHistoryContract = null
this.$nextTick(() => {
this.resettingForm = false
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
},
buildForm() {
if (this.isEdit) {
return {
orgCode: this.editData.orgCode || '892000',
runType: this.editData.runType || '1',
custIsn: this.editData.custIsn,
custName: this.editData.custName,
idType: this.editData.idType,
idNum: this.editData.idNum,
guarType: this.editData.guarType,
applyAmt: this.editData.applyAmt,
loanTerm: this.editData.loanTerm,
businessType: this.editData.businessType,
loanRateHistory: this.editData.loanRateHistory,
loanLoop: this.isOne(this.editData.loanLoop),
collType: this.editData.collType,
collThirdParty: this.isOne(this.editData.collThirdParty),
couponRate: this.editData.couponRate
}
}
return {
orgCode: '892000',
runType: '1',
custIsn: this.customerMap ? this.customerMap.cust_isn : undefined,
@@ -275,25 +336,17 @@ export default {
idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined,
guarType: undefined,
applyAmt: undefined,
loanPurpose: undefined,
loanTerm: undefined,
businessType: undefined,
loanRateHistory: undefined,
bizProof: false,
loanLoop: false,
collType: undefined,
collThirdParty: false
collThirdParty: false,
couponRate: undefined
}
this.submitting = false
this.showHistorySelector = false
this.historyLoading = false
this.historyContracts = []
this.selectedHistoryContract = null
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
},
isOne(value) {
return value === true || value === 'true' || value === '1'
},
/** 对话框关闭处理 */
handleClose() {
@@ -308,12 +361,23 @@ export default {
resetCollateralFields() {
this.form.collType = undefined
this.form.collThirdParty = false
this.resetCouponRateIfNotCertificatePledge()
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate(['collType', 'collThirdParty'])
this.$refs.form.clearValidate(['collType', 'collThirdParty', 'couponRate'])
}
})
},
resetCouponRateIfNotCertificatePledge() {
if (!this.isCertificatePledge) {
this.form.couponRate = undefined
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.clearValidate(['couponRate'])
}
})
}
},
handleBusinessTypeChange(value) {
this.clearHistoryContract()
if (value === '存量转贷') {
@@ -363,11 +427,14 @@ export default {
this.$modal.msgWarning("请选择历史贷款合同")
return
}
if (this.isCertificatePledge && !this.form.couponRate) {
this.$modal.msgWarning("存单票面利率不能为空")
return
}
this.submitting = true
// 转换开关值为字符串
const data = {
...this.form,
bizProof: this.form.bizProof ? '1' : '0',
loanLoop: this.form.loanLoop ? '1' : '0'
}
if (this.isCollateralGuarantee) {
@@ -379,9 +446,15 @@ export default {
if (!this.isStockTransfer) {
delete data.loanRateHistory
}
if (!this.isCertificatePledge) {
delete data.couponRate
}
createPersonalWorkflow(data).then(response => {
this.$modal.msgSuccess("新增成功")
const submitRequest = this.isEdit
? updatePersonalWorkflow(this.editData.serialNum, data)
: createPersonalWorkflow(data)
submitRequest.then(response => {
this.$modal.msgSuccess(this.isEdit ? "编辑成功" : "新增成功")
this.dialogVisible = false
this.$emit('success')
}).catch(error => {

View File

@@ -81,7 +81,7 @@
<el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
<el-descriptions-item label="贷款用途">{{ formatLoanPurpose(detailData.loanPurpose) }}</el-descriptions-item>
<el-descriptions-item label="业务种类">{{ detailData.businessType || '-' }}</el-descriptions-item>
<el-descriptions-item label="历史贷款利率">{{ detailData.loanRateHistory || '-' }}</el-descriptions-item>
<el-descriptions-item label="历史贷款利率">{{ formatRate(detailData.loanRateHistory) }}</el-descriptions-item>
<el-descriptions-item label="借款期限">{{ detailData.loanTerm || '-' }}</el-descriptions-item>
<el-descriptions-item label="是否有经营佐证">{{
formatBoolean(detailData.bizProof)
@@ -114,6 +114,7 @@
import {setExecuteRate} from "@/api/loanPricing/workflow"
import ModelOutputDisplay from "./ModelOutputDisplay.vue"
import BargainingPoolDisplay from "./BargainingPoolDisplay.vue"
import {formatRate} from "@/utils/rate"
export default {
name: "PersonalWorkflowDetail",
@@ -144,12 +145,13 @@ export default {
watch: {
'detailData.executeRate': {
handler(newVal) {
this.executeRateInput = newVal || ''
this.executeRateInput = formatRate(newVal, '')
},
immediate: true
}
},
methods: {
formatRate,
/** 格式化布尔值为中文 */
formatBoolean(value) {
if (value === 'true' || value === true || value === '1' || value === 1) return '是'
@@ -164,7 +166,7 @@ export default {
},
/** 获取基准利率 */
getBaseLoanRate() {
return this.retailOutput?.baseLoanRate || '-'
return this.formatRate(this.retailOutput?.baseLoanRate)
},
/** 获取浮动BP */
getTotalBp() {
@@ -172,7 +174,7 @@ export default {
},
/** 获取最终测算利率 */
getCalculateRate() {
return this.retailOutput?.finalCalculateRate || '-'
return this.formatRate(this.retailOutput?.finalCalculateRate)
},
/** 设定执行利率 */
handleSetExecuteRate() {

View File

@@ -3,25 +3,39 @@
<!-- 页面头部标题和返回按钮 -->
<div class="page-header">
<h2 class="page-title">流程详情</h2>
<el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button>
<div class="page-actions">
<el-button
class="workflow-print-button"
icon="el-icon-printer"
size="small"
type="primary"
:disabled="loading || !workflowDetail"
@click="handlePrint"
>
打印
</el-button>
<el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button>
</div>
</div>
<!-- 根据客户类型渲染对应的详情组件 -->
<personal-workflow-detail
v-if="!loading && workflowDetail && workflowDetail.custType === '个人'"
:detail-data="workflowDetail"
:retail-output="retailOutput"
:bargaining-pool="bargainingPool"
@refresh="getDetail"
/>
<div class="workflow-print-area">
<!-- 根据客户类型渲染对应的详情组件 -->
<personal-workflow-detail
v-if="!loading && workflowDetail && workflowDetail.custType === '个人'"
:detail-data="workflowDetail"
:retail-output="retailOutput"
:bargaining-pool="bargainingPool"
@refresh="getDetail"
/>
<corporate-workflow-detail
v-if="!loading && workflowDetail && workflowDetail.custType === '企业'"
:detail-data="workflowDetail"
:corp-output="corpOutput"
:bargaining-pool="bargainingPool"
@refresh="getDetail"
/>
<corporate-workflow-detail
v-if="!loading && workflowDetail && workflowDetail.custType === '企业'"
:detail-data="workflowDetail"
:corp-output="corpOutput"
:bargaining-pool="bargainingPool"
@refresh="getDetail"
/>
</div>
</div>
</template>
@@ -72,6 +86,37 @@ export default {
/** 返回上一页 */
goBack() {
this.$router.go(-1)
},
/** 打印流程详情 */
handlePrint() {
this.$nextTick(() => {
const originalTitle = document.title
document.title = this.buildPrintFileName()
window.print()
document.title = originalTitle
})
},
/** 生成打印文件名 */
buildPrintFileName() {
const custName = this.sanitizePrintFileName(this.workflowDetail?.custName || '流程详情')
return `${custName}_${this.formatPrintTimestamp(new Date())}`
},
/** 格式化打印时间戳 */
formatPrintTimestamp(date) {
const pad = value => String(value).padStart(2, '0')
return [
date.getFullYear(),
pad(date.getMonth() + 1),
pad(date.getDate())
].join('') + [
pad(date.getHours()),
pad(date.getMinutes()),
pad(date.getSeconds())
].join('')
},
/** 清理文件名中的非法字符 */
sanitizePrintFileName(value) {
return String(value).replace(/[\\/:*?"<>|]/g, '').trim() || '流程详情'
}
}
}
@@ -92,6 +137,96 @@ export default {
font-weight: 500;
color: #303133;
}
.page-actions {
display: flex;
align-items: center;
gap: 8px;
}
}
}
</style>
<style lang="scss">
@media print {
@page {
size: A4;
margin: 12mm;
}
body {
background: #fff !important;
}
.sidebar-container,
.fixed-header,
.navbar,
.tags-view-container,
.page-actions,
.el-loading-mask,
.execute-rate-input-wrapper .el-button {
display: none !important;
}
.main-container,
.app-main,
.app-container,
.workflow-detail-container,
.workflow-print-area {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
max-width: none !important;
min-height: auto !important;
overflow: visible !important;
}
.workflow-detail-container .page-header {
display: block !important;
margin-bottom: 12px !important;
padding: 0 !important;
}
.workflow-detail-container .page-title {
font-size: 20px !important;
font-weight: 600 !important;
color: #000 !important;
}
.personal-workflow-detail .detail-layout,
.corporate-workflow-detail .detail-layout {
display: block !important;
}
.personal-workflow-detail .left-panel,
.corporate-workflow-detail .left-panel,
.personal-workflow-detail .right-panel,
.corporate-workflow-detail .right-panel {
width: 100% !important;
max-width: none !important;
flex: none !important;
}
.el-card {
break-inside: avoid;
page-break-inside: avoid;
box-shadow: none !important;
border: 1px solid #dcdfe6 !important;
margin-bottom: 12px !important;
}
.el-card__header {
background: #f5f7fa !important;
}
.el-descriptions {
page-break-inside: avoid;
}
.el-input__inner,
.el-input-group__append {
border-color: #dcdfe6 !important;
color: #000 !important;
}
}
</style>

View File

@@ -44,21 +44,30 @@
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="workflowList">
<el-table-column label="业务方流水号" align="center" prop="serialNum" width="180" :show-overflow-tooltip="true" />
<el-table-column label="客户名称" align="center" prop="custName" :show-overflow-tooltip="true" />
<el-table-column label="客户类型" align="center" prop="custType" width="100" />
<el-table-column label="担保方式" align="center" prop="guarType" width="100" />
<el-table-column label="申请金额(元)" align="center" prop="applyAmt" width="120" />
<el-table-column label="测算利率(%)" align="center" prop="calculateRate" width="100" />
<el-table-column label="执行利率(%)" align="center" prop="executeRate" width="100" />
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<el-table v-loading="loading" :data="workflowList" class="workflow-table" style="width: 100%">
<el-table-column label="业务方流水号" align="center" prop="serialNum" min-width="320" class-name="workflow-important-column" />
<el-table-column label="客户内码" align="center" prop="custIsn" min-width="320" class-name="workflow-important-column" />
<el-table-column label="客户名称" align="center" prop="custName" min-width="240" class-name="workflow-important-column" />
<el-table-column label="客户类型" align="center" prop="custType" min-width="110" />
<el-table-column label="担保方式" align="center" prop="guarType" min-width="120" />
<el-table-column label="申请金额(元)" align="center" prop="applyAmt" min-width="140" />
<el-table-column label="最终测算利率(%)" align="center" prop="calculateRate" min-width="150">
<template slot-scope="scope">
<span>{{ formatRate(scope.row.calculateRate) }}</span>
</template>
</el-table-column>
<el-table-column label="执行利率(%)" align="center" prop="executeRate" min-width="120">
<template slot-scope="scope">
<span>{{ formatRate(scope.row.executeRate) }}</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" min-width="170">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="创建者" align="center" prop="createBy" width="120" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="创建者" align="center" prop="createBy" min-width="220" class-name="workflow-important-column" />
<el-table-column label="操作" align="center" fixed="right" min-width="150" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
@@ -66,6 +75,13 @@
icon="el-icon-view"
@click="handleView(scope.row)"
>查看</el-button>
<el-button
v-if="canEdit(scope.row)"
size="mini"
type="text"
icon="el-icon-edit"
@click="handleEdit(scope.row)"
>编辑</el-button>
</template>
</el-table-column>
</el-table>
@@ -92,6 +108,7 @@
<personal-create-dialog
:visible.sync="showPersonalDialog"
:customer-map="selectedCustomerMap"
:edit-data="editWorkflow"
@success="handleCreateSuccess"
/>
@@ -99,17 +116,20 @@
<corporate-create-dialog
:visible.sync="showCorporateDialog"
:customer-map="selectedCustomerMap"
:edit-data="editWorkflow"
@success="handleCreateSuccess"
/>
</div>
</template>
<script>
import {listWorkflow} from "@/api/loanPricing/workflow"
import {getWorkflowEdit, listWorkflow} from "@/api/loanPricing/workflow"
import {mapGetters} from "vuex"
import CustomerTypeSelector from "./components/CustomerTypeSelector"
import CustomerMapSelector from "./components/CustomerMapSelector"
import PersonalCreateDialog from "./components/PersonalCreateDialog"
import CorporateCreateDialog from "./components/CorporateCreateDialog"
import {formatRate} from "@/utils/rate"
export default {
name: "LoanPricingWorkflow",
@@ -137,6 +157,8 @@ export default {
selectedCustomerType: undefined,
// 当前选择的客户号映射记录
selectedCustomerMap: null,
// 当前编辑的流程记录
editWorkflow: null,
// 是否显示个人客户创建弹出层
showPersonalDialog: false,
// 是否显示企业客户创建弹出层
@@ -154,6 +176,15 @@ export default {
created() {
this.getList()
},
computed: {
...mapGetters([
'name',
'nickName'
]),
currentCreateBy() {
return `${this.nickName}-${this.name}`
}
},
activated() {
this.getList()
},
@@ -170,6 +201,7 @@ export default {
}
},
methods: {
formatRate,
/** 查询利率定价流程列表 */
getList() {
this.loading = true
@@ -196,8 +228,26 @@ export default {
params: { serialNum: row.serialNum }
})
},
/** 是否允许编辑 */
canEdit(row) {
return row && row.createBy === this.currentCreateBy
},
/** 编辑操作 */
handleEdit(row) {
getWorkflowEdit(row.serialNum).then(response => {
this.editWorkflow = response.data
this.selectedCustomerMap = null
this.selectedCustomerType = undefined
if (this.editWorkflow.custType === '个人') {
this.showPersonalDialog = true
} else if (this.editWorkflow.custType === '企业') {
this.showCorporateDialog = true
}
})
},
/** 新增按钮操作 */
handleAdd() {
this.editWorkflow = null
this.showTypeSelector = true
},
/** 选择客户类型回调 */
@@ -208,6 +258,7 @@ export default {
},
/** 选择客户号映射记录回调 */
handleCustomerMapSelect(row) {
this.editWorkflow = null
this.selectedCustomerMap = row
if (this.selectedCustomerType === 'personal') {
this.showPersonalDialog = true
@@ -224,7 +275,17 @@ export default {
clearSelectedCustomer() {
this.selectedCustomerMap = null
this.selectedCustomerType = undefined
this.editWorkflow = null
}
}
}
</script>
<style scoped>
.workflow-table ::v-deep .workflow-important-column .cell {
overflow: hidden;
text-overflow: clip;
white-space: nowrap;
word-break: normal;
}
</style>

View File

@@ -1,7 +1,6 @@
<template>
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{title}}</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"

View File

@@ -95,7 +95,7 @@
</el-row>
<!-- 添加或修改用户配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
<el-dialog :title="title" :visible.sync="open" width="80%" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-row>
<el-col :span="12">
@@ -562,4 +562,4 @@ export default {
}
}
}
</script>
</script>

View File

@@ -10,8 +10,19 @@ const personalCreateDialog = read('src/views/loanPricing/workflow/components/Per
const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
assert(
personalCreateDialog.includes('label="贷款用途"') && personalCreateDialog.includes('prop="loanPurpose"'),
'个人新增弹窗缺少贷款用途字段'
personalCreateDialog.includes('updatePersonalWorkflow') &&
personalCreateDialog.includes("this.isEdit ? '编辑个人利率定价流程' : '新增个人利率定价流程'") &&
personalCreateDialog.includes('editData.serialNum') &&
personalCreateDialog.includes('updatePersonalWorkflow(this.editData.serialNum, data)') &&
personalCreateDialog.includes(': createPersonalWorkflow(data)'),
'个人弹窗应支持编辑模式回显并调用个人更新接口'
)
assert(
!personalCreateDialog.includes('label="贷款用途"') &&
!personalCreateDialog.includes('prop="loanPurpose"') &&
!personalCreateDialog.includes('loanPurpose:'),
'个人新增弹窗不应继续保留贷款用途字段'
)
assert(
@@ -20,8 +31,8 @@ assert(
)
assert(
personalCreateDialog.includes("value=\"consumer\"") && personalCreateDialog.includes("value=\"business\""),
'个人新增弹窗缺少贷款用途选项'
!personalCreateDialog.includes("value=\"consumer\"") && !personalCreateDialog.includes("value=\"business\""),
'个人新增弹窗不应继续保留贷款用途选项'
)
assert(
@@ -41,19 +52,20 @@ assert(
assert(
personalCreateDialog.includes('collateralTypeOptions') &&
personalCreateDialog.includes("return ['一类', '二类', '三类', '四类', '其他']") &&
personalCreateDialog.includes("return ['存单质押', '其他']") &&
!personalCreateDialog.includes('label="一线"'),
personalCreateDialog.includes("return ['一线', '一类', '二类', '三类']") &&
personalCreateDialog.includes("return ['存单质押', '其他质押']"),
'个人新增弹窗抵质押类型选项未按担保方式动态切换'
)
assert(
!personalCreateDialog.includes('{required: true, message: "请选择抵质押类型", trigger: "change"}'),
'个人新增弹窗仍将抵质押类型为必填'
personalCreateDialog.includes('{required: true, message: "请选择抵质押类型", trigger: "change"}'),
'个人新增弹窗抵质押类型为必填'
)
assert(
personalCreateDialog.includes("bizProof: this.form.bizProof ? '1' : '0'") &&
!personalCreateDialog.includes('label="是否有经营佐证"') &&
!personalCreateDialog.includes('prop="bizProof"') &&
!personalCreateDialog.includes('bizProof:') &&
personalCreateDialog.includes("loanLoop: this.form.loanLoop ? '1' : '0'") &&
personalCreateDialog.includes("data.collThirdParty = this.form.collThirdParty ? '1' : '0'") &&
personalCreateDialog.includes('delete data.collType') &&

View File

@@ -20,7 +20,18 @@ function loadComponentOptions(filePath) {
}
const stubImports = importNames.map(name => `const ${name} = {};`).join('\n')
const transformed = `${stubImports}\n${scriptMatch[1]}`
const namedImportStubs = `
const listWorkflow = function() {};
const getWorkflowEdit = function() {};
const formatRate = function(value) { return value; };
const mapGetters = function(names) {
const result = {};
names.forEach((name) => {
result[name] = function() { return ''; };
});
return result;
};`
const transformed = `${stubImports}\n${namedImportStubs}\n${scriptMatch[1]}`
.replace(/^import .*$/gm, '')
.replace(/export default/, 'module.exports =')
@@ -37,8 +48,13 @@ function loadComponentOptions(filePath) {
const filePath = path.resolve(__dirname, '../src/views/loanPricing/workflow/index.vue')
const component = loadComponentOptions(filePath)
const source = fs.readFileSync(filePath, 'utf8')
assert.strictEqual(typeof component.activated, 'function', '流程列表页应在激活时刷新数据')
assert(source.includes('icon="el-icon-edit"') && source.includes('>编辑</el-button>'), '流程列表操作列应包含编辑按钮')
assert(source.includes('v-if="canEdit(scope.row)"'), '流程列表编辑按钮应只对可编辑行显示')
assert(source.includes('row.createBy === this.currentCreateBy'), '流程列表应按创建者判断编辑按钮权限')
assert(source.includes('getWorkflowEdit(row.serialNum)'), '编辑操作应先查询编辑数据')
let refreshCount = 0
component.activated.call({
@@ -49,4 +65,16 @@ component.activated.call({
assert.strictEqual(refreshCount, 1, '流程列表页激活时应调用一次 getList')
assert.strictEqual(component.methods.canEdit.call({
currentCreateBy: '张三-8920001'
}, {
createBy: '张三-8920001'
}), true, '创建者应允许编辑')
assert.strictEqual(component.methods.canEdit.call({
currentCreateBy: '张三-8920001'
}, {
createBy: '李四-8920002'
}), false, '非创建者不应允许编辑')
console.log('workflow-index-refresh test passed')

View File

@@ -0,0 +1,3 @@
-- 上虞利率定价存单票面利率字段
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率' AFTER `loan_rate_history`;

View File

@@ -0,0 +1,5 @@
-- 为流程表增加创建人部门ID用于支行管理员流程列表数据权限
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `dept_id` bigint(20) DEFAULT NULL COMMENT '创建人部门ID' AFTER `is_inclusive_finance`;
CREATE INDEX `idx_dept_id` ON `loan_pricing_workflow` (`dept_id`);

View File

@@ -0,0 +1,286 @@
-- 根据 /Users/wkc/Downloads/892.xlsx Sheet1 生成 sys_user_role 角色关系
-- 生成日期: 2026-05-20
-- Excel 数据行数: 257
-- 角色分布: 管理员 4, 支行管理员 37, 客户经理 213
-- 重复柜员号按支行管理员处理,不再插入客户经理角色。
-- user_id 取值规则: Excel 柜员号 -> sys_user.user_name -> sys_user.user_id。
-- 本脚本不创建临时表,三条 INSERT 语句分别插入三种角色。
-- 执行前需确保 sys_user 已按柜员号写入 user_namesys_role 已存在对应角色名。
-- 插入管理员角色关系,共 4 行
INSERT IGNORE INTO sys_user_role (user_id, role_id)
SELECT DISTINCT u.user_id, r.role_id
FROM (
SELECT '8925071' AS teller_no -- Excel 第 255 行,吴津江,机构 892000
UNION ALL SELECT '8923774' -- Excel 第 256 行,陈夏超,机构 892000
UNION ALL SELECT '8925041' -- Excel 第 257 行,鲁琳洁,机构 892000
UNION ALL SELECT '8923490' -- Excel 第 258 行,王赛金,机构 892000
) src
JOIN sys_user u ON u.user_name = src.teller_no AND u.del_flag = '0'
JOIN sys_role r ON r.role_name = '管理员' AND r.status = '0' AND r.del_flag = '0';
-- 插入支行管理员角色关系,共 37 行
INSERT IGNORE INTO sys_user_role (user_id, role_id)
SELECT DISTINCT u.user_id, r.role_id
FROM (
SELECT '8922653' AS teller_no -- Excel 第 2 行,赵健君,机构 892160
UNION ALL SELECT '8922319' -- Excel 第 3 行,俞宏,机构 892200
UNION ALL SELECT '8923428' -- Excel 第 4 行,胡海松,机构 892060
UNION ALL SELECT '8922185' -- Excel 第 5 行,朱江龙,机构 892110
UNION ALL SELECT '8923393' -- Excel 第 6 行,常斌斌,机构 892020
UNION ALL SELECT '8922645' -- Excel 第 7 行,陈勇毅,机构 892100
UNION ALL SELECT '8923511' -- Excel 第 8 行,胡卓奇,机构 892170
UNION ALL SELECT '8923706' -- Excel 第 9 行,夏俊杰,机构 892190
UNION ALL SELECT '8927547' -- Excel 第 10 行,鲁文倬,机构 892130
UNION ALL SELECT '8923370' -- Excel 第 11 行,杨力,机构 892050
UNION ALL SELECT '8923612' -- Excel 第 12 行,沈萍,机构 892220
UNION ALL SELECT '8923105' -- Excel 第 13 行,王忠,机构 892070
UNION ALL SELECT '8923326' -- Excel 第 14 行,陈燕,机构 892140
UNION ALL SELECT '8922341' -- Excel 第 15 行,王丽娜,机构 892210
UNION ALL SELECT '8922250' -- Excel 第 16 行,孙银峰,机构 892030
UNION ALL SELECT '8923475' -- Excel 第 17 行,张绒,机构 892010
UNION ALL SELECT '8923249' -- Excel 第 18 行,朱珊珊,机构 892090
UNION ALL SELECT '8923244' -- Excel 第 19 行,汤琴华,机构 892080
UNION ALL SELECT '8923215' -- Excel 第 113 行,徐国栋,机构 892100
UNION ALL SELECT '8923351' -- Excel 第 114 行,刘春燕,机构 892050
UNION ALL SELECT '8923384' -- Excel 第 115 行,马钦,机构 892090
UNION ALL SELECT '8922686' -- Excel 第 116 行,许训华,机构 892020
UNION ALL SELECT '8922557' -- Excel 第 117 行,裘朝山,机构 892220
UNION ALL SELECT '8923504' -- Excel 第 118 行,陈俊杰,机构 892170
UNION ALL SELECT '8922344' -- Excel 第 119 行,徐浩锋,机构 892110
UNION ALL SELECT '8923510' -- Excel 第 120 行,陆波,机构 892130
UNION ALL SELECT '8922667' -- Excel 第 121 行,徐华源,机构 892080
UNION ALL SELECT '8923190' -- Excel 第 122 行,倪舟军,机构 892160
UNION ALL SELECT '8922309' -- Excel 第 123 行,刘炯,机构 892210
UNION ALL SELECT '8922630' -- Excel 第 124 行,李文江,机构 892200
UNION ALL SELECT '8922257' -- Excel 第 125 行,傅泽民,机构 892080
UNION ALL SELECT '8928849' -- Excel 第 126 行,赵红江,机构 892030
UNION ALL SELECT '8923354' -- Excel 第 127 行,马斯,机构 892060
UNION ALL SELECT '8923102' -- Excel 第 128 行,徐峰,机构 892010
UNION ALL SELECT '8922534' -- Excel 第 129 行,谢铭杰,机构 892170
UNION ALL SELECT '8923229' -- Excel 第 130 行,杜铜方,机构 892190
UNION ALL SELECT '8921245' -- Excel 第 131 行,徐红玲,机构 892140
) src
JOIN sys_user u ON u.user_name = src.teller_no AND u.del_flag = '0'
JOIN sys_role r ON r.role_name = '支行管理员' AND r.status = '0' AND r.del_flag = '0';
-- 插入客户经理角色关系,共 213 行
INSERT IGNORE INTO sys_user_role (user_id, role_id)
SELECT DISTINCT u.user_id, r.role_id
FROM (
SELECT '8923387' AS teller_no -- Excel 第 20 行,倪嫣倩,机构 892020
UNION ALL SELECT '8923976' -- Excel 第 21 行,俞丽佳,机构 892020
UNION ALL SELECT '8922535' -- Excel 第 22 行,金军勇,机构 892142
UNION ALL SELECT '8923566' -- Excel 第 23 行,范水月,机构 892213
UNION ALL SELECT '8923179' -- Excel 第 24 行,章恩耀,机构 892014
UNION ALL SELECT '8923388' -- Excel 第 25 行,陈立波,机构 892080
UNION ALL SELECT '8928990' -- Excel 第 27 行,郑永锋,机构 892200
UNION ALL SELECT '8925312' -- Excel 第 29 行,俞一凡,机构 892020
UNION ALL SELECT '8922316' -- Excel 第 30 行,陈建囡,机构 892080
UNION ALL SELECT '8923253' -- Excel 第 31 行,潘烨,机构 892214
UNION ALL SELECT '8923188' -- Excel 第 32 行,卢丰,机构 892140
UNION ALL SELECT '8923872' -- Excel 第 33 行,戴魏杰,机构 892050
UNION ALL SELECT '8923478' -- Excel 第 34 行,陈泽宇,机构 892102
UNION ALL SELECT '8923315' -- Excel 第 35 行,赵政,机构 892010
UNION ALL SELECT '8925714' -- Excel 第 36 行,陈承,机构 892030
UNION ALL SELECT '8923752' -- Excel 第 37 行,金佳裕,机构 892160
UNION ALL SELECT '8922227' -- Excel 第 38 行,谢冬青,机构 892050
UNION ALL SELECT '8925040' -- Excel 第 39 行,谢顺达,机构 892193
UNION ALL SELECT '8923242' -- Excel 第 40 行,夏德鑫,机构 892030
UNION ALL SELECT '8922392' -- Excel 第 41 行,阮寺均,机构 892100
UNION ALL SELECT '8923327' -- Excel 第 42 行,屠超众,机构 892110
UNION ALL SELECT '8923223' -- Excel 第 43 行,沈佳敏,机构 892022
UNION ALL SELECT '8923309' -- Excel 第 44 行,徐岳锋,机构 892070
UNION ALL SELECT '8923561' -- Excel 第 45 行,赵斌,机构 892220
UNION ALL SELECT '8923421' -- Excel 第 46 行,王燕薇,机构 892210
UNION ALL SELECT '8923324' -- Excel 第 47 行,孙绍平,机构 892170
UNION ALL SELECT '8923766' -- Excel 第 48 行,孙章丽,机构 892021
UNION ALL SELECT '8923429' -- Excel 第 49 行,袁叶晨,机构 892192
UNION ALL SELECT '8923262' -- Excel 第 50 行,黄滨,机构 892111
UNION ALL SELECT '8923838' -- Excel 第 51 行,潘晋洋,机构 892011
UNION ALL SELECT '8923317' -- Excel 第 52 行,朱鋆,机构 892051
UNION ALL SELECT '8923271' -- Excel 第 53 行,陈军,机构 892060
UNION ALL SELECT '8923981' -- Excel 第 54 行,王鋆盈,机构 892200
UNION ALL SELECT '8922473' -- Excel 第 55 行,徐伟达,机构 892170
UNION ALL SELECT '8922323' -- Excel 第 56 行,赵杰,机构 892141
UNION ALL SELECT '8923799' -- Excel 第 57 行,赵焱,机构 892090
UNION ALL SELECT '8922399' -- Excel 第 58 行,陈烈,机构 892010
UNION ALL SELECT '8923379' -- Excel 第 59 行,金鑫,机构 892191
UNION ALL SELECT '8923101' -- Excel 第 60 行,娄瑛,机构 892030
UNION ALL SELECT '8923773' -- Excel 第 61 行,叶鑫,机构 892090
UNION ALL SELECT '8922625' -- Excel 第 62 行,李银钢,机构 892140
UNION ALL SELECT '8923871' -- Excel 第 63 行,高飞雍,机构 892082
UNION ALL SELECT '8925534' -- Excel 第 64 行,龚泽烨,机构 892010
UNION ALL SELECT '8925220' -- Excel 第 65 行,陈方正,机构 892166
UNION ALL SELECT '8923820' -- Excel 第 67 行,徐金燕,机构 892170
UNION ALL SELECT '8923104' -- Excel 第 68 行,阮钟威,机构 892160
UNION ALL SELECT '8923278' -- Excel 第 69 行,楼建灿,机构 892200
UNION ALL SELECT '8923435' -- Excel 第 70 行,王金锦,机构 892035
UNION ALL SELECT '8923501' -- Excel 第 71 行,范少材,机构 892041
UNION ALL SELECT '8923848' -- Excel 第 72 行,沈燕燕,机构 892220
UNION ALL SELECT '8923231' -- Excel 第 73 行,张何,机构 892161
UNION ALL SELECT '8923368' -- Excel 第 74 行,王钗钗,机构 892133
UNION ALL SELECT '8923816' -- Excel 第 75 行,陈姝莹,机构 892101
UNION ALL SELECT '8923209' -- Excel 第 76 行,徐敏敏,机构 892050
UNION ALL SELECT '8923549' -- Excel 第 77 行,陆颖琳,机构 892130
UNION ALL SELECT '8923118' -- Excel 第 78 行,邵琴,机构 892172
UNION ALL SELECT '8923914' -- Excel 第 79 行,陆佳英,机构 892226
UNION ALL SELECT '8921232' -- Excel 第 80 行,陈曙光,机构 892010
UNION ALL SELECT '8923747' -- Excel 第 81 行,王祝芳,机构 892060
UNION ALL SELECT '8923254' -- Excel 第 82 行,章霄,机构 892030
UNION ALL SELECT '8923434' -- Excel 第 83 行,姚明权,机构 892200
UNION ALL SELECT '8923502' -- Excel 第 84 行,邵钰凤,机构 892021
UNION ALL SELECT '8923390' -- Excel 第 85 行,谢羽青,机构 892170
UNION ALL SELECT '8923550' -- Excel 第 86 行,俞李鲲,机构 892211
UNION ALL SELECT '8922425' -- Excel 第 87 行,罗国杰,机构 892100
UNION ALL SELECT '8923540' -- Excel 第 88 行,王帅,机构 892190
UNION ALL SELECT '8923224' -- Excel 第 89 行,应振宇,机构 892020
UNION ALL SELECT '8923894' -- Excel 第 90 行,任鹏飞,机构 892021
UNION ALL SELECT '8923222' -- Excel 第 91 行,朱烨,机构 892010
UNION ALL SELECT '8923357' -- Excel 第 92 行,陈滟,机构 892110
UNION ALL SELECT '8923812' -- Excel 第 93 行,周虹,机构 892083
UNION ALL SELECT '8922167' -- Excel 第 94 行,魏铁柱,机构 892182
UNION ALL SELECT '8922248' -- Excel 第 95 行,吴智光,机构 892070
UNION ALL SELECT '8922247' -- Excel 第 96 行,吴俊勋,机构 892060
UNION ALL SELECT '8923383' -- Excel 第 97 行,王宽,机构 892140
UNION ALL SELECT '8923350' -- Excel 第 98 行,徐骏妮,机构 892200
UNION ALL SELECT '8923352' -- Excel 第 99 行,韩镭,机构 892146
UNION ALL SELECT '8923861' -- Excel 第 100 行,严禹彬,机构 892160
UNION ALL SELECT '8922743' -- Excel 第 101 行,刘建军,机构 892130
UNION ALL SELECT '8923963' -- Excel 第 102 行,王一,机构 892030
UNION ALL SELECT '8923922' -- Excel 第 103 行,徐鹤立,机构 892113
UNION ALL SELECT '8925176' -- Excel 第 104 行,徐家栋,机构 892210
UNION ALL SELECT '8922258' -- Excel 第 105 行,朱蓉蓉,机构 892161
UNION ALL SELECT '8923247' -- Excel 第 106 行,王陈炯,机构 892031
UNION ALL SELECT '8923117' -- Excel 第 107 行,黄辉,机构 892223
UNION ALL SELECT '8923929' -- Excel 第 108 行,俞佳娜,机构 892090
UNION ALL SELECT '8925461' -- Excel 第 109 行,厉佳贝,机构 892224
UNION ALL SELECT '8923751' -- Excel 第 110 行,劳泽涛,机构 892132
UNION ALL SELECT '8923458' -- Excel 第 111 行,徐泽钏,机构 892145
UNION ALL SELECT '8923415' -- Excel 第 112 行,吴璟琳,机构 892200
UNION ALL SELECT '8922687' -- Excel 第 132 行,张蓉,机构 892133
UNION ALL SELECT '8923320' -- Excel 第 133 行,方一锋,机构 892021
UNION ALL SELECT '8923046' -- Excel 第 134 行,吴晓萍,机构 892012
UNION ALL SELECT '8923363' -- Excel 第 135 行,朱晖,机构 892223
UNION ALL SELECT '8925213' -- Excel 第 136 行,何佳文,机构 892014
UNION ALL SELECT '8923810' -- Excel 第 137 行,金彬,机构 892083
UNION ALL SELECT '8925535' -- Excel 第 138 行,何志琦,机构 892060
UNION ALL SELECT '8925532' -- Excel 第 139 行,董栋,机构 892110
UNION ALL SELECT '8923256' -- Excel 第 140 行,黄勇,机构 892060
UNION ALL SELECT '8925159' -- Excel 第 141 行,吴学舟,机构 892145
UNION ALL SELECT '8925462' -- Excel 第 142 行,李佳彬,机构 892146
UNION ALL SELECT '8923493' -- Excel 第 143 行,张磊,机构 892060
UNION ALL SELECT '8923599' -- Excel 第 144 行,徐江琴,机构 892210
UNION ALL SELECT '8925208' -- Excel 第 145 行,潘列当,机构 892110
UNION ALL SELECT '8925539' -- Excel 第 146 行,秦佳浩,机构 892100
UNION ALL SELECT '8922824' -- Excel 第 147 行,许伟松,机构 892170
UNION ALL SELECT '8923477' -- Excel 第 148 行,陈淑清,机构 892213
UNION ALL SELECT '8925393' -- Excel 第 149 行,余恒涛,机构 892190
UNION ALL SELECT '8923865' -- Excel 第 150 行,金洁琼,机构 892210
UNION ALL SELECT '8923920' -- Excel 第 151 行,华盛榆,机构 892183
UNION ALL SELECT '8923239' -- Excel 第 152 行,赵宏,机构 892213
UNION ALL SELECT '8923369' -- Excel 第 153 行,陈丽君,机构 892050
UNION ALL SELECT '8923541' -- Excel 第 154 行,赵珊,机构 892220
UNION ALL SELECT '8925419' -- Excel 第 155 行,阮昕岚,机构 892014
UNION ALL SELECT '8923119' -- Excel 第 156 行,李莹,机构 892035
UNION ALL SELECT '8925596' -- Excel 第 157 行,郑炫奇,机构 892214
UNION ALL SELECT '8925544' -- Excel 第 158 行,徐洁汀,机构 892090
UNION ALL SELECT '8923723' -- Excel 第 159 行,王未,机构 892200
UNION ALL SELECT '8923974' -- Excel 第 160 行,余菲菲,机构 892140
UNION ALL SELECT '8923171' -- Excel 第 161 行,陈洲焰,机构 892101
UNION ALL SELECT '8922199' -- Excel 第 162 行,章国栋,机构 892223
UNION ALL SELECT '8923418' -- Excel 第 163 行,阮俊杰,机构 892030
UNION ALL SELECT '8923708' -- Excel 第 164 行,严思培,机构 892050
UNION ALL SELECT '8923607' -- Excel 第 165 行,刘明亮,机构 892160
UNION ALL SELECT '8923839' -- Excel 第 166 行,董生超,机构 892161
UNION ALL SELECT '8921175' -- Excel 第 167 行,俞曙光,机构 892041
UNION ALL SELECT '8923308' -- Excel 第 168 行,叶超,机构 892172
UNION ALL SELECT '8925564' -- Excel 第 169 行,张镇涛,机构 892083
UNION ALL SELECT '8925067' -- Excel 第 170 行,袁江锋,机构 892211
UNION ALL SELECT '8923531' -- Excel 第 171 行,夏威,机构 892200
UNION ALL SELECT '8923813' -- Excel 第 172 行,赵佳伟,机构 892080
UNION ALL SELECT '8923371' -- Excel 第 173 行,朱益浪,机构 892166
UNION ALL SELECT '8923979' -- Excel 第 174 行,徐嘉婕,机构 892021
UNION ALL SELECT '8925086' -- Excel 第 175 行,陈坚,机构 892051
UNION ALL SELECT '8922166' -- Excel 第 176 行,任静钰,机构 892210
UNION ALL SELECT '8923585' -- Excel 第 177 行,金鹏飞,机构 892200
UNION ALL SELECT '8923548' -- Excel 第 178 行,阮锦薇,机构 892161
UNION ALL SELECT '8925177' -- Excel 第 179 行,严翔,机构 892166
UNION ALL SELECT '8925420' -- Excel 第 180 行,郑清枫,机构 892022
UNION ALL SELECT '8925217' -- Excel 第 181 行,屠林洁,机构 892030
UNION ALL SELECT '8922462' -- Excel 第 182 行,何文达,机构 892226
UNION ALL SELECT '8923551' -- Excel 第 183 行,陈文波,机构 892132
UNION ALL SELECT '8923587' -- Excel 第 184 行,徐霜霜,机构 892211
UNION ALL SELECT '8923823' -- Excel 第 185 行,董佳杨,机构 892022
UNION ALL SELECT '8923377' -- Excel 第 186 行,魏子涵,机构 892200
UNION ALL SELECT '8922192' -- Excel 第 187 行,金智煌,机构 892223
UNION ALL SELECT '8923654' -- Excel 第 188 行,杜圣磊,机构 892010
UNION ALL SELECT '8925152' -- Excel 第 189 行,阮益权,机构 892192
UNION ALL SELECT '8923580' -- Excel 第 190 行,金陈坷,机构 892170
UNION ALL SELECT '8925311' -- Excel 第 191 行,章天豪,机构 892100
UNION ALL SELECT '8925157' -- Excel 第 192 行,潘彬彬,机构 892080
UNION ALL SELECT '8925068' -- Excel 第 193 行,沈天阳,机构 892161
UNION ALL SELECT '8923689' -- Excel 第 194 行,傅俊杰,机构 892224
UNION ALL SELECT '8923874' -- Excel 第 195 行,连郡,机构 892142
UNION ALL SELECT '8923604' -- Excel 第 196 行,顾涵,机构 892142
UNION ALL SELECT '8923899' -- Excel 第 197 行,杜燕燕,机构 892160
UNION ALL SELECT '8923986' -- Excel 第 198 行,阮婷,机构 892090
UNION ALL SELECT '8922523' -- Excel 第 199 行,金强,机构 892010
UNION ALL SELECT '8925630' -- Excel 第 200 行,何一东,机构 892070
UNION ALL SELECT '8925385' -- Excel 第 201 行,陈浩,机构 892160
UNION ALL SELECT '8923579' -- Excel 第 202 行,吴滢榆,机构 892102
UNION ALL SELECT '8923629' -- Excel 第 203 行,王洁,机构 892200
UNION ALL SELECT '8925478' -- Excel 第 204 行,叶铃君,机构 892140
UNION ALL SELECT '8923537' -- Excel 第 205 行,章吉丽,机构 892031
UNION ALL SELECT '8925215' -- Excel 第 206 行,姚橙枫,机构 892090
UNION ALL SELECT '8925038' -- Excel 第 207 行,曹铠宇,机构 892011
UNION ALL SELECT '8923219' -- Excel 第 208 行,许蕾,机构 892011
UNION ALL SELECT '8923836' -- Excel 第 209 行,徐宏来,机构 892050
UNION ALL SELECT '8925128' -- Excel 第 210 行,陈梦宇,机构 892070
UNION ALL SELECT '8923902' -- Excel 第 211 行,徐玉玮,机构 892030
UNION ALL SELECT '8923258' -- Excel 第 212 行,严金锋,机构 892132
UNION ALL SELECT '8923124' -- Excel 第 213 行,徐雪荣,机构 892191
UNION ALL SELECT '8922187' -- Excel 第 214 行,沈颖冲,机构 892100
UNION ALL SELECT '8925604' -- Excel 第 215 行,陈一鸣,机构 892166
UNION ALL SELECT '8923627' -- Excel 第 216 行,徐佳斌,机构 892193
UNION ALL SELECT '8923703' -- Excel 第 217 行,王河杰,机构 892080
UNION ALL SELECT '8923211' -- Excel 第 218 行,龚世炎,机构 892012
UNION ALL SELECT '8923647' -- Excel 第 219 行,沈冰妮,机构 892145
UNION ALL SELECT '8925392' -- Excel 第 220 行,任琳,机构 892020
UNION ALL SELECT '8923260' -- Excel 第 221 行,郑芦康,机构 892050
UNION ALL SELECT '8925206' -- Excel 第 222 行,刘威,机构 892020
UNION ALL SELECT '8925212' -- Excel 第 223 行,俞洁,机构 892101
UNION ALL SELECT '8925427' -- Excel 第 224 行,阮天宇,机构 892020
UNION ALL SELECT '8925028' -- Excel 第 225 行,李益斐,机构 892133
UNION ALL SELECT '8923567' -- Excel 第 226 行,俞琴,机构 892030
UNION ALL SELECT '8923293' -- Excel 第 227 行,马岚,机构 892080
UNION ALL SELECT '8923875' -- Excel 第 228 行,吕姗珊,机构 892020
UNION ALL SELECT '8923509' -- Excel 第 229 行,杜俊杭,机构 892214
UNION ALL SELECT '8923720' -- Excel 第 230 行,陈云,机构 892031
UNION ALL SELECT '8923323' -- Excel 第 231 行,王微,机构 892200
UNION ALL SELECT '8925227' -- Excel 第 232 行,朱恒杰,机构 892171
UNION ALL SELECT '8925423' -- Excel 第 233 行,章佳妍,机构 892070
UNION ALL SELECT '8925221' -- Excel 第 234 行,柳奕帆,机构 892140
UNION ALL SELECT '8923500' -- Excel 第 235 行,王锦瑜,机构 892200
UNION ALL SELECT '8923971' -- Excel 第 236 行,任依婷,机构 892182
UNION ALL SELECT '8923302' -- Excel 第 237 行,朱杰权,机构 892160
UNION ALL SELECT '8923701' -- Excel 第 238 行,沈炜汉,机构 892130
UNION ALL SELECT '8923292' -- Excel 第 239 行,阮严波,机构 892082
UNION ALL SELECT '8925119' -- Excel 第 240 行,吕丹旋,机构 892141
UNION ALL SELECT '8925125' -- Excel 第 241 行,范梦煖,机构 892010
UNION ALL SELECT '8925477' -- Excel 第 242 行,徐天柱,机构 892113
UNION ALL SELECT '8923515' -- Excel 第 243 行,高齐天,机构 892141
UNION ALL SELECT '8923307' -- Excel 第 244 行,孙威,机构 892170
UNION ALL SELECT '8923757' -- Excel 第 245 行,徐煊楷,机构 892183
UNION ALL SELECT '8925396' -- Excel 第 246 行,陈益未,机构 892110
UNION ALL SELECT '8923316' -- Excel 第 247 行,徐小雪,机构 892190
UNION ALL SELECT '8925605' -- Excel 第 248 行,陈礼炀,机构 892220
UNION ALL SELECT '8923459' -- Excel 第 249 行,章丽,机构 892224
UNION ALL SELECT '8923616' -- Excel 第 250 行,宋灵蓉,机构 892111
UNION ALL SELECT '8923389' -- Excel 第 251 行,张丹凤,机构 892200
UNION ALL SELECT '8923870' -- Excel 第 252 行,曹坚,机构 892130
UNION ALL SELECT '8923214' -- Excel 第 253 行,冯佳,机构 892112
UNION ALL SELECT '8923759' -- Excel 第 254 行,吕琦,机构 892182
) src
JOIN sys_user u ON u.user_name = src.teller_no AND u.del_flag = '0'
JOIN sys_role r ON r.role_name = '客户经理' AND r.status = '0' AND r.del_flag = '0';

View File

@@ -765,6 +765,8 @@ CREATE TABLE `loan_pricing_workflow` (
`loan_purpose` varchar(20) DEFAULT NULL COMMENT '贷款用途: consumer-消费/business-经营',
`business_type` varchar(20) DEFAULT NULL COMMENT '业务种类',
`loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率',
`coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率',
`res_cover` varchar(10) DEFAULT NULL COMMENT '余值覆盖: 0/1',
`biz_proof` varchar(10) DEFAULT NULL COMMENT '是否有经营佐证: true/false',
`loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false贷款合同是否开通循环功能',
`coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一类/二类/三类/四类',
@@ -775,6 +777,7 @@ CREATE TABLE `loan_pricing_workflow` (
`id_type` varchar(50) DEFAULT NULL COMMENT '证件类型',
`id_num` varchar(100) DEFAULT NULL COMMENT '证件号码',
`is_inclusive_finance` varchar(10) DEFAULT NULL COMMENT '是否普惠小微借款人: true/false',
`dept_id` bigint(20) DEFAULT NULL COMMENT '创建人部门ID',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
@@ -782,6 +785,7 @@ CREATE TABLE `loan_pricing_workflow` (
PRIMARY KEY (`id`),
UNIQUE KEY `uk_serial_num` (`serial_num`),
KEY `idx_org_code` (`org_code`),
KEY `idx_dept_id` (`dept_id`),
KEY `idx_create_by` (`create_by`),
KEY `idx_cust_name` (`cust_name`),
KEY `idx_update_time` (`update_time`)

View File

@@ -347,6 +347,8 @@ CREATE TABLE `loan_pricing_workflow` (
`loan_purpose` varchar(20) DEFAULT NULL COMMENT '贷款用途: consumer-消费/business-经营',
`business_type` varchar(20) DEFAULT NULL COMMENT '业务种类',
`loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率',
`coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率',
`res_cover` varchar(10) DEFAULT NULL COMMENT '余值覆盖: 0/1',
`biz_proof` varchar(10) DEFAULT NULL COMMENT '是否有经营佐证: true/false',
`loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false贷款合同是否开通循环功能',
`coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一类/二类/三类/四类',
@@ -357,6 +359,7 @@ CREATE TABLE `loan_pricing_workflow` (
`id_type` varchar(50) DEFAULT NULL COMMENT '证件类型',
`id_num` varchar(100) DEFAULT NULL COMMENT '证件号码',
`is_inclusive_finance` varchar(10) DEFAULT NULL COMMENT '是否普惠小微借款人: true/false',
`dept_id` bigint(20) DEFAULT NULL COMMENT '创建人部门ID',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
@@ -364,6 +367,7 @@ CREATE TABLE `loan_pricing_workflow` (
PRIMARY KEY (`id`),
UNIQUE KEY `uk_serial_num` (`serial_num`),
KEY `idx_org_code` (`org_code`),
KEY `idx_dept_id` (`dept_id`),
KEY `idx_create_by` (`create_by`),
KEY `idx_cust_name` (`cust_name`),
KEY `idx_update_time` (`update_time`)

View File

@@ -28,6 +28,8 @@ CREATE TABLE `loan_pricing_workflow` (
`loan_purpose` varchar(20) DEFAULT NULL COMMENT '贷款用途: consumer-消费/business-经营',
`business_type` varchar(20) DEFAULT NULL COMMENT '业务种类',
`loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率',
`coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率',
`res_cover` varchar(10) DEFAULT NULL COMMENT '余值覆盖: 0/1',
`biz_proof` varchar(10) DEFAULT NULL COMMENT '是否有经营佐证: true/false',
`loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false贷款合同是否开通循环功能',
`coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一类/二类/三类/四类',
@@ -38,6 +40,7 @@ CREATE TABLE `loan_pricing_workflow` (
`id_type` varchar(50) DEFAULT NULL COMMENT '证件类型',
`id_num` varchar(100) DEFAULT NULL COMMENT '证件号码',
`is_inclusive_finance` varchar(10) DEFAULT NULL COMMENT '是否普惠小微借款人: true/false',
`dept_id` bigint(20) DEFAULT NULL COMMENT '创建人部门ID',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
@@ -45,6 +48,7 @@ CREATE TABLE `loan_pricing_workflow` (
PRIMARY KEY (`id`),
UNIQUE KEY `uk_serial_num` (`serial_num`),
KEY `idx_org_code` (`org_code`),
KEY `idx_dept_id` (`dept_id`),
KEY `idx_create_by` (`create_by`),
KEY `idx_cust_name` (`cust_name`),
KEY `idx_update_time` (`update_time`)