Compare commits
34 Commits
codex/migr
...
a4f927fdcb
| Author | SHA1 | Date | |
|---|---|---|---|
| a4f927fdcb | |||
| a22c83ba7e | |||
| 9ed6fc2d38 | |||
| 6ef3cfcaea | |||
| a50b25e4ec | |||
| 947c955415 | |||
| 6c949fee40 | |||
| 366d8e499a | |||
| abc8b127e1 | |||
| 709a314107 | |||
| e4a8cf4a13 | |||
| 938f9bb28e | |||
| 764bc7f363 | |||
| 01a0bee5c0 | |||
| 761b9a0612 | |||
| 801310ee10 | |||
| 289937f264 | |||
| 6081ee87f2 | |||
| ade4100aeb | |||
| 33365a0d74 | |||
| b8b8d21b09 | |||
| 6a91cd7ea6 | |||
| ca19ba754d | |||
| 0f2a22f2c6 | |||
| 6dbd56dc52 | |||
| 86b8704ad9 | |||
| 3180c33500 | |||
| a90ab42be6 | |||
| ddf2f976f5 | |||
| 886cba6b3d | |||
| 571f7bc075 | |||
| 5a80653917 | |||
| bfe1b346d9 | |||
| ef40675422 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -49,5 +49,13 @@ nbdist/
|
||||
|
||||
logs/
|
||||
ruoyi-ui/dist.zip
|
||||
????????_892.zip
|
||||
*/src/test/
|
||||
ruoyi-ui/tests
|
||||
.playwright-cli
|
||||
|
||||
.playwright-cli
|
||||
tongweb_63310.properties
|
||||
audit.log
|
||||
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
|
||||
30
AGENTS.md
30
AGENTS.md
@@ -1,30 +0,0 @@
|
||||
# AGENTS.md - AI Coding Assistant Guide
|
||||
|
||||
## GIT
|
||||
- git提交时使用中文添加描述
|
||||
- 无视`.DS_Store`
|
||||
|
||||
## AGENT
|
||||
- 不开启subagent
|
||||
|
||||
## 文档
|
||||
- 如果是前后端开发任务,根据设计文档产出实施计划时,输出两份执行文档,一份为后端的实施计划,一份为前端的实施计划
|
||||
- 每一次改动都需要留下实施文档,记录修改的内容
|
||||
- 每次写设计文档的时候,都要检查一下保存路径是否正确
|
||||
|
||||
## 测试
|
||||
- 开发完成后必须执行与本次改动直接对应的验证步骤,完成验证后才能结束本次任务
|
||||
- 如果是接口开发完成,先重启后端进程,确保最新代码已经生效,再调用接口进行测试
|
||||
- 接口测试时必须覆盖多种情况,至少包含正常场景、必填/参数错误场景、分支场景;如接口逻辑包含状态、类型、金额、期限等关键条件,需要分别验证对应分支
|
||||
- 如果是前端页面开发完成,必须启动前端页面并调用浏览器检查功能是否正常,确认页面展示、交互流程、接口联动和关键提示信息符合预期
|
||||
- 测试结束后,自动结束测试时开启的前后端进程
|
||||
|
||||
## 开发
|
||||
- 在开发前端的时候,不需要使用git worktree,直接在当前分支进行开发
|
||||
|
||||
## 方案规范
|
||||
- 当需要你给出方案时,必须符合以下规范
|
||||
- 不允许给出兼容性或补丁性的方案
|
||||
- 不允许过度设计,保持最短路径实现,且不能违反上一条要求
|
||||
- 不允许自行给出我提供的需求以外的方案,例如一些兜底和降级方案,这可能导致业务逻辑偏移问题
|
||||
- 必须确保方案的逻辑正确,必须经过全链路的逻辑验证
|
||||
95
build_release_892.sh
Executable file
95
build_release_892.sh
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
ROOT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||
DATE_STAMP=$(date "+%Y%m%d")
|
||||
RELEASE_ZIP="$ROOT_DIR/${DATE_STAMP}_892.zip"
|
||||
BACKEND_JAR_SOURCE="$ROOT_DIR/ruoyi-admin/target/ruoyi-admin.jar"
|
||||
FRONTEND_DIR="$ROOT_DIR/ruoyi-ui"
|
||||
FRONTEND_DIST_DIR="$FRONTEND_DIR/dist"
|
||||
FRONTEND_DIST_ZIP="$FRONTEND_DIR/dist.zip"
|
||||
NODE_VERSION="14"
|
||||
|
||||
log_info() {
|
||||
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >&2
|
||||
}
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
log_error "缺少命令: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then
|
||||
rm -rf "$WORK_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
build_backend() {
|
||||
log_info "开始构建后端生产 jar"
|
||||
(
|
||||
cd "$ROOT_DIR"
|
||||
mvn -pl ruoyi-admin -am clean package -DskipTests
|
||||
)
|
||||
|
||||
if [ ! -f "$BACKEND_JAR_SOURCE" ]; then
|
||||
log_error "未生成后端 jar: $BACKEND_JAR_SOURCE"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
build_frontend() {
|
||||
log_info "开始构建前端生产 dist"
|
||||
ROOT_DIR="$ROOT_DIR" NODE_VERSION="$NODE_VERSION" zsh -lic 'nvm use "$NODE_VERSION" >/dev/null && npm --prefix "$ROOT_DIR/ruoyi-ui" run build:prod'
|
||||
|
||||
if [ ! -f "$FRONTEND_DIST_DIR/index.html" ]; then
|
||||
log_error "前端生产构建失败,未找到: $FRONTEND_DIST_DIR/index.html"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "$FRONTEND_DIST_ZIP"
|
||||
(
|
||||
cd "$FRONTEND_DIR"
|
||||
zip -qr "$FRONTEND_DIST_ZIP" dist
|
||||
)
|
||||
|
||||
if [ ! -f "$FRONTEND_DIST_ZIP" ]; then
|
||||
log_error "未生成前端压缩包: $FRONTEND_DIST_ZIP"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
package_release() {
|
||||
WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/loan_pricing_release.XXXXXX")
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
cp "$BACKEND_JAR_SOURCE" "$WORK_DIR/ruoyi-admin.jar"
|
||||
cp "$FRONTEND_DIST_ZIP" "$WORK_DIR/dist.zip"
|
||||
|
||||
rm -f "$RELEASE_ZIP"
|
||||
(
|
||||
cd "$WORK_DIR"
|
||||
zip -qr "$RELEASE_ZIP" ruoyi-admin.jar dist.zip
|
||||
)
|
||||
|
||||
log_info "上线压缩包已生成: $RELEASE_ZIP"
|
||||
}
|
||||
|
||||
main() {
|
||||
require_command mvn
|
||||
require_command zsh
|
||||
require_command zip
|
||||
|
||||
build_backend
|
||||
build_frontend
|
||||
package_release
|
||||
}
|
||||
|
||||
main "$@"
|
||||
52
doc/2026-04-16-shangyu-corporate-alignment-backend-plan.md
Normal file
52
doc/2026-04-16-shangyu-corporate-alignment-backend-plan.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# 上虞对公利率测算字段对齐后端实施计划
|
||||
|
||||
## 目标
|
||||
- 对齐对公创建接口、模型调用入参、流程详情返回、mock 返回和 SQL 基线。
|
||||
|
||||
## 实施内容
|
||||
- 创建请求字段改为 Excel `上传指标` 口径:
|
||||
- 新增 `repayMethod`
|
||||
- `isTradeConstruction` 改为 `isTradeBuildEnt`
|
||||
- 移除对公创建链路中的 `isAgriGuar`、`isTechEnt`
|
||||
- 流程主表实体补 `repayMethod`,并将 `isTradeBuildEnt` 映射到数据库列 `is_trade_construction`
|
||||
- 对公模型输出实体补齐:
|
||||
- `repayMethod`
|
||||
- `isTradeBuildEnt`
|
||||
- `loanRateHistory`
|
||||
- `minRateProduct`
|
||||
- `smoothRange`
|
||||
- `finalCalculateRate`
|
||||
- `referenceRate`
|
||||
- 对公模型输出实体不再暴露:
|
||||
- `isAgriGuar`
|
||||
- `midEntTax`
|
||||
- `cardOverdue`
|
||||
- 企业模型入参统一值域:
|
||||
- `isGreenLoan`、`isTradeBuildEnt`、`collThirdParty` 发送 `0/1`
|
||||
- `repayMethod` 发送 `分期/不分期`
|
||||
- 企业流程详情主利率改为 `finalCalculateRate`
|
||||
- mock 继续保留 `data.mappingOutputFields` 包装层,只更新企业字段集合和值域
|
||||
|
||||
## SQL 调整
|
||||
- `loan_pricing_workflow` 新增 `repay_method`
|
||||
- `model_corp_output_fields` 新增:
|
||||
- `repay_method`
|
||||
- `is_trade_build_ent`
|
||||
- `loan_rate_history`
|
||||
- `min_rate_product`
|
||||
- `smooth_range`
|
||||
- `final_calculate_rate`
|
||||
- `reference_rate`
|
||||
- 已同步更新:
|
||||
- `sql/loan_pricing_workflow.sql`
|
||||
- `sql/model_corp.sql`
|
||||
- `sql/loan_pricing_schema_20260328.sql`
|
||||
- `sql/loan_pricing_prod_init_20260331.sql`
|
||||
- `sql/2026-04-16-shangyu-corporate-alignment.sql`
|
||||
|
||||
## 验证
|
||||
- 运行后端定向单测,确认对公字段和详情主利率断言通过
|
||||
- 使用 `/login/test` 获取 token 后调用对公创建和详情接口,确认:
|
||||
- 正常场景成功
|
||||
- 缺少 `repayMethod` 返回校验错误
|
||||
- 详情返回包含新增字段且 `loanRate = finalCalculateRate`
|
||||
48
doc/2026-04-16-shangyu-corporate-alignment-frontend-plan.md
Normal file
48
doc/2026-04-16-shangyu-corporate-alignment-frontend-plan.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 上虞对公利率测算字段对齐前端实施计划
|
||||
|
||||
## 目标
|
||||
- 对齐对公新增弹窗和企业流程详情页展示,严格跟随 Excel `上传指标` 与 `展示指标`。
|
||||
|
||||
## 实施内容
|
||||
- 对公新增弹窗调整为 Excel `上传指标`:
|
||||
- 新增 `repayMethod`
|
||||
- `isTradeConstruction` 改为 `isTradeBuildEnt`
|
||||
- 删除 `省农担担保贷款`、`科技型企业`
|
||||
- `loanTerm` 文案改为按年
|
||||
- `collType` 选项改为 `一类/二类/三类/四类`
|
||||
- `isGreenLoan`、`isTradeBuildEnt`、`collThirdParty` 提交值改为 `1/0`
|
||||
- 企业详情左侧关键信息:
|
||||
- 标签改为 `最终测算利率`
|
||||
- 读取 `corpOutput.finalCalculateRate`
|
||||
- 企业流程详情业务信息:
|
||||
- 新增展示 `repayMethod`
|
||||
- 新增展示 `isTradeBuildEnt`
|
||||
- 保留 `isGreenLoan`
|
||||
- 移除不在本次口径内的企业业务展示
|
||||
- 企业模型输出补齐展示:
|
||||
- `repayMethod`
|
||||
- `isTradeBuildEnt`
|
||||
- `loanRateHistory`
|
||||
- `minRateProduct`
|
||||
- `smoothRange`
|
||||
- `finalCalculateRate`
|
||||
- `referenceRate`
|
||||
- 企业模型输出移除展示:
|
||||
- `isAgriGuar`
|
||||
- `midEntTax`
|
||||
- `cardOverdue`
|
||||
|
||||
## 测试脚本
|
||||
- 新增:
|
||||
- `ruoyi-ui/tests/corporate-create-input-params.test.js`
|
||||
- `ruoyi-ui/tests/corporate-display-fields.test.js`
|
||||
- 更新 `ruoyi-ui/package.json`,补充对应 npm scripts
|
||||
|
||||
## 验证
|
||||
- `nvm use default` 后执行两个对公静态断言脚本
|
||||
- 执行前端生产构建
|
||||
- 启动前端页面并在浏览器中确认:
|
||||
- 对公新增弹窗字段和选项正确
|
||||
- 创建成功后列表刷新
|
||||
- 企业详情页显示 `最终测算利率`
|
||||
- 企业详情页和模型输出出现新增字段
|
||||
@@ -284,6 +284,7 @@ GET /loanPricing/workflow/20250119143025123
|
||||
"idType": "身份证",
|
||||
"idNum": "330102199001011234",
|
||||
"baseLoanRate": "3.45",
|
||||
"greyBlackCust": "1",
|
||||
"isFirstLoan": "true",
|
||||
"faithDay": "365",
|
||||
"custAge": "35",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# 上虞对公利率测算字段对齐实施记录
|
||||
|
||||
## 修改时间
|
||||
- 2026-04-16
|
||||
|
||||
## 修改内容
|
||||
- 对齐对公创建请求字段,新增 `repayMethod`,将 `isTradeConstruction` 统一为 `isTradeBuildEnt`
|
||||
- 对齐企业详情返回与页面展示,左侧主利率改为 `finalCalculateRate`
|
||||
- 对齐对公模型输出字段,补齐 `loanRateHistory`、`minRateProduct`、`smoothRange`、`finalCalculateRate`、`referenceRate`
|
||||
- 裁剪企业模型输出和页面展示,不再暴露 `isAgriGuar`、`midEntTax`、`cardOverdue`
|
||||
- 对公新增弹窗中的 `贷款期限(年)` 调整为下拉框,选项固定为 `1-10` 年
|
||||
- 更新企业 mock 返回和 SQL 基线、迁移脚本
|
||||
|
||||
## 文档与脚本
|
||||
- `doc/2026-04-16-shangyu-corporate-alignment-backend-plan.md`
|
||||
- `doc/2026-04-16-shangyu-corporate-alignment-frontend-plan.md`
|
||||
- `sql/2026-04-16-shangyu-corporate-alignment.sql`
|
||||
|
||||
## 验证记录
|
||||
- 后端单测:
|
||||
- `mvn -pl ruoyi-loan-pricing -Dtest=ModelCorpOutputFieldsTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest test`
|
||||
- 结果:13 个测试全部通过
|
||||
- 前端静态断言:
|
||||
- `zsh -lic 'nvm use default >/dev/null && npm --prefix ruoyi-ui run test:corporate-create-input-params'`
|
||||
- `zsh -lic 'nvm use default >/dev/null && npm --prefix ruoyi-ui run test:corporate-display-fields'`
|
||||
- 结果:两个脚本均通过
|
||||
- 前端构建:
|
||||
- `zsh -lic 'nvm use default >/dev/null && npm --prefix ruoyi-ui run build:prod'`
|
||||
- 结果:构建成功,仅有体积告警
|
||||
- 接口联调:
|
||||
- 使用 `/login/test` 获取 token
|
||||
- 验证了对公创建正常场景、缺少 `repayMethod` 的参数错误场景、`分期/不分期` 与 `1/0` 分支场景
|
||||
- 详情接口确认返回新增字段,且 `loanPricingWorkflow.loanRate = modelCorpOutputFields.finalCalculateRate`
|
||||
- 浏览器联调:
|
||||
- 启动前端开发服务并打开流程列表
|
||||
- 验证对公新增弹窗字段、选项、提交流程
|
||||
- 验证创建后列表新增记录
|
||||
- 验证企业详情页出现 `最终测算利率`、`还款方式`、`贸易和建筑业企业`、`历史利率`、`产品最低利率下限`、`平滑幅度`、`参考利率`
|
||||
@@ -0,0 +1,21 @@
|
||||
# 流程详情页模型输出平铺展示实施记录
|
||||
|
||||
## 改动日期
|
||||
- 2026-04-16
|
||||
|
||||
## 改动范围
|
||||
- 前端:`ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
|
||||
- 前端测试:`ruoyi-ui/tests/model-output-flat-display.test.js`
|
||||
- 前端脚本:`ruoyi-ui/package.json`
|
||||
|
||||
## 改动内容
|
||||
- 取消流程详情页“模型输出”区域的 Tab 切换结构。
|
||||
- 保留原有分组顺序与字段内容,将“基本信息”“忠诚度分析”“贡献度分析”等分组改为自上而下平铺展示。
|
||||
- 按最新要求,将“测算结果”分组前移到“基本信息”下方,优先展示最终测算相关结果。
|
||||
- 按最新要求,将“测算结果”中的“最终测算利率”调整到最后一行展示。
|
||||
- 移除组件内仅用于 Tab 默认选中的 `activeTab` 和相关监听逻辑。
|
||||
- 新增最小回归测试,校验模型输出组件不再包含 `el-tabs`、`el-tab-pane`,并具备平铺分组区块,同时校验“基本信息”后紧跟“测算结果”。
|
||||
|
||||
## 验证计划
|
||||
- 使用 `nvm` 显式切换前端 Node 版本后执行 `npm run test:model-output-flat-display`。
|
||||
- 启动前端页面,在浏览器中打开流程详情页,确认模型输出区域已按分组平铺展示,且不再出现 Tab 切换。
|
||||
@@ -0,0 +1,35 @@
|
||||
# 对公还款方式移除与抵质押字段联动实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 对公新增弹窗移除 `还款方式` 输入项、初始化字段、重置字段、必填校验和提交字段。
|
||||
- 对公详情页与模型输出展示移除 `还款方式`。
|
||||
- 对公、对私新增弹窗中,`担保方式` 为 `抵押` 或 `质押` 时才展示 `抵质押类型`、`抵质押物是否第三方所有`。
|
||||
- `抵质押类型` 根据担保方式动态切换:
|
||||
- `抵押`:`一类`、`二类`、`三类`、`四类`、`其他`
|
||||
- `质押`:`存单质押`、`其他`
|
||||
- 担保方式切换时清空已选抵质押类型和第三方所有标识,隐藏抵质押字段时不向后端提交。
|
||||
- 对公创建 DTO 取消 `repayMethod` 必填与枚举校验;`collType` 不再全局必填,合法值调整为 `一类/二类/三类/四类/其他/存单质押`。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 前端静态测试通过:
|
||||
- `npm run test:corporate-create-input-params`
|
||||
- `npm run test:corporate-display-fields`
|
||||
- `npm run test:personal-create-input-params`
|
||||
- 后端编译与单测通过:
|
||||
- `mvn -pl ruoyi-loan-pricing -am -Dtest=ModelCorpOutputFieldsTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- 后端接口验证通过:
|
||||
- `信用` 不传 `repayMethod`、不传抵质押字段可创建。
|
||||
- `抵押` 传 `一类` 且不传 `repayMethod` 可创建。
|
||||
- `质押` 传 `存单质押` 且不传 `repayMethod` 可创建。
|
||||
- 缺少 `custIsn`、缺少 `guarType`、非法 `guarType` 仍返回参数错误。
|
||||
- 真实前端页面验证通过:
|
||||
- 对公新增弹窗不显示 `还款方式`。
|
||||
- 对公、对私新增弹窗在 `信用/保证` 下隐藏抵质押字段。
|
||||
- 对公、对私新增弹窗在 `抵押/质押` 下显示抵质押字段,且选项分别符合规则。
|
||||
- 对公详情页与模型输出区域不再显示 `还款方式`。
|
||||
|
||||
## 说明
|
||||
|
||||
- 本次不删除数据库字段和实体字段,仅停止创建入口要求和页面展示,保留历史数据结构。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 个人/企业模型接口拆分实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 将统一模型接口配置 `model.url` 拆分为 `model.personal-url` 和 `model.corporate-url`。
|
||||
- `dev`、`uat` 环境分别指向本地个人/企业 mock:
|
||||
- `http://localhost:63310/rate/pricing/mock/invokeModel/personal`
|
||||
- `http://localhost:63310/rate/pricing/mock/invokeModel/corporate`
|
||||
- `pro` 环境改为从 `MODEL_PERSONAL_URL`、`MODEL_CORPORATE_URL` 读取真实接口地址。
|
||||
- `ModelService` 拆分为 `invokePersonalModel` 和 `invokeCorporateModel`,分别返回 `ModelRetailOutputFields`、`ModelCorpOutputFields`。
|
||||
- `LoanPricingModelService` 根据 `custType` 调用对应模型接口,个人只写个人模型输出表,企业只写企业模型输出表。
|
||||
- mock 控制器拆分为个人、企业两个入口,不再保留统一 mock 路径作为业务调用入口。
|
||||
|
||||
## 字段管理
|
||||
|
||||
- 个人模型返回字段继续由 `ModelRetailOutputFields` 与 `model_retail_output_fields` 管理。
|
||||
- 企业模型返回字段继续由 `ModelCorpOutputFields` 与 `model_corp_output_fields` 管理。
|
||||
- 未新增统一返回对象,避免个人/企业字段混在同一套结构中。
|
||||
|
||||
## 验证记录
|
||||
|
||||
- 后端单测:
|
||||
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServiceTest,ModelRetailOutputFieldsTest,ModelCorpOutputFieldsTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- 结果:通过,`Tests run: 5, Failures: 0, Errors: 0`
|
||||
- 后端打包与启动:
|
||||
- `./bin/restart_java_backend.sh restart`
|
||||
- 结果:打包成功,提升权限后启动成功,后端监听 `63310`。
|
||||
- 真实接口验证:
|
||||
- `/login/test` 获取 token 成功。
|
||||
- 调用 `/loanPricing/workflow/create/personal` 创建个人流程,流水号 `20260427150819677`。
|
||||
- 查询个人详情,返回 `modelRetailOutputFields.finalCalculateRate=6.05`,`modelCorpOutputFields=null`。
|
||||
- 调用 `/loanPricing/workflow/create/corporate` 创建企业流程,流水号 `20260427150820494`。
|
||||
- 查询企业详情,返回 `modelCorpOutputFields.finalCalculateRate=3.732`,`modelRetailOutputFields=null`。
|
||||
- 缺少 `custIsn` 的个人创建请求返回 `客户内码不能为空`。
|
||||
- 后端日志确认个人命中 `/rate/pricing/mock/invokeModel/personal`,企业命中 `/rate/pricing/mock/invokeModel/corporate`。
|
||||
- 测试结束后已执行 `./bin/restart_java_backend.sh stop` 停止本次启动的后端进程。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 生产环境启动前必须提供 `MODEL_PERSONAL_URL`、`MODEL_CORPORATE_URL`。
|
||||
- 本次不改前端页面和现有业务接口路径。
|
||||
@@ -0,0 +1,54 @@
|
||||
# 2026-04-27 个人模型输出灰黑名单客户字段实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 后端个人模型输出实体 `ModelRetailOutputFields` 新增 `greyBlackCust` 字段,承接个人模型返回的 `0/1` 输出值。
|
||||
- 个人模型 mock 返回文件 `retail_output.json` 新增 `greyBlackCust: "1"`,用于本地模型调用链路验证。
|
||||
- `model_retail_output_fields` 表结构新增 `grey_black_cust` 字段,并补充增量迁移脚本 `sql/add_model_retail_grey_black_cust_20260427.sql`。
|
||||
- 前端模型输出组件在个人客户“基本信息”分组中展示“灰黑名单客户”,直接展示后端返回值 `0/1`。
|
||||
- 接口文档示例补充 `greyBlackCust` 返回字段。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFields.java`
|
||||
- `ruoyi-loan-pricing/src/main/resources/data/retail_output.json`
|
||||
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/entity/ModelRetailOutputFieldsTest.java`
|
||||
- `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
|
||||
- `ruoyi-ui/tests/retail-display-fields.test.js`
|
||||
- `sql/model_retail.sql`
|
||||
- `sql/loan_pricing_schema_20260328.sql`
|
||||
- `sql/loan_pricing_prod_init_20260331.sql`
|
||||
- `sql/add_model_retail_grey_black_cust_20260427.sql`
|
||||
- `doc/api/loan-pricing-workflow-api.md`
|
||||
|
||||
## 数据库变更
|
||||
|
||||
- 已在开发库 `loan-pricing.model_retail_output_fields` 执行新增列:
|
||||
|
||||
```sql
|
||||
ALTER TABLE model_retail_output_fields
|
||||
ADD COLUMN grey_black_cust varchar(100) DEFAULT '' COMMENT '灰黑名单客户' AFTER base_loan_rate;
|
||||
```
|
||||
|
||||
- 回查结果确认 `grey_black_cust` 字段存在。
|
||||
|
||||
## 验证记录
|
||||
|
||||
- `mvn -pl ruoyi-loan-pricing -Dtest=ModelRetailOutputFieldsTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- 通过,确认实体字段存在,个人/企业模型调用基础链路未回归。
|
||||
- `zsh -lic 'nvm use 14 >/dev/null && npm --prefix ruoyi-ui run test:retail-display-fields && npm --prefix ruoyi-ui run test:model-output-flat-display'`
|
||||
- 通过,确认前端包含 `retailOutput.greyBlackCust` 且字段位于个人模型输出“基本信息”分组。
|
||||
- `zsh -lic 'nvm use 14 >/dev/null && npm --prefix ruoyi-ui run build:prod'`
|
||||
- 通过,存在既有包体积 warning,无编译错误。
|
||||
- 后端真实接口验证:
|
||||
- 重启后端后调用个人流程创建接口,业务流水号 `20260427153305173`。
|
||||
- 调用详情接口返回 `modelRetailOutputFields.greyBlackCust = 1`。
|
||||
- 数据库联表回查 `model_retail_output_fields.grey_black_cust = 1`。
|
||||
- browser-use 真实页面验证:
|
||||
- 使用前端开发服务 `http://127.0.0.1:63311/` 打开真实流程详情页。
|
||||
- 页面 `模型输出 > 基本信息` 中可见“灰黑名单客户”,展示值为 `1`。
|
||||
- 测试结束后已停止本次启动的后端 `63310` 与前端 `63311` 进程,并回查端口不再监听。
|
||||
|
||||
## 备注
|
||||
|
||||
- 组合执行 `LoanPricingModelServicePersonalParamsTest` 时,当前本机 JDK 21 下 Mockito inline ByteBuddy 自附加失败;该失败与本次字段改动无关。已单独执行本次直接相关的非 Mockito 失败用例并通过。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 根目录 892 上线压缩包生成脚本实施记录
|
||||
|
||||
## 保存路径检查
|
||||
- 脚本保存路径:项目根目录 `build_release_892.sh`
|
||||
- 实施记录保存路径:`doc/implementation-report-2026-04-27-root-release-package-892.md`
|
||||
|
||||
## 修改内容
|
||||
- 新增根目录脚本 `build_release_892.sh`
|
||||
- 脚本执行后自动在项目根目录生成 `YYYYMMDD_892.zip`
|
||||
- 压缩包根层结构固定为:
|
||||
- `ruoyi-admin.jar`
|
||||
- `dist.zip`
|
||||
- 后端产物来自最新执行的 `mvn -pl ruoyi-admin -am clean package -DskipTests`
|
||||
- 前端产物来自 `nvm use 14` 后执行的 `npm --prefix ruoyi-ui run build:prod`
|
||||
- 前端构建完成后重新生成 `ruoyi-ui/dist.zip`
|
||||
- 更新 `.gitignore`,忽略根目录生成的 `????????_892.zip`
|
||||
|
||||
## 验证结果
|
||||
- 已执行 `sh -n build_release_892.sh`,语法校验通过
|
||||
- 已执行 `./build_release_892.sh`,后端 Maven 构建成功,前端生产构建成功
|
||||
- 前端构建过程中仅出现原有包体积 warning 与 npm 更新检查权限提示,不影响产物生成
|
||||
- 已生成根目录压缩包:`20260427_892.zip`
|
||||
- 已按最新要求调整压缩包结构,根层直接放置两个文件,不再包含 `deploy/` 目录
|
||||
- 已执行 `unzip -l 20260427_892.zip`,确认压缩包内容为:
|
||||
- `ruoyi-admin.jar`
|
||||
- `dist.zip`
|
||||
- 已执行 `git check-ignore -v 20260427_892.zip ruoyi-ui/dist.zip`,确认根目录上线压缩包和前端临时压缩包均不会进入 git
|
||||
@@ -0,0 +1,20 @@
|
||||
# 面包屑重复 key 告警处理实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 修复流程列表首页进入后控制台出现 `Duplicate keys detected: '/index'` 的问题。
|
||||
- 根因是当前首页实际路由为 `/index`,面包屑组件仍只按路由名 `Index` 判断首页,导致额外追加的“首页”项与“流程列表”项使用相同路径 `/index` 作为 key。
|
||||
- 将面包屑首页判断补充为同时识别 `path === '/index'`,避免在首页路由重复追加“首页”项。
|
||||
|
||||
## 验证方式
|
||||
|
||||
- 启动前端开发服务后,使用真实浏览器访问 `/index`。
|
||||
- 检查控制台不再出现 `Duplicate keys detected: '/index'`。
|
||||
- 检查流程列表页面仍可正常展示。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 已使用 Node 14.21.3 启动前端开发服务并通过 Playwright 访问真实页面 `http://127.0.0.1:8080/index`。
|
||||
- 页面成功进入“流程列表”,面包屑仅展示“流程列表”,未再重复追加“首页”。
|
||||
- 浏览器控制台统计为 `Errors: 0, Warnings: 0`,未再出现 `Duplicate keys detected: '/index'`。
|
||||
- 验证结束后已关闭本次启动的前端 `8080` 进程;后端 `63310` 为验证前已有进程,未做关闭处理。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 2026-04-29 业务种类与历史贷款利率设计记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 新增设计文档 `docs/superpowers/specs/2026-04-29-business-type-history-rate-design.md`。
|
||||
- 明确个人和企业新增流程同时增加业务种类和历史贷款利率。
|
||||
- 明确业务种类保存到流程表并在详情展示,但不进入模型入参。
|
||||
- 明确历史贷款利率保存到流程表,并作为模型调用入参上传。
|
||||
- 明确存量转贷时必须查询历史合同并单选一条;未选择历史合同禁止提交。
|
||||
- 明确历史合同查询采用后端代理外部接口、前端列表单选回填的方案。
|
||||
- 根据审查意见补充 `LoanPricingConverter` 字段映射、服务层跨字段校验、固定 mock 测试场景、直接接口测试覆盖和历史合同 URL 拼参方式。
|
||||
|
||||
## 验证说明
|
||||
|
||||
- 本次仅完成设计文档,未进入代码实现。
|
||||
- 首轮设计审查发现文档存在 5 个实施风险,已按意见补充到设计文档。
|
||||
- 第二轮设计审查结论为 Approved。
|
||||
- 后续实施时需要按设计文档分别覆盖后端接口验证、前端交互验证和真实页面浏览器验证。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 2026-04-29 业务种类与历史贷款利率实施计划记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 新增后端实施计划 `docs/superpowers/plans/2026-04-29-business-type-history-rate-backend-plan.md`。
|
||||
- 新增前端实施计划 `docs/superpowers/plans/2026-04-29-business-type-history-rate-frontend-plan.md`。
|
||||
- 后端计划覆盖字段、转换器、服务层校验、历史合同代理接口、mock、SQL 和后端测试。
|
||||
- 前端计划覆盖历史合同查询 API、历史合同单选弹窗、个人/企业新增弹窗、详情展示、静态测试和 browser-use 真实页面验证。
|
||||
- 根据计划审查意见,补充固定客户号 mock 场景 `HISTORY_EMPTY` / `HISTORY_EMPTY_RATE`,确保 browser-use 真实页面测试能稳定覆盖空列表和空历史利率。
|
||||
- 根据计划审查意见,修正前端客户号选择测试命令,避免调用不存在的 npm script。
|
||||
|
||||
## 验证说明
|
||||
|
||||
- 本次仅产出实施计划,未进入代码实现。
|
||||
- 真实页面测试已按用户要求明确使用 `browser-use:browser`,并禁止打开 prototype 页面。
|
||||
- 首轮计划审查发现 4 个执行风险,已按意见补充和修订。
|
||||
- 第二轮计划审查结论为 Approved。
|
||||
- 后续实施完成后需要补充 `doc/implementation-report-2026-04-29-business-type-history-rate.md` 记录代码改动和验证结果。
|
||||
@@ -0,0 +1,91 @@
|
||||
# 业务种类与历史贷款利率实施记录
|
||||
|
||||
## 后端实施
|
||||
|
||||
- 个人/企业利率定价创建 DTO 新增 `businessType`、`loanRateHistory` 字段。
|
||||
- 利率定价流程实体新增 `businessType`、`loanRateHistory` 持久化字段。
|
||||
- 模型调用 DTO 新增 `loanRateHistory` 字段,保持不新增 `businessType`。
|
||||
- 个人/企业创建转换器已映射业务种类和历史贷款利率。
|
||||
- 流程创建服务新增业务种类校验:必填,限定 `新客`、`存量新增`、`存量转贷`;`存量转贷` 必须选择历史贷款合同。
|
||||
- 新增历史贷款合同代理服务 `LoanRateHistoryService` 和接口 `GET /loanPricing/workflow/history-contract`。
|
||||
- 本地 mock 新增 `GET /rate/pricing/mock/history-contract`,覆盖正常、无历史合同、历史利率为空场景。
|
||||
- 本地 mock 客户号映射新增固定测试客户号 `HISTORY_EMPTY` 和 `HISTORY_EMPTY_RATE`。
|
||||
- dev/uat/pro 配置新增 `loan-rate-history.url`。
|
||||
- SQL 迁移和初始化脚本新增 `business_type`、`loan_rate_history` 字段。
|
||||
|
||||
## 后端验证
|
||||
|
||||
- 首次按计划运行 `mvn -pl ruoyi-loan-pricing -am -Dtest=... test` 时,`ruoyi-common` 因未匹配测试触发 Surefire 失败;后续按本仓库多模块测试习惯补充 `-Dsurefire.failIfNoSpecifiedTests=false`。
|
||||
- 当前 Oracle JDK 21 环境下 Mockito inline mock maker 需要预加载 Byte Buddy agent,验证命令使用 `JAVA_TOOL_OPTIONS=-javaagent:/Users/wkc/.m2/repository/net/bytebuddy/byte-buddy-agent/1.17.8/byte-buddy-agent-1.17.8.jar`。
|
||||
- 已执行并通过:
|
||||
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,HistoryLoanContractVOTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRateHistoryServiceTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowControllerHistoryContractTest,LoanRatePricingMockControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- `mvn -pl ruoyi-loan-pricing -am -Dtest=HistoryLoanContractVOTest,LoanRateHistoryServiceTest,LoanPricingWorkflowControllerHistoryContractTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest,LoanPricingWorkflowControllerCustomerMapTest,LoanRatePricingMockControllerCustomerMapTest,CustomerMapRecordVOTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
|
||||
## 前端实施
|
||||
|
||||
- `workflow.js` 新增 `queryHistoryContracts(custIsn)`,请求 `GET /loanPricing/workflow/history-contract`。
|
||||
- 新增共享组件 `HistoryContractSelector.vue`,按单选方式展示历史贷款合同,字段包含客户内码、历史贷款合同号、历史贷款担保方式、历史贷款产品代码、历史贷款利率、历史贷款金额、历史贷款签订时间。
|
||||
- 个人/企业新增弹窗新增 `业务种类`,选项为 `新客`、`存量新增`、`存量转贷`。
|
||||
- 当业务种类为 `存量转贷` 时,按客户内码查询历史贷款合同并弹出单选弹窗;未选合同、无历史合同、历史贷款利率为空时禁止提交。
|
||||
- 非 `存量转贷` 创建时不提交 `loanRateHistory`。
|
||||
- 个人/企业详情页在业务信息中展示 `业务种类`、`历史贷款利率`。
|
||||
|
||||
## 前端静态验证
|
||||
|
||||
- 已执行并通过:
|
||||
- `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'`
|
||||
- `zsh -lic 'nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/customer-map-selection.test.js && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params'`
|
||||
- `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'`
|
||||
- `build:prod` 通过,仍存在项目原有资源体积 warning。
|
||||
|
||||
## 数据库变更验证
|
||||
|
||||
- 已按 SQL 脚本对当前开发库执行:
|
||||
- `ALTER TABLE loan_pricing_workflow ADD COLUMN business_type varchar(20) DEFAULT NULL COMMENT '业务种类' AFTER loan_purpose, ADD COLUMN loan_rate_history varchar(100) DEFAULT NULL COMMENT '历史贷款利率' AFTER business_type;`
|
||||
- 已回查字段存在:
|
||||
- `business_type varchar(20)`
|
||||
- `loan_rate_history varchar(100)`
|
||||
|
||||
## 真实页面验证
|
||||
|
||||
- 后端已通过 `bin/restart_java_backend.sh restart` 重启并加载最新代码。
|
||||
- 前端已通过 `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run dev -- --port 8080'` 启动。
|
||||
- 使用 in-app browser 打开真实页面 `http://localhost:8080/index`,未使用 prototype 页面。
|
||||
- 已验证个人 `存量转贷`:
|
||||
- 测试客户内码 `81000922431`
|
||||
- 历史合同弹窗展示 7 个字段并支持单选。
|
||||
- 选择合同后提交成功。
|
||||
- 详情页展示 `业务种类=存量转贷`、`历史贷款利率=3.65`。
|
||||
- 已验证企业 `存量转贷`:
|
||||
- 测试客户内码 `81000329003`
|
||||
- 历史合同弹窗展示 7 个字段并支持单选。
|
||||
- 选择合同后提交成功。
|
||||
- 详情页展示 `业务种类=存量转贷`、`历史贷款利率=3.65`。
|
||||
- 已验证个人 `新客`:
|
||||
- 测试客户内码 `81000525694`
|
||||
- 不弹出历史贷款合同选择。
|
||||
- 提交成功。
|
||||
- 详情页展示 `业务种类=新客`,历史贷款利率为空值展示。
|
||||
- 已验证企业 `存量新增`:
|
||||
- 测试客户内码 `81000769824`
|
||||
- 不弹出历史贷款合同选择。
|
||||
- 提交成功。
|
||||
- 详情页展示 `业务种类=存量新增`,历史贷款利率为空值展示。
|
||||
- 已验证拦截场景:
|
||||
- `存量转贷` 打开历史合同弹窗后未选择合同,提示 `请选择历史贷款合同`,禁止提交。
|
||||
- 固定客户号 `HISTORY_EMPTY` 映射到 `EMPTY_HISTORY`,历史合同查询为空,提示 `未查询到历史贷款合同`,提交时校验 `请选择历史贷款合同`。
|
||||
- 固定客户号 `HISTORY_EMPTY_RATE` 映射到 `EMPTY_RATE`,历史合同存在但历史贷款利率为空,选择时提示 `历史贷款利率不能为空`,提交时仍校验 `请选择历史贷款合同`。
|
||||
- 已回查数据库:
|
||||
- `81000922431 / 个人 / 存量转贷 / 3.65 / 321000`
|
||||
- `81000329003 / 企业 / 存量转贷 / 3.65 / 654000`
|
||||
- `81000525694 / 个人 / 新客 / NULL / 321000`
|
||||
- `81000769824 / 企业 / 存量新增 / NULL / 654000`
|
||||
|
||||
## 进程清理
|
||||
|
||||
- 页面验证结束后已停止本次测试启动的前端和后端进程。
|
||||
- 已确认 `8080`、`63310` 端口无监听进程。
|
||||
@@ -0,0 +1,15 @@
|
||||
# 2026-04-29 客户号查询选择客户内码设计记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 新增设计文档 `docs/superpowers/specs/2026-04-29-customer-map-selection-design.md`。
|
||||
- 明确新增流程从“选择客户类型后直接打开新增弹窗”调整为“选择客户类型 -> 客户号查询 -> 选择客户内码 -> 打开新增弹窗”。
|
||||
- 明确后端新增个人/企业客户号映射业务接口和两个 mock 接口,配置地址先指向本项目 mock。
|
||||
- 明确客户号映射返回字段保持下划线命名。
|
||||
- 明确新增弹窗中的客户内码和客户名称由选择结果自动带入并只读。
|
||||
|
||||
## 验证说明
|
||||
|
||||
- 本次仅完成设计文档,未进入代码实现。
|
||||
- 设计文档已通过审查子代理审查,结论为 Approved,无阻塞问题。
|
||||
- 后续实施时需按设计文档中的后端、前端和真实页面测试范围执行验证。
|
||||
@@ -0,0 +1,14 @@
|
||||
# 2026-04-29 客户号查询选择客户内码实施计划记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 新增后端实施计划 `docs/superpowers/plans/2026-04-29-customer-map-selection-backend-plan.md`。
|
||||
- 新增前端实施计划 `docs/superpowers/plans/2026-04-29-customer-map-selection-frontend-plan.md`。
|
||||
- 后端计划覆盖客户号映射 VO、服务、业务接口、mock 接口、profile 配置和接口验证。
|
||||
- 前端计划覆盖客户号查询 API、查询选择弹窗、列表页流程串联、个人/企业新增弹窗只读带入和真实页面验证。
|
||||
|
||||
## 验证说明
|
||||
|
||||
- 本次仅完成实施计划文档,未进入代码实现。
|
||||
- 计划已按已确认设计拆分为前端和后端两份执行文档。
|
||||
- 实施计划已通过审查子代理审查,结论为 Approved,无阻塞问题。
|
||||
@@ -0,0 +1,60 @@
|
||||
# 2026-04-29 客户号查询选择客户内码实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 后端新增个人/企业客户号映射业务接口:
|
||||
- `GET /loanPricing/workflow/customer-map/personal?custId=...`
|
||||
- `GET /loanPricing/workflow/customer-map/corporate?custId=...`
|
||||
- 后端新增个人/企业客户号映射 mock 接口:
|
||||
- `GET /rate/pricing/mock/customer-map/personal?cust_id=...`
|
||||
- `GET /rate/pricing/mock/customer-map/corporate?cust_id=...`
|
||||
- 配置文件新增 `customer-map` 个人/企业地址,并在 `dev`、`uat`、`pro` profile 中统一指向本项目 mock。
|
||||
- 前端新增客户号查询选择弹窗。
|
||||
- 客户号查询选择弹窗宽度调整为窗口宽度的 `80%`。
|
||||
- 客户号查询输入值在查询前去除前后空格;后端转发个人/企业客户号映射接口前同步去除前后空格,并对下游 `cust_id` 查询参数做 URI 编码,避免尾随空格进入 request target 触发 TongWeb `HTTP Status 400`。
|
||||
- 选中客户号查询结果后,返回参数 `cust_id` 去除前三位后自动回填到新增弹窗基础信息的 `证件号码` 字段。
|
||||
- 个人/企业新增流程改为先查询客户号、选择客户内码,再打开新增弹窗。
|
||||
- 新增弹窗客户内码和客户名称由选中记录自动带入并只读。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 后端针对性测试:通过。
|
||||
- 命令:`mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest,LoanPricingCustomerMapServiceTest,LoanRatePricingMockControllerCustomerMapTest,LoanPricingWorkflowControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- 结果:`Tests run: 8, Failures: 0, Errors: 0, Skipped: 0`。
|
||||
- 客户号空格 400 修复补充测试:通过。
|
||||
- 命令:`mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest,LoanRatePricingMockControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||
- 结果:`Tests run: 6, Failures: 0, Errors: 0, Skipped: 0`。
|
||||
- 后端打包与启动验证:通过。
|
||||
- 命令:`mvn -pl ruoyi-admin -am -DskipTests package`
|
||||
- 命令:`java -jar ruoyi-admin/target/ruoyi-admin.jar --spring.profiles.active=dev`
|
||||
- 结果:后端以 `dev` profile 启动成功,端口 `63310` 可用。
|
||||
- 接口验证:通过。
|
||||
- `GET /rate/pricing/mock/customer-map/personal?cust_id=P001` 返回随机个人客户映射列表,字段为 `cust_id`、`cust_isn`、`cust_name`、`faith_day`、`balance_avg`、`loan_count_his`、`last_loan_date`。
|
||||
- `GET /rate/pricing/mock/customer-map/corporate?cust_id=C001` 返回随机企业客户映射列表,字段同上。
|
||||
- `GET /rate/pricing/mock/customer-map/personal?cust_id=` 返回 `客户号不能为空`。
|
||||
- 登录后调用 `GET /loanPricing/workflow/customer-map/personal?custId=P001` 和 `GET /loanPricing/workflow/customer-map/corporate?custId=C001` 均返回对应映射列表。
|
||||
- 登录后调用 `GET /loanPricing/workflow/customer-map/personal?custId=` 返回 `客户号不能为空`。
|
||||
- 补充验证:以最新后端临时端口 `63311` 调用 `GET /loanPricing/workflow/customer-map/personal?custId=1w0xc20xb7%20` 返回 `code=200`,返回 `cust_id` 为 `1w0xc20xb7`;调用 `GET /loanPricing/workflow/customer-map/corporate?custId=C001%20` 返回 `code=200`,返回 `cust_id` 为 `C001`;调用 `GET /loanPricing/workflow/customer-map/personal?custId=%20` 返回 `客户号不能为空`。
|
||||
- 前端针对性测试:通过。
|
||||
- 命令:`zsh -lic 'nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/customer-map-selection.test.js && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/workflow-index-refresh.test.js'`
|
||||
- 结果:`customer map selection assertions passed`、`personal create input params assertions passed`、`corporate create input params assertions passed`、`workflow-index-refresh test passed`。
|
||||
- 补充命令:`zsh -lic 'nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/customer-map-selection.test.js'`
|
||||
- 补充结果:`customer map selection assertions passed`,覆盖客户号去前三位回填证件号码断言。
|
||||
- 补充回归命令:`zsh -lic 'nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/customer-map-selection.test.js && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/id-number-validation-removal.test.js'`
|
||||
- 补充回归结果:`customer map selection assertions passed`、`personal create input params assertions passed`、`corporate create input params assertions passed`、`id number validation removal assertions passed`。
|
||||
- 前端生产构建:通过。
|
||||
- 命令:`zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'`
|
||||
- 结果:构建成功;输出 2 个既有体积 warning。
|
||||
- 真实页面验证:通过。
|
||||
- 启动前端 `http://localhost:18080/`,登录后进入真实页面 `http://localhost:18080/loanPricing/workflow`。
|
||||
- 个人客户:点击 `新增` -> `个人客户` -> 客户号查询输入 `PTEST003` -> 返回多条客户映射 -> 选择首行 -> 打开 `新增个人利率定价流程`,客户内码自动带入 `81000450472`,客户名称自动带入 `个人客户1`,两个字段均为 `readonly`。
|
||||
- 企业客户:点击 `新增` -> `企业客户` -> 客户号查询输入 `CTEST001` -> 返回多条客户映射 -> 选择首行 -> 打开 `新增企业利率定价流程`,客户内码自动带入 `81000448819`,客户名称自动带入 `企业客户1`,两个字段均为 `readonly`。
|
||||
- 弹窗宽度补充验证:点击 `新增` -> `个人客户` 后,`客户号查询` 弹窗 DOM 样式为 `margin-top: 15vh; width: 80%;`。
|
||||
- 客户号空格补充验证:点击 `新增` -> `个人客户`,客户号输入 `1w0xc20xb7 ` 后查询,输入框显示为 `1w0xc20xb7`,列表正常返回客户映射,页面未出现 `400` 或 `Bad Request`。
|
||||
- 客户号到证件号码补充验证:点击 `新增` -> `个人客户`,客户号输入 `ABC123456789` 并选择返回行后,新增个人弹窗基础信息的 `证件号码` 自动填入 `123456789`。
|
||||
- 页面验证仅验证查询选择与自动回填链路,未提交新增表单,避免写入额外流程测试数据。
|
||||
- 回归补充说明:
|
||||
- `LoanPricingModelServiceTest` 已通过。
|
||||
- 组合回归命令包含 `LoanPricingWorkflowServiceImplTest` 时,当前本机 Oracle JDK 21 环境下 Mockito inline/Byte Buddy self-attach 失败,属于既有测试环境限制,不是本次客户号映射功能断言失败。
|
||||
- 进程清理:已关闭本次启动的后端和前端进程。
|
||||
- 本次宽度与客户号空格补充验证结束后,`18080`、`63311` 无监听;`63310` 为验证前已存在的后端进程,未处理。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 历史贷款合同单选异常文本修复实施记录
|
||||
|
||||
## 问题
|
||||
|
||||
- 历史贷款合同选择弹窗的选择列出现 `{ "cus...` 这类异常文本。
|
||||
- 原因是 `el-radio` 使用整行对象 `scope.row` 作为 `label`,Element UI 会把对象值渲染到单选文案区域。
|
||||
|
||||
## 修改
|
||||
|
||||
- `HistoryContractSelector.vue` 将单选绑定值从整行对象调整为 `selectedContractKey`。
|
||||
- 新增 `contractRadioValue(row, index)` 生成稳定单选值,优先使用历史贷款合同号。
|
||||
- 保留 `selectedContract` 单独保存选中行对象,提交时仍向父组件返回完整合同记录。
|
||||
- 隐藏单选组件内部 label 文案,选择列只展示单选圆点,不展示对象文本。
|
||||
- `business-type-history-rate.test.js` 增加断言,禁止再出现 `:label="scope.row"`。
|
||||
|
||||
## 验证
|
||||
|
||||
- 已执行并通过:
|
||||
- `zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'`
|
||||
- 已启动后端和前端后使用 in-app browser 真实页面验证:
|
||||
- 打开 `http://localhost:8080/index`。
|
||||
- 新增个人客户,选择业务种类 `存量转贷`。
|
||||
- 历史贷款合同选择弹窗正常展示客户内码、历史贷款合同号、历史贷款利率等字段。
|
||||
- 选择列文本为空,不再出现 `{ "cus...` 或行对象 JSON 文本。
|
||||
- 验证结果:`hasObjectText=false`。
|
||||
- 验证结束后已关闭测试弹窗。
|
||||
- 验证结束后已停止本次启动的前端和后端进程,并确认 `8080`、`63310` 端口无监听。
|
||||
@@ -0,0 +1,17 @@
|
||||
# 实施记录 - 外部接口调用日志
|
||||
|
||||
## 日期
|
||||
|
||||
2026-04-30
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 在根目录 `AGENTS.md` 的测试规范中新增外部接口调用日志要求:每次调用外部接口进行测试或联调时,必须在后端日志中完整输出请求 URL、请求参数和返回参数。
|
||||
- 补齐客户映射接口、历史贷款合同接口、通用 `HttpUtils` 外部接口调用日志,输出请求 URL、请求参数和返回参数。
|
||||
- 将外部接口日志调整为多行可读格式:请求 URL、请求参数、返回参数分段输出,参数对象和返回对象使用 pretty JSON 展开。
|
||||
- 调整单元测试,覆盖客户映射外呼日志、历史贷款合同外呼日志。
|
||||
|
||||
## 验证
|
||||
|
||||
- 已检查规则保存路径为项目根目录 `AGENTS.md`。
|
||||
- 已执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest,LoanRateHistoryServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`,测试通过。
|
||||
34
doc/implementation-report-2026-05-09-operation-manual.md
Normal file
34
doc/implementation-report-2026-05-09-operation-manual.md
Normal 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/`,避免误提交临时文件。
|
||||
@@ -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`。
|
||||
@@ -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`,记录真实代码改动、测试命令和页面验证结果。
|
||||
@@ -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 来自验证时故意触发必填校验。
|
||||
@@ -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`,确认新背景图已渲染,账号、密码和登录按钮显示正常。
|
||||
18
doc/implementation-report-2026-05-12-login-title-removal.md
Normal file
18
doc/implementation-report-2026-05-12-login-title-removal.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 登录页标题文案移除实施记录
|
||||
|
||||
## 修改时间
|
||||
|
||||
2026-05-12
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 移除登录框顶部标题展示,不再显示“上虞利率定价系统”。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `ruoyi-ui/src/views/login.vue`
|
||||
|
||||
## 验证情况
|
||||
|
||||
- 已检查登录页模板,确认登录框内不再渲染标题节点。
|
||||
- 已使用 browser-use 打开 `http://localhost:9527/login` 进行实际页面验证,确认页面跳转到登录页后不再出现“上虞利率定价系统”,账号和密码输入项仍正常显示。
|
||||
@@ -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。
|
||||
BIN
doc/上虞_客户内码客户_历史利率_映射表.xlsx
Normal file
BIN
doc/上虞_客户内码客户_历史利率_映射表.xlsx
Normal file
Binary file not shown.
BIN
doc/上虞利率定价系统操作手册.docx
Normal file
BIN
doc/上虞利率定价系统操作手册.docx
Normal file
Binary file not shown.
BIN
doc/上虞对公利率测算_上传字段与展示字段 .xlsx
Normal file
BIN
doc/上虞对公利率测算_上传字段与展示字段 .xlsx
Normal file
Binary file not shown.
18
doc/利率定价接口.txt
Normal file
18
doc/利率定价接口.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
--------客户号与客户内码映射接口
|
||||
----对私
|
||||
http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_lilvcesuan_ind_idmapno?appCode=1a89fa84abda480ba93ed73fd01ffd07&cust_id=
|
||||
---案例:1对1 101330419198206033217 101330419197511072429
|
||||
---案例:1对n 101330682197911073012 10133062319810217642X
|
||||
|
||||
|
||||
----对公
|
||||
http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_lilvcesuan_ent_idmapno?appCode=1a89fa84abda480ba93ed73fd01ffd07&cust_id=
|
||||
---案例:1对1 20291330600146150140Y 20291330600146150466L
|
||||
---案例:1对n 202913306047458026221 2029133060475302009XU
|
||||
|
||||
|
||||
|
||||
-----历史合同查询接口
|
||||
http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_loan_rate_history?appCode=1a89fa84abda480ba93ed73fd01ffd07&cust_isn=
|
||||
----1对1 81033011438 81035265634
|
||||
----1对n 82002469287 82000882275
|
||||
@@ -0,0 +1,27 @@
|
||||
# TongWeb 接入后端实施文档
|
||||
|
||||
## 目标
|
||||
|
||||
按照 `tongweb/2026-04-16-TongWeb接入全流程通用指南.md` 在当前后端工程中接入东方通 TongWeb,替换默认内嵌 Tomcat,并将 license 文件随 `ruoyi-admin` 启动模块一起打包。
|
||||
|
||||
## 实施内容
|
||||
|
||||
1. 在 `ruoyi-admin/pom.xml` 增加 TongWeb Maven 仓库。
|
||||
2. 在 `ruoyi-admin/pom.xml` 对 `ruoyi-framework`、`ruoyi-loan-pricing` 依赖排除 `spring-boot-starter-tomcat`。
|
||||
3. 在 `ruoyi-admin/pom.xml` 引入 `com.tongweb.springboot:tongweb-spring-boot-starter-3.x:7.0.E.7`。
|
||||
4. 将 TongWeb license 复制到 `ruoyi-admin/src/main/resources/license.dat`,并在 `application.yml` 中配置 `server.tongweb.license.path=classpath:license.dat`。
|
||||
5. 增加资源存在性测试,验证 `license.dat` 可从 classpath 加载。
|
||||
6. 执行后端构建、依赖树、打包产物和测试验证,确认 TongWeb 依赖与 license 已正确接入。
|
||||
|
||||
## 变更说明
|
||||
|
||||
- 当前项目基于 Spring Boot 3.5.x,因此实际接入使用 `tongweb-spring-boot-starter-3.x`,版本号延续指南中的 `7.0.E.7`。
|
||||
- license 按本次要求保持文件名为 `license.dat`,不改名为 `Tongweb_license.dat`。
|
||||
- 现有 `application-dev.yml`、`application-pro.yml`、`application-uat.yml` 中的 `server.tomcat` 参数暂时保留,后续以 TongWeb 实际启动结果为准决定是否继续清理。
|
||||
|
||||
## 验证步骤
|
||||
|
||||
1. `mvn -pl ruoyi-admin -Dtest=TongWebLicenseResourceTest test`
|
||||
2. `mvn -pl ruoyi-admin -am package -DskipTests`
|
||||
3. `jar tf ruoyi-admin/target/ruoyi-admin.jar | rg 'license.dat|tongweb'`
|
||||
4. `mvn -pl ruoyi-admin dependency:tree '-Dincludes=com.tongweb.springboot:*,com.tongweb:*,org.springframework.boot:spring-boot-starter-tomcat,org.apache.tomcat.embed:*'`
|
||||
@@ -0,0 +1,742 @@
|
||||
# Business Type History Rate Backend Implementation Plan
|
||||
|
||||
> **For agentic workers:** Follow this repository's `AGENTS.md`: do not enable subagents or `using-superpowers` during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add backend support for business type persistence, historical loan-rate lookup, and historical loan-rate model input for both personal and corporate workflow creation.
|
||||
|
||||
**Architecture:** Keep the existing workflow creation API shape and extend the request DTOs, entity, converter, service validation, SQL schema, and model DTO. Add one backend proxy service/controller endpoint for historical contracts, following the existing customer-map proxy pattern, plus a mock endpoint for local development and browser-use testing.
|
||||
|
||||
**Tech Stack:** Spring Boot, RuoYi, MyBatis Plus, Lombok, JUnit 5, Mockito, Spring `MockRestServiceServer`, MySQL schema SQL.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.java`
|
||||
- Add `businessType` and `loanRateHistory`.
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.java`
|
||||
- Add the same fields.
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
|
||||
- Add persisted fields `businessType` and `loanRateHistory`.
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/ModelInvokeDTO.java`
|
||||
- Add `loanRateHistory` only.
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVO.java`
|
||||
- Represent external historical contract rows with underscore JSON names.
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
|
||||
- Map `businessType` and `loanRateHistory` for personal and corporate create DTOs.
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanRateHistoryService.java`
|
||||
- Proxy external historical contract lookup.
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
|
||||
- Add `GET /loanPricing/workflow/history-contract`.
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
|
||||
- Add service-layer cross-field validation before insert/model invocation.
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
|
||||
- Add mock historical contract endpoint with fixed normal, empty, and empty-rate scenarios.
|
||||
- Add fixed customer-map mock customer IDs that return `cust_isn=EMPTY_HISTORY` and `cust_isn=EMPTY_RATE` for browser-use testing.
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
|
||||
- Add local mock `loan-rate-history.url`.
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-uat.yml`
|
||||
- Add local mock `loan-rate-history.url`.
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-pro.yml`
|
||||
- Add real external `loan-rate-history.url` without `cust_isn=`.
|
||||
- Create: `sql/add_business_type_history_rate_20260429.sql`
|
||||
- Migration for existing databases.
|
||||
- Modify: `sql/loan_pricing_workflow.sql`
|
||||
- Update standalone workflow schema.
|
||||
- Modify: `sql/loan_pricing_schema_20260328.sql`
|
||||
- Update bundled schema.
|
||||
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
|
||||
- Update production init schema.
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVOTest.java`
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanRateHistoryServiceTest.java`
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerHistoryContractTest.java`
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerHistoryContractTest.java`
|
||||
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java`
|
||||
- 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`
|
||||
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
|
||||
|
||||
## Task 1: Add Backend Field Contracts
|
||||
|
||||
**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`
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVO.java`
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVOTest.java`
|
||||
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
|
||||
|
||||
- [ ] **Step 1: Write failing field tests**
|
||||
|
||||
Add to `LoanPricingModelServicePersonalParamsTest`:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldContainBusinessTypeAndLoanRateHistoryInCreateDtosAndWorkflow() throws NoSuchFieldException {
|
||||
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("businessType"));
|
||||
assertNotNull(PersonalLoanPricingCreateDTO.class.getDeclaredField("loanRateHistory"));
|
||||
assertNotNull(CorporateLoanPricingCreateDTO.class.getDeclaredField("businessType"));
|
||||
assertNotNull(CorporateLoanPricingCreateDTO.class.getDeclaredField("loanRateHistory"));
|
||||
assertNotNull(LoanPricingWorkflow.class.getDeclaredField("businessType"));
|
||||
assertNotNull(LoanPricingWorkflow.class.getDeclaredField("loanRateHistory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldContainLoanRateHistoryButNotBusinessTypeInModelInvokeDto() throws NoSuchFieldException {
|
||||
assertNotNull(ModelInvokeDTO.class.getDeclaredField("loanRateHistory"));
|
||||
assertThrows(NoSuchFieldException.class, () -> ModelInvokeDTO.class.getDeclaredField("businessType"));
|
||||
}
|
||||
```
|
||||
|
||||
Create `HistoryLoanContractVOTest`:
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.domain.vo;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class HistoryLoanContractVOTest {
|
||||
@Test
|
||||
void shouldDeserializeUnderscoreFields() throws Exception {
|
||||
String json = "{\"cust_isn\":\"81033011438\",\"loan_contract_history\":\"HT001\",\"guar_type_history\":\"信用\",\"product_code_history\":\"P001\",\"loan_rate_history\":\"3.65\",\"loan_amount_history\":\"100000\",\"loan_sign_date_history\":\"2025-01-01\"}";
|
||||
|
||||
HistoryLoanContractVO vo = new ObjectMapper().readValue(json, HistoryLoanContractVO.class);
|
||||
|
||||
assertNotNull(vo.getCustIsn());
|
||||
assertNotNull(vo.getLoanContractHistory());
|
||||
assertNotNull(vo.getLoanRateHistory());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,HistoryLoanContractVOTest test
|
||||
```
|
||||
|
||||
Expected: FAIL because the fields and VO do not exist.
|
||||
|
||||
- [ ] **Step 3: Add DTO/entity/model fields**
|
||||
|
||||
Add to both create DTOs:
|
||||
|
||||
```java
|
||||
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"新客", "存量新增", "存量转贷"})
|
||||
@NotBlank(message = "业务种类不能为空")
|
||||
@Pattern(regexp = "^(新客|存量新增|存量转贷)$", message = "业务种类必须是:新客、存量新增、存量转贷之一")
|
||||
private String businessType;
|
||||
|
||||
@Schema(description = "历史贷款利率", example = "3.65")
|
||||
private String loanRateHistory;
|
||||
```
|
||||
|
||||
Add to `LoanPricingWorkflow`:
|
||||
|
||||
```java
|
||||
/** 业务种类: 新客/存量新增/存量转贷 */
|
||||
private String businessType;
|
||||
|
||||
/** 历史贷款利率 */
|
||||
private String loanRateHistory;
|
||||
```
|
||||
|
||||
Add only this field to `ModelInvokeDTO`:
|
||||
|
||||
```java
|
||||
/**
|
||||
* 历史贷款利率
|
||||
*/
|
||||
private String loanRateHistory;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create `HistoryLoanContractVO`**
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class HistoryLoanContractVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonProperty("cust_isn")
|
||||
private String custIsn;
|
||||
|
||||
@JsonProperty("loan_contract_history")
|
||||
private String loanContractHistory;
|
||||
|
||||
@JsonProperty("guar_type_history")
|
||||
private String guarTypeHistory;
|
||||
|
||||
@JsonProperty("product_code_history")
|
||||
private String productCodeHistory;
|
||||
|
||||
@JsonProperty("loan_rate_history")
|
||||
private String loanRateHistory;
|
||||
|
||||
@JsonProperty("loan_amount_history")
|
||||
private String loanAmountHistory;
|
||||
|
||||
@JsonProperty("loan_sign_date_history")
|
||||
private String loanSignDateHistory;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run field tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,HistoryLoanContractVOTest test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```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/domain/vo/HistoryLoanContractVO.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/HistoryLoanContractVOTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java
|
||||
git commit -m "新增业务种类与历史利率后端字段"
|
||||
```
|
||||
|
||||
## Task 2: Map and Validate Workflow Creation
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
|
||||
- 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/LoanPricingModelServicePersonalParamsTest.java`
|
||||
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java`
|
||||
|
||||
- [ ] **Step 1: Write failing converter tests**
|
||||
|
||||
Add:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldMapBusinessTypeAndLoanRateHistoryFromPersonalDto() {
|
||||
PersonalLoanPricingCreateDTO dto = new PersonalLoanPricingCreateDTO();
|
||||
dto.setCustIsn("CUST001");
|
||||
dto.setGuarType("信用");
|
||||
dto.setApplyAmt("100000");
|
||||
dto.setBusinessType("存量转贷");
|
||||
dto.setLoanRateHistory("3.65");
|
||||
|
||||
LoanPricingWorkflow workflow = LoanPricingConverter.toEntity(dto);
|
||||
|
||||
assertEquals("存量转贷", workflow.getBusinessType());
|
||||
assertEquals("3.65", workflow.getLoanRateHistory());
|
||||
}
|
||||
```
|
||||
|
||||
Add a similar corporate DTO test in `LoanPricingModelServiceTest` or a new converter-focused test.
|
||||
|
||||
- [ ] **Step 2: Write failing service validation tests**
|
||||
|
||||
Add to `LoanPricingWorkflowServiceImplTest`:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldRejectMissingBusinessTypeBeforeInsert() {
|
||||
LoanPricingWorkflow workflow = validWorkflow();
|
||||
workflow.setBusinessType(null);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class, () -> loanPricingWorkflowService.createLoanPricing(workflow));
|
||||
|
||||
assertEquals("业务种类不能为空", ex.getMessage());
|
||||
verify(loanPricingWorkflowMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectInvalidBusinessTypeBeforeInsert() {
|
||||
LoanPricingWorkflow workflow = validWorkflow();
|
||||
workflow.setBusinessType("其他");
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class, () -> loanPricingWorkflowService.createLoanPricing(workflow));
|
||||
|
||||
assertEquals("业务种类必须是:新客、存量新增、存量转贷之一", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectStockTransferWithoutLoanRateHistory() {
|
||||
LoanPricingWorkflow workflow = validWorkflow();
|
||||
workflow.setBusinessType("存量转贷");
|
||||
workflow.setLoanRateHistory(" ");
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class, () -> loanPricingWorkflowService.createLoanPricing(workflow));
|
||||
|
||||
assertEquals("请选择历史贷款合同", ex.getMessage());
|
||||
}
|
||||
```
|
||||
|
||||
Add helper:
|
||||
|
||||
```java
|
||||
private LoanPricingWorkflow validWorkflow() {
|
||||
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
|
||||
workflow.setCustIsn("81033011438");
|
||||
workflow.setCustType("个人");
|
||||
workflow.setCustName("张三");
|
||||
workflow.setIdNum("110101199001011234");
|
||||
workflow.setGuarType("信用");
|
||||
workflow.setApplyAmt("100000");
|
||||
workflow.setBusinessType("新客");
|
||||
return workflow;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run tests to verify they fail**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest test
|
||||
```
|
||||
|
||||
Expected: FAIL because mapping and validation do not exist.
|
||||
|
||||
- [ ] **Step 4: Implement converter mapping**
|
||||
|
||||
Add to both `toEntity(...)` methods:
|
||||
|
||||
```java
|
||||
entity.setBusinessType(dto.getBusinessType());
|
||||
entity.setLoanRateHistory(dto.getLoanRateHistory());
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Implement service validation**
|
||||
|
||||
In `LoanPricingWorkflowServiceImpl`, before defaults/encryption/insert in `createLoanPricing(...)`, call:
|
||||
|
||||
```java
|
||||
validateBusinessTypeAndHistoryRate(loanPricingWorkflow);
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```java
|
||||
private void validateBusinessTypeAndHistoryRate(LoanPricingWorkflow workflow) {
|
||||
if (!StringUtils.hasText(workflow.getBusinessType())) {
|
||||
throw new ServiceException("业务种类不能为空");
|
||||
}
|
||||
if (!"新客".equals(workflow.getBusinessType())
|
||||
&& !"存量新增".equals(workflow.getBusinessType())
|
||||
&& !"存量转贷".equals(workflow.getBusinessType())) {
|
||||
throw new ServiceException("业务种类必须是:新客、存量新增、存量转贷之一");
|
||||
}
|
||||
if ("存量转贷".equals(workflow.getBusinessType())
|
||||
&& !StringUtils.hasText(workflow.getLoanRateHistory())) {
|
||||
throw new ServiceException("请选择历史贷款合同");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Import `com.ruoyi.common.exception.ServiceException`.
|
||||
|
||||
- [ ] **Step 6: Run validation tests**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java \
|
||||
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImplTest.java
|
||||
git commit -m "校验并映射业务种类历史利率"
|
||||
```
|
||||
|
||||
## Task 3: Add Historical Contract Proxy and Mock
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanRateHistoryService.java`
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-uat.yml`
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-pro.yml`
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanRateHistoryServiceTest.java`
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerHistoryContractTest.java`
|
||||
- Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerHistoryContractTest.java`
|
||||
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java`
|
||||
|
||||
- [ ] **Step 1: Write failing service tests**
|
||||
|
||||
Create tests following `LoanPricingCustomerMapServiceTest`:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldQueryHistoryContractsWithCustIsnParam() {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
|
||||
LoanRateHistoryService service = new LoanRateHistoryService(restTemplate, "http://mock/history?appCode=abc");
|
||||
|
||||
server.expect(requestTo("http://mock/history?appCode=abc&cust_isn=81033011438"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"data\":[{\"cust_isn\":\"81033011438\",\"loan_contract_history\":\"HT001\",\"loan_rate_history\":\"3.65\"}]}", MediaType.APPLICATION_JSON));
|
||||
|
||||
List<HistoryLoanContractVO> result = service.query(" 81033011438 ");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("3.65", result.get(0).getLoanRateHistory());
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectBlankCustIsn() {
|
||||
LoanRateHistoryService service = new LoanRateHistoryService(new RestTemplate(), "http://mock/history");
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class, () -> service.query(" "));
|
||||
|
||||
assertEquals("客户内码不能为空", ex.getMessage());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write failing controller/mock tests**
|
||||
|
||||
Mock endpoint tests must cover:
|
||||
|
||||
- Normal `cust_isn=81033011438` returns at least one row with `loan_rate_history`.
|
||||
- Fixed empty scenario, for example `cust_isn=EMPTY_HISTORY`, returns `data: []`.
|
||||
- Fixed empty-rate scenario, for example `cust_isn=EMPTY_RATE`, returns a row whose `loan_rate_history` is empty.
|
||||
|
||||
Customer-map mock tests must also cover browser-use fixed customer numbers:
|
||||
|
||||
- `cust_id=HISTORY_EMPTY` returns exactly one customer-map row with `cust_isn=EMPTY_HISTORY`.
|
||||
- `cust_id=HISTORY_EMPTY_RATE` returns exactly one customer-map row with `cust_isn=EMPTY_RATE`.
|
||||
|
||||
Controller test must verify `/loanPricing/workflow/history-contract?custIsn=81033011438` delegates to `LoanRateHistoryService.query("81033011438")`.
|
||||
|
||||
- [ ] **Step 3: Run tests to verify they fail**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRateHistoryServiceTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowControllerHistoryContractTest test
|
||||
```
|
||||
|
||||
Expected: FAIL because service/controller/mock do not exist.
|
||||
|
||||
- [ ] **Step 4: Implement `LoanRateHistoryService`**
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class LoanRateHistoryService {
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Value("${loan-rate-history.url}")
|
||||
private String historyUrl;
|
||||
|
||||
public LoanRateHistoryService() {
|
||||
this(new RestTemplate());
|
||||
}
|
||||
|
||||
LoanRateHistoryService(RestTemplate restTemplate) {
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
LoanRateHistoryService(RestTemplate restTemplate, String historyUrl) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.historyUrl = historyUrl;
|
||||
}
|
||||
|
||||
public List<HistoryLoanContractVO> query(String custIsn) {
|
||||
String normalizedCustIsn = StringUtils.trimWhitespace(custIsn);
|
||||
if (!StringUtils.hasText(normalizedCustIsn)) {
|
||||
throw new ServiceException("客户内码不能为空");
|
||||
}
|
||||
URI uri = UriComponentsBuilder.fromHttpUrl(historyUrl)
|
||||
.queryParam("cust_isn", normalizedCustIsn)
|
||||
.build()
|
||||
.encode()
|
||||
.toUri();
|
||||
HistoryLoanContractResponse response = restTemplate.getForObject(uri, HistoryLoanContractResponse.class);
|
||||
if (response == null) {
|
||||
throw new ServiceException("历史贷款合同接口无返回");
|
||||
}
|
||||
if (response.getCode() != null && response.getCode() != 200) {
|
||||
throw new ServiceException(StringUtils.hasText(response.getMsg()) ? response.getMsg() : "历史贷款合同查询失败");
|
||||
}
|
||||
return response.getData() == null ? Collections.emptyList() : response.getData();
|
||||
}
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static class HistoryLoanContractResponse {
|
||||
private Integer code;
|
||||
private String msg;
|
||||
private List<HistoryLoanContractVO> data;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add controller endpoint**
|
||||
|
||||
Inject `LoanRateHistoryService` into `LoanPricingWorkflowController` and add:
|
||||
|
||||
```java
|
||||
@Operation(summary = "查询历史贷款合同")
|
||||
@GetMapping("/history-contract")
|
||||
public AjaxResult queryHistoryContract(@RequestParam("custIsn") String custIsn) {
|
||||
return success(loanRateHistoryService.query(custIsn));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Add mock endpoint**
|
||||
|
||||
Add to `LoanRatePricingMockController`:
|
||||
|
||||
```java
|
||||
@Anonymous
|
||||
@Operation(summary = "模拟历史贷款合同查询")
|
||||
@GetMapping("/history-contract")
|
||||
public AjaxResult queryHistoryContract(@RequestParam("cust_isn") String custIsn) {
|
||||
String normalizedCustIsn = StringUtils.trimWhitespace(custIsn);
|
||||
if (!StringUtils.hasText(normalizedCustIsn)) {
|
||||
throw new ServiceException("客户内码不能为空");
|
||||
}
|
||||
if ("EMPTY_HISTORY".equals(normalizedCustIsn)) {
|
||||
return success(Collections.emptyList());
|
||||
}
|
||||
List<HistoryLoanContractVO> records = new ArrayList<>();
|
||||
HistoryLoanContractVO record = new HistoryLoanContractVO();
|
||||
record.setCustIsn(normalizedCustIsn);
|
||||
record.setLoanContractHistory("HT" + normalizedCustIsn);
|
||||
record.setGuarTypeHistory("信用");
|
||||
record.setProductCodeHistory("P001");
|
||||
record.setLoanRateHistory("EMPTY_RATE".equals(normalizedCustIsn) ? "" : "3.65");
|
||||
record.setLoanAmountHistory("100000");
|
||||
record.setLoanSignDateHistory(LocalDate.now().minusMonths(6).toString());
|
||||
records.add(record);
|
||||
return success(records);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Add fixed customer-map mock scenarios**
|
||||
|
||||
Before the random-record loop in `randomCustomerMapRecords`, add:
|
||||
|
||||
```java
|
||||
if ("HISTORY_EMPTY".equals(normalizedCustId)) {
|
||||
CustomerMapRecordVO record = new CustomerMapRecordVO();
|
||||
record.setCustId(normalizedCustId);
|
||||
record.setCustIsn("EMPTY_HISTORY");
|
||||
record.setCustName(namePrefix + "空历史合同");
|
||||
record.setFaithDay("0");
|
||||
record.setBalanceAvg("0");
|
||||
record.setLoanCountHis("0");
|
||||
record.setLastLoanDate("");
|
||||
return Collections.singletonList(record);
|
||||
}
|
||||
if ("HISTORY_EMPTY_RATE".equals(normalizedCustId)) {
|
||||
CustomerMapRecordVO record = new CustomerMapRecordVO();
|
||||
record.setCustId(normalizedCustId);
|
||||
record.setCustIsn("EMPTY_RATE");
|
||||
record.setCustName(namePrefix + "空历史利率");
|
||||
record.setFaithDay("30");
|
||||
record.setBalanceAvg("10000");
|
||||
record.setLoanCountHis("1");
|
||||
record.setLastLoanDate(LocalDate.now().minusMonths(3).toString());
|
||||
return Collections.singletonList(record);
|
||||
}
|
||||
```
|
||||
|
||||
Import `java.util.Collections` if needed.
|
||||
|
||||
- [ ] **Step 8: Add profile config**
|
||||
|
||||
Use mock URLs in dev/uat:
|
||||
|
||||
```yaml
|
||||
loan-rate-history:
|
||||
url: http://localhost:63310/rate/pricing/mock/history-contract
|
||||
```
|
||||
|
||||
Use real URL in pro without `cust_isn=`:
|
||||
|
||||
```yaml
|
||||
loan-rate-history:
|
||||
url: http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_loan_rate_history?appCode=1a89fa84abda480ba93ed73fd01ffd07
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Run proxy/mock tests**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRateHistoryServiceTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowControllerHistoryContractTest,LoanRatePricingMockControllerCustomerMapTest test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 10: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanRateHistoryService.java \
|
||||
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java \
|
||||
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java \
|
||||
ruoyi-admin/src/main/resources/application-dev.yml \
|
||||
ruoyi-admin/src/main/resources/application-uat.yml \
|
||||
ruoyi-admin/src/main/resources/application-pro.yml \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanRateHistoryServiceTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerHistoryContractTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerHistoryContractTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java
|
||||
git commit -m "新增历史贷款合同查询接口"
|
||||
```
|
||||
|
||||
## Task 4: Persist SQL Schema Changes
|
||||
|
||||
**Files:**
|
||||
- Create: `sql/add_business_type_history_rate_20260429.sql`
|
||||
- Modify: `sql/loan_pricing_workflow.sql`
|
||||
- Modify: `sql/loan_pricing_schema_20260328.sql`
|
||||
- Modify: `sql/loan_pricing_prod_init_20260331.sql`
|
||||
|
||||
- [ ] **Step 1: Write migration SQL**
|
||||
|
||||
Create:
|
||||
|
||||
```sql
|
||||
-- 为利率定价流程添加业务种类和历史贷款利率字段
|
||||
ALTER TABLE `loan_pricing_workflow`
|
||||
ADD COLUMN `business_type` varchar(20) DEFAULT NULL COMMENT '业务种类' AFTER `loan_purpose`,
|
||||
ADD COLUMN `loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率' AFTER `business_type`;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update schema SQL files**
|
||||
|
||||
In each `loan_pricing_workflow` create-table definition, add:
|
||||
|
||||
```sql
|
||||
`business_type` varchar(20) DEFAULT NULL COMMENT '业务种类',
|
||||
`loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率',
|
||||
```
|
||||
|
||||
Place them after `loan_purpose`.
|
||||
|
||||
- [ ] **Step 3: Verify SQL text**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n "business_type|loan_rate_history" sql/add_business_type_history_rate_20260429.sql sql/loan_pricing_workflow.sql sql/loan_pricing_schema_20260328.sql sql/loan_pricing_prod_init_20260331.sql
|
||||
```
|
||||
|
||||
Expected: each file contains both fields in the workflow table context.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add sql/add_business_type_history_rate_20260429.sql \
|
||||
sql/loan_pricing_workflow.sql \
|
||||
sql/loan_pricing_schema_20260328.sql \
|
||||
sql/loan_pricing_prod_init_20260331.sql
|
||||
git commit -m "新增业务种类历史利率数据库字段"
|
||||
```
|
||||
|
||||
## Task 5: Verify Model Input Chain
|
||||
|
||||
**Files:**
|
||||
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java`
|
||||
- Modify Test: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java`
|
||||
|
||||
- [ ] **Step 1: Add personal model assertion**
|
||||
|
||||
In `shouldInvokePersonalModelWithExpectedParams`, set:
|
||||
|
||||
```java
|
||||
workflow.setBusinessType("存量转贷");
|
||||
workflow.setLoanRateHistory("3.65");
|
||||
```
|
||||
|
||||
Add to the `argThat`:
|
||||
|
||||
```java
|
||||
&& Objects.equals("3.65", dto.getLoanRateHistory())
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add corporate model assertion**
|
||||
|
||||
Add or update a corporate model invocation test in `LoanPricingModelServiceTest`:
|
||||
|
||||
```java
|
||||
workflow.setCustType("企业");
|
||||
workflow.setBusinessType("存量转贷");
|
||||
workflow.setLoanRateHistory("3.75");
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```java
|
||||
verify(modelService).invokeCorporateModel(argThat((ModelInvokeDTO dto) ->
|
||||
Objects.equals("3.75", dto.getLoanRateHistory())));
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run model tests**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest test
|
||||
```
|
||||
|
||||
Expected: PASS and no `businessType` field exists in `ModelInvokeDTO`.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServicePersonalParamsTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingModelServiceTest.java
|
||||
git commit -m "验证历史利率模型入参链路"
|
||||
```
|
||||
|
||||
## Task 6: Backend Final Verification
|
||||
|
||||
**Files:**
|
||||
- No new files unless a test failure requires a focused fix.
|
||||
|
||||
- [ ] **Step 1: Run focused backend test suite**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=HistoryLoanContractVOTest,LoanRateHistoryServiceTest,LoanPricingWorkflowControllerHistoryContractTest,LoanRatePricingMockControllerHistoryContractTest,LoanPricingWorkflowServiceImplTest,LoanPricingModelServicePersonalParamsTest,LoanPricingModelServiceTest test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run existing related customer-map tests**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest,LoanPricingWorkflowControllerCustomerMapTest,LoanRatePricingMockControllerCustomerMapTest,CustomerMapRecordVOTest test
|
||||
```
|
||||
|
||||
Expected: PASS, confirming the new proxy pattern did not break customer-map behavior.
|
||||
|
||||
- [ ] **Step 3: Record backend implementation notes**
|
||||
|
||||
Append backend verification notes to a new or existing implementation report, for example:
|
||||
|
||||
```text
|
||||
doc/implementation-report-2026-04-29-business-type-history-rate.md
|
||||
```
|
||||
|
||||
Include commands run and whether browser-use verification remains for the frontend plan.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add doc/implementation-report-2026-04-29-business-type-history-rate.md
|
||||
git commit -m "记录业务种类历史利率后端验证"
|
||||
```
|
||||
@@ -0,0 +1,732 @@
|
||||
# Business Type History Rate Frontend Implementation Plan
|
||||
|
||||
> **For agentic workers:** Follow this repository's `AGENTS.md`: do not enable subagents or `using-superpowers` during implementation unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add business type selection and historical loan-contract single-selection to the personal and corporate workflow creation UI, then verify the real page with browser-use.
|
||||
|
||||
**Architecture:** Reuse the current customer-map driven creation flow. Add one shared `HistoryContractSelector` component, one API function, fields and validation in both create dialogs, and detail display in both detail components. Final browser verification must use `browser-use:browser` with the in-app browser, not Playwright CLI or prototype pages.
|
||||
|
||||
**Tech Stack:** Vue 2, Element UI, RuoYi request wrapper, Node static tests, nvm Node 14.21.3, browser-use `iab` runtime for real page testing.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
|
||||
- Add `queryHistoryContracts(custIsn)`.
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue`
|
||||
- Shared modal for historical contract query results and single selection.
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
|
||||
- Add business type, history-rate display, query trigger, selection handling, and submit validation.
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
|
||||
- Same behavior for corporate creation.
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
|
||||
- Show business type and historical loan rate.
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
|
||||
- Show business type and historical loan rate.
|
||||
- Modify: `ruoyi-ui/package.json`
|
||||
- Add `test:business-type-history-rate`.
|
||||
- Create: `ruoyi-ui/tests/business-type-history-rate.test.js`
|
||||
- Static coverage for API, selector, create dialogs, validation, clearing, and detail display.
|
||||
- Modify: `doc/implementation-report-2026-04-29-business-type-history-rate.md`
|
||||
- Add frontend and browser-use verification notes after execution.
|
||||
|
||||
## Task 1: Add Frontend API and Static Test Skeleton
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
|
||||
- Modify: `ruoyi-ui/package.json`
|
||||
- Create: `ruoyi-ui/tests/business-type-history-rate.test.js`
|
||||
|
||||
- [ ] **Step 1: Write failing API assertions**
|
||||
|
||||
Create `ruoyi-ui/tests/business-type-history-rate.test.js`:
|
||||
|
||||
```js
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const assert = require('assert')
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
|
||||
}
|
||||
|
||||
const workflowApi = read('src/api/loanPricing/workflow.js')
|
||||
|
||||
assert(
|
||||
workflowApi.includes('export function queryHistoryContracts') &&
|
||||
workflowApi.includes("url: '/loanPricing/workflow/history-contract'") &&
|
||||
workflowApi.includes('params: { custIsn: custIsn }'),
|
||||
'缺少历史贷款合同查询 API'
|
||||
)
|
||||
|
||||
console.log('business type history rate assertions passed')
|
||||
```
|
||||
|
||||
Add package script:
|
||||
|
||||
```json
|
||||
"test:business-type-history-rate": "node tests/business-type-history-rate.test.js"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
|
||||
```
|
||||
|
||||
Expected: FAIL because the API function does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement API function**
|
||||
|
||||
Append to `workflow.js`:
|
||||
|
||||
```js
|
||||
// 查询历史贷款合同
|
||||
export function queryHistoryContracts(custIsn) {
|
||||
return request({
|
||||
url: '/loanPricing/workflow/history-contract',
|
||||
method: 'get',
|
||||
params: { custIsn: custIsn }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run API test**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/api/loanPricing/workflow.js ruoyi-ui/package.json ruoyi-ui/tests/business-type-history-rate.test.js
|
||||
git commit -m "新增历史贷款合同前端接口"
|
||||
```
|
||||
|
||||
## Task 2: Create Historical Contract Selector
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue`
|
||||
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
|
||||
|
||||
- [ ] **Step 1: Extend failing selector assertions**
|
||||
|
||||
Add:
|
||||
|
||||
```js
|
||||
const historySelector = read('src/views/loanPricing/workflow/components/HistoryContractSelector.vue')
|
||||
|
||||
assert(
|
||||
historySelector.includes('title="历史贷款合同选择"') &&
|
||||
historySelector.includes('width="80%"') &&
|
||||
historySelector.includes(':data="contracts"'),
|
||||
'历史贷款合同选择弹窗缺少标题、宽度或表格数据'
|
||||
)
|
||||
|
||||
;[
|
||||
'cust_isn',
|
||||
'loan_contract_history',
|
||||
'guar_type_history',
|
||||
'product_code_history',
|
||||
'loan_rate_history',
|
||||
'loan_amount_history',
|
||||
'loan_sign_date_history'
|
||||
].forEach((field) => {
|
||||
assert(historySelector.includes(`prop="${field}"`) || historySelector.includes(`row.${field}`), `历史合同弹窗缺少字段 ${field}`)
|
||||
})
|
||||
|
||||
assert(
|
||||
historySelector.includes('type="radio"') &&
|
||||
historySelector.includes('selectedContract') &&
|
||||
historySelector.includes("this.$emit('select', this.selectedContract)") &&
|
||||
historySelector.includes('请选择历史贷款合同'),
|
||||
'历史合同弹窗缺少单选、确定选择或未选提示'
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
|
||||
```
|
||||
|
||||
Expected: FAIL because the component does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement `HistoryContractSelector.vue`**
|
||||
|
||||
Use a small presentational component. It receives already-loaded `contracts` and does not call the API itself.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-dialog title="历史贷款合同选择" :visible.sync="dialogVisible" width="80%" append-to-body
|
||||
:close-on-click-modal="false" @close="handleClose">
|
||||
<el-table :data="contracts" v-loading="loading" @row-click="handleRowClick">
|
||||
<el-table-column label="选择" align="center" width="70">
|
||||
<template slot-scope="scope">
|
||||
<el-radio v-model="selectedContract" :label="scope.row"> </el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户内码" prop="cust_isn" align="center" :show-overflow-tooltip="true"/>
|
||||
<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_amount_history" align="center"/>
|
||||
<el-table-column label="历史贷款签订时间" prop="loan_sign_date_history" align="center" width="150"/>
|
||||
</el-table>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="confirmSelect">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
```
|
||||
|
||||
Script:
|
||||
|
||||
```js
|
||||
export default {
|
||||
name: "HistoryContractSelector",
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
contracts: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
return { selectedContract: null }
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
get() { return this.visible },
|
||||
set(val) { this.$emit('update:visible', val) }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleRowClick(row) {
|
||||
this.selectedContract = row
|
||||
},
|
||||
confirmSelect() {
|
||||
if (!this.selectedContract) {
|
||||
this.$modal.msgWarning("请选择历史贷款合同")
|
||||
return
|
||||
}
|
||||
if (!this.selectedContract.loan_rate_history) {
|
||||
this.$modal.msgWarning("历史贷款利率不能为空")
|
||||
return
|
||||
}
|
||||
this.$emit('select', this.selectedContract)
|
||||
this.dialogVisible = false
|
||||
},
|
||||
handleClose() {
|
||||
this.selectedContract = null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run selector test**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
|
||||
```
|
||||
|
||||
Expected: PASS for selector assertions.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/loanPricing/workflow/components/HistoryContractSelector.vue \
|
||||
ruoyi-ui/tests/business-type-history-rate.test.js
|
||||
git commit -m "新增历史贷款合同选择弹窗"
|
||||
```
|
||||
|
||||
## Task 3: Add Personal Create Dialog Behavior
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
|
||||
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
|
||||
|
||||
- [ ] **Step 1: Extend failing personal dialog assertions**
|
||||
|
||||
Add:
|
||||
|
||||
```js
|
||||
const personalCreate = read('src/views/loanPricing/workflow/components/PersonalCreateDialog.vue')
|
||||
|
||||
assert(
|
||||
personalCreate.includes('label="业务种类"') &&
|
||||
personalCreate.includes('v-model="form.businessType"') &&
|
||||
personalCreate.includes('新客') &&
|
||||
personalCreate.includes('存量新增') &&
|
||||
personalCreate.includes('存量转贷'),
|
||||
'个人新增弹窗缺少业务种类选择'
|
||||
)
|
||||
|
||||
assert(
|
||||
personalCreate.includes('label="历史贷款利率"') &&
|
||||
personalCreate.includes('v-model="form.loanRateHistory"') &&
|
||||
personalCreate.includes(':readonly="true"'),
|
||||
'个人新增弹窗缺少只读历史贷款利率'
|
||||
)
|
||||
|
||||
assert(
|
||||
personalCreate.includes('queryHistoryContracts') &&
|
||||
personalCreate.includes('HistoryContractSelector') &&
|
||||
personalCreate.includes('handleBusinessTypeChange') &&
|
||||
personalCreate.includes('handleHistoryContractSelect'),
|
||||
'个人新增弹窗缺少历史合同查询和选择逻辑'
|
||||
)
|
||||
|
||||
assert(
|
||||
personalCreate.includes('请选择历史贷款合同') &&
|
||||
personalCreate.includes('未查询到历史贷款合同') &&
|
||||
personalCreate.includes('delete data.loanRateHistory'),
|
||||
'个人新增弹窗缺少存量转贷拦截、空列表提示或非存量转贷清理'
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
|
||||
```
|
||||
|
||||
Expected: FAIL.
|
||||
|
||||
- [ ] **Step 3: Implement personal form fields**
|
||||
|
||||
In the loan information area, add:
|
||||
|
||||
```vue
|
||||
<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-select>
|
||||
</el-form-item>
|
||||
</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-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
```
|
||||
|
||||
Add selector under the form:
|
||||
|
||||
```vue
|
||||
<history-contract-selector
|
||||
:visible.sync="showHistorySelector"
|
||||
:contracts="historyContracts"
|
||||
:loading="historyLoading"
|
||||
@select="handleHistoryContractSelect"
|
||||
/>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Implement personal script**
|
||||
|
||||
Import:
|
||||
|
||||
```js
|
||||
import {createPersonalWorkflow, queryHistoryContracts} from "@/api/loanPricing/workflow"
|
||||
import HistoryContractSelector from "./HistoryContractSelector"
|
||||
```
|
||||
|
||||
Register component, then add state:
|
||||
|
||||
```js
|
||||
businessTypeOptions: ['新客', '存量新增', '存量转贷'],
|
||||
showHistorySelector: false,
|
||||
historyLoading: false,
|
||||
historyContracts: [],
|
||||
selectedHistoryContract: null,
|
||||
```
|
||||
|
||||
Add form fields in initial data and `reset()`:
|
||||
|
||||
```js
|
||||
businessType: undefined,
|
||||
loanRateHistory: undefined,
|
||||
```
|
||||
|
||||
Add rules:
|
||||
|
||||
```js
|
||||
businessType: [
|
||||
{required: true, message: "请选择业务种类", trigger: "change"}
|
||||
],
|
||||
loanRateHistory: [
|
||||
{required: true, message: "请选择历史贷款合同", trigger: "change"}
|
||||
]
|
||||
```
|
||||
|
||||
Add computed:
|
||||
|
||||
```js
|
||||
isStockTransfer() {
|
||||
return this.form.businessType === '存量转贷'
|
||||
}
|
||||
```
|
||||
|
||||
Add methods:
|
||||
|
||||
```js
|
||||
handleBusinessTypeChange(value) {
|
||||
this.clearHistoryContract()
|
||||
if (value === '存量转贷') {
|
||||
this.queryHistoryContracts()
|
||||
}
|
||||
},
|
||||
queryHistoryContracts() {
|
||||
if (!this.form.custIsn) {
|
||||
this.$modal.msgWarning("客户内码不能为空")
|
||||
return
|
||||
}
|
||||
this.historyLoading = true
|
||||
queryHistoryContracts(this.form.custIsn).then(response => {
|
||||
this.historyContracts = response.data || []
|
||||
if (this.historyContracts.length === 0) {
|
||||
this.$modal.msgWarning("未查询到历史贷款合同")
|
||||
return
|
||||
}
|
||||
this.showHistorySelector = true
|
||||
}).finally(() => {
|
||||
this.historyLoading = false
|
||||
})
|
||||
},
|
||||
handleHistoryContractSelect(row) {
|
||||
if (!row.loan_rate_history) {
|
||||
this.$modal.msgWarning("历史贷款利率不能为空")
|
||||
return
|
||||
}
|
||||
this.selectedHistoryContract = row
|
||||
this.form.loanRateHistory = row.loan_rate_history
|
||||
},
|
||||
clearHistoryContract() {
|
||||
this.selectedHistoryContract = null
|
||||
this.form.loanRateHistory = undefined
|
||||
this.historyContracts = []
|
||||
}
|
||||
```
|
||||
|
||||
In `submitForm`, before setting `submitting`:
|
||||
|
||||
```js
|
||||
if (this.isStockTransfer && !this.form.loanRateHistory) {
|
||||
this.$modal.msgWarning("请选择历史贷款合同")
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
When building `data`, remove historical rate for non-stock-transfer:
|
||||
|
||||
```js
|
||||
if (!this.isStockTransfer) {
|
||||
delete data.loanRateHistory
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run personal static test**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
|
||||
```
|
||||
|
||||
Expected: PASS for personal assertions.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue \
|
||||
ruoyi-ui/tests/business-type-history-rate.test.js
|
||||
git commit -m "个人新增支持业务种类和历史利率"
|
||||
```
|
||||
|
||||
## Task 4: Add Corporate Create Dialog Behavior
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
|
||||
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
|
||||
|
||||
- [ ] **Step 1: Extend failing corporate assertions**
|
||||
|
||||
Mirror the personal assertions for `CorporateCreateDialog.vue`, replacing assertion messages with “企业新增弹窗...”.
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
|
||||
```
|
||||
|
||||
Expected: FAIL.
|
||||
|
||||
- [ ] **Step 3: Implement corporate fields and selector**
|
||||
|
||||
Apply the same form, selector, state, rules, computed, and methods from Task 3, but keep existing corporate-specific fields and submission conversion intact.
|
||||
|
||||
- [ ] **Step 4: Keep collateral and green/trade behavior untouched**
|
||||
|
||||
When editing submit payload, keep:
|
||||
|
||||
```js
|
||||
isGreenLoan: this.form.isGreenLoan ? '1' : '0',
|
||||
isTradeBuildEnt: this.form.isTradeBuildEnt ? '1' : '0'
|
||||
```
|
||||
|
||||
Do not reintroduce `repayMethod` UI.
|
||||
|
||||
- [ ] **Step 5: Run corporate static test**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue \
|
||||
ruoyi-ui/tests/business-type-history-rate.test.js
|
||||
git commit -m "企业新增支持业务种类和历史利率"
|
||||
```
|
||||
|
||||
## Task 5: Add Detail Display
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
|
||||
- Modify: `ruoyi-ui/tests/business-type-history-rate.test.js`
|
||||
|
||||
- [ ] **Step 1: Add failing detail assertions**
|
||||
|
||||
```js
|
||||
const personalDetail = read('src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue')
|
||||
const corporateDetail = read('src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue')
|
||||
|
||||
;[
|
||||
['个人详情', personalDetail],
|
||||
['企业详情', corporateDetail]
|
||||
].forEach(([name, source]) => {
|
||||
assert(source.includes('label="业务种类"') && source.includes('detailData.businessType'), `${name} 缺少业务种类展示`)
|
||||
assert(source.includes('label="历史贷款利率"') && source.includes('detailData.loanRateHistory'), `${name} 缺少历史贷款利率展示`)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
|
||||
```
|
||||
|
||||
Expected: FAIL.
|
||||
|
||||
- [ ] **Step 3: Add personal detail fields**
|
||||
|
||||
In personal “业务信息” descriptions, add:
|
||||
|
||||
```vue
|
||||
<el-descriptions-item label="业务种类">{{ detailData.businessType || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="历史贷款利率">{{ detailData.loanRateHistory || '-' }}</el-descriptions-item>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add corporate detail fields**
|
||||
|
||||
Add the same two fields to corporate “业务信息” descriptions.
|
||||
|
||||
- [ ] **Step 5: Run detail test**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue \
|
||||
ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue \
|
||||
ruoyi-ui/tests/business-type-history-rate.test.js
|
||||
git commit -m "详情展示业务种类和历史利率"
|
||||
```
|
||||
|
||||
## Task 6: Frontend Static Verification
|
||||
|
||||
**Files:**
|
||||
- No new files unless tests expose a focused defect.
|
||||
|
||||
- [ ] **Step 1: Run new focused test**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:business-type-history-rate'
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run related existing frontend tests**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/customer-map-selection.test.js && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params'
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Build frontend**
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 4: Commit only if fixes were required**
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: no unexpected frontend changes beyond this plan.
|
||||
|
||||
## Task 7: Real Page Verification With browser-use
|
||||
|
||||
**Files:**
|
||||
- Modify: `doc/implementation-report-2026-04-29-business-type-history-rate.md`
|
||||
|
||||
- [ ] **Step 1: Start backend with latest code**
|
||||
|
||||
Start or restart the backend on port `63310` using the repo's existing scripts or Maven command. Confirm the backend has loaded the latest backend code before browser testing.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
bin/restart_java_backend_test.sh
|
||||
```
|
||||
|
||||
Expected: backend listens on `63310`.
|
||||
|
||||
- [ ] **Step 2: Start frontend with Node 14**
|
||||
|
||||
Use nvm-managed Node:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run dev -- --port 8080'
|
||||
```
|
||||
|
||||
Expected: frontend dev server is available at `http://localhost:8080`.
|
||||
|
||||
- [ ] **Step 3: Initialize browser-use**
|
||||
|
||||
Use the Node REPL `js` tool and the in-app browser backend. First browser cell:
|
||||
|
||||
```js
|
||||
if (!globalThis.agent) {
|
||||
const { setupAtlasRuntime } = await import("/Users/wkc/.codex/plugins/cache/openai-bundled/browser-use/0.1.0-alpha1/scripts/browser-client.mjs");
|
||||
const backend = "iab";
|
||||
await setupAtlasRuntime({ globals: globalThis, backend });
|
||||
}
|
||||
await agent.browser.nameSession("🔎 利率定价历史利率测试");
|
||||
if (typeof tab === "undefined") {
|
||||
globalThis.tab = await agent.browser.tabs.new();
|
||||
}
|
||||
await tab.goto("http://localhost:8080");
|
||||
await tab.playwright.waitForLoadState({ state: "domcontentloaded", timeoutMs: 10000 });
|
||||
console.log(await tab.title());
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Log in on the real page**
|
||||
|
||||
Use the real login page, not a prototype. If the default test login path is available, use the project-supported login route. Otherwise log in through the visible login form with the configured test account.
|
||||
|
||||
After login, verify that the actual workflow list is visible.
|
||||
|
||||
- [ ] **Step 5: Verify personal new customer path**
|
||||
|
||||
In the browser:
|
||||
|
||||
1. Open 利率定价流程 list.
|
||||
2. Click 新增.
|
||||
3. Select 个人客户.
|
||||
4. Query a customer number and choose a customer internal code.
|
||||
5. In the personal create dialog, choose 业务种类 = 新客.
|
||||
6. Confirm no historical contract selector appears.
|
||||
7. Fill required fields and submit.
|
||||
8. Open detail and verify 业务种类 displays 新客 and 历史贷款利率 is empty/`-`.
|
||||
|
||||
- [ ] **Step 6: Verify personal existing-new path**
|
||||
|
||||
Repeat personal creation with 业务种类 = 存量新增. Confirm no historical contract selector appears, submit succeeds, and detail displays 存量新增.
|
||||
|
||||
- [ ] **Step 7: Verify personal stock-transfer path**
|
||||
|
||||
Repeat personal creation with 业务种类 = 存量转贷. Confirm:
|
||||
|
||||
1. Historical contract selector opens.
|
||||
2. All 7 columns are visible.
|
||||
3. A single radio selection is possible.
|
||||
4. Selecting a row fills 历史贷款利率.
|
||||
5. Submit succeeds.
|
||||
6. Detail displays 业务种类 = 存量转贷 and the chosen 历史贷款利率.
|
||||
|
||||
- [ ] **Step 8: Verify corporate three paths**
|
||||
|
||||
Repeat Steps 5-7 for 企业客户: 新客、存量新增、存量转贷.
|
||||
|
||||
- [ ] **Step 9: Verify blocked stock-transfer submission**
|
||||
|
||||
Use a stock-transfer flow and attempt to submit without selecting a historical contract. Confirm the page blocks submission and shows “请选择历史贷款合同”.
|
||||
|
||||
- [ ] **Step 10: Verify empty history scenario**
|
||||
|
||||
Use the fixed customer-map mock customer number `HISTORY_EMPTY`. In the customer number selector, query `HISTORY_EMPTY`, select the returned row with `cust_isn=EMPTY_HISTORY`, then choose 业务种类 = 存量转贷. Confirm “未查询到历史贷款合同” appears and submission remains blocked.
|
||||
|
||||
- [ ] **Step 11: Verify empty historical-rate scenario**
|
||||
|
||||
Use the fixed customer-map mock customer number `HISTORY_EMPTY_RATE`. In the customer number selector, query `HISTORY_EMPTY_RATE`, select the returned row with `cust_isn=EMPTY_RATE`, then choose 业务种类 = 存量转贷. Confirm the history selector opens with a row whose `loan_rate_history` is empty; selecting it and clicking 确定 must show “历史贷款利率不能为空”, must not fill the create form's historical loan-rate field, and submission must remain blocked.
|
||||
|
||||
- [ ] **Step 12: Capture evidence**
|
||||
|
||||
Capture screenshots or DOM snapshots for:
|
||||
|
||||
- Personal stock-transfer selector with all 7 columns.
|
||||
- Corporate stock-transfer selector with all 7 columns.
|
||||
- Detail page showing 业务种类 and 历史贷款利率.
|
||||
- Blocked submit warning.
|
||||
- Empty-history warning from `HISTORY_EMPTY`.
|
||||
- Empty-rate warning from `HISTORY_EMPTY_RATE`.
|
||||
|
||||
Use browser-use screenshots through `await display(await tab.playwright.screenshot({ fullPage: false }))` when visual confirmation matters.
|
||||
|
||||
- [ ] **Step 13: Stop test processes**
|
||||
|
||||
Stop only the backend/frontend processes started for this test. Do not kill unrelated user processes.
|
||||
|
||||
- [ ] **Step 14: Record verification**
|
||||
|
||||
Update:
|
||||
|
||||
```text
|
||||
doc/implementation-report-2026-04-29-business-type-history-rate.md
|
||||
```
|
||||
|
||||
Include:
|
||||
|
||||
- Static test commands and results.
|
||||
- Backend/frontend server commands.
|
||||
- browser-use URL and scenarios covered.
|
||||
- Confirmation that test processes were stopped.
|
||||
|
||||
- [ ] **Step 15: Commit verification notes**
|
||||
|
||||
```bash
|
||||
git add doc/implementation-report-2026-04-29-business-type-history-rate.md
|
||||
git commit -m "记录业务种类历史利率页面验证"
|
||||
```
|
||||
@@ -0,0 +1,678 @@
|
||||
# Customer Map Selection Backend Implementation Plan
|
||||
|
||||
> **For agentic workers:** Steps use checkbox (`- [ ]`) syntax for tracking. Follow this repository's AGENTS.md rule: do not enable subagents or superpowers execution modes unless the user explicitly requests them for the implementation session.
|
||||
|
||||
**Goal:** Add backend customer-id-to-customer-internal-code query APIs for personal and corporate workflow creation, backed by local mock interfaces and profile configuration.
|
||||
|
||||
**Architecture:** Keep workflow creation APIs unchanged. Add a small customer-map service that reads separate personal/corporate URLs from configuration, calls them with GET `cust_id`, and returns mapping records with underscore JSON field names through the existing RuoYi `AjaxResult` response convention. Add mock endpoints under the existing mock controller so all active profiles can point to local mock URLs first.
|
||||
|
||||
**Tech Stack:** Spring Boot, RuoYi `AjaxResult`, Spring `RestTemplate`, Jackson `@JsonProperty`, JUnit 5, Mockito, Maven.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVO.java`
|
||||
- Holds one customer mapping record.
|
||||
- Uses Java camelCase fields internally and `@JsonProperty` to serialize/deserialize underscore field names.
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapService.java`
|
||||
- Owns config URL selection, GET forwarding, parameter validation, and response parsing.
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
|
||||
- Adds authenticated business endpoints used by the frontend.
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
|
||||
- Adds anonymous local mock endpoints.
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
|
||||
- Adds `customer-map` mock URL config.
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-uat.yml`
|
||||
- Adds `customer-map` mock URL config.
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-pro.yml`
|
||||
- Adds `customer-map` mock URL config for the production profile, currently pointing to local mock as requested.
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVOTest.java`
|
||||
- Verifies JSON field names stay underscored.
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapServiceTest.java`
|
||||
- Verifies personal/corporate URL routing, `cust_id` forwarding, response parsing, and missing customer-id errors.
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java`
|
||||
- Verifies mock endpoints return one or more underscore-field records.
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerCustomerMapTest.java`
|
||||
- Verifies business controller methods delegate to the service and preserve the result records.
|
||||
|
||||
## Task 1: Add Customer Map Record VO
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVO.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVOTest.java`
|
||||
|
||||
- [ ] **Step 1: Write the failing serialization test**
|
||||
|
||||
Create `CustomerMapRecordVOTest`:
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.domain.vo;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class CustomerMapRecordVOTest
|
||||
{
|
||||
@Test
|
||||
void shouldSerializeCustomerMapFieldsWithUnderscoreNames() throws Exception
|
||||
{
|
||||
CustomerMapRecordVO record = new CustomerMapRecordVO();
|
||||
record.setCustId("101330419198206033217");
|
||||
record.setCustIsn("81033011438");
|
||||
record.setCustName("张三");
|
||||
record.setFaithDay("20");
|
||||
record.setBalanceAvg("300000");
|
||||
record.setLoanCountHis("2");
|
||||
record.setLastLoanDate("2025-12-01");
|
||||
|
||||
String json = new ObjectMapper().writeValueAsString(record);
|
||||
|
||||
assertTrue(json.contains("\"cust_id\""));
|
||||
assertTrue(json.contains("\"cust_isn\""));
|
||||
assertTrue(json.contains("\"cust_name\""));
|
||||
assertTrue(json.contains("\"faith_day\""));
|
||||
assertTrue(json.contains("\"balance_avg\""));
|
||||
assertTrue(json.contains("\"loan_count_his\""));
|
||||
assertTrue(json.contains("\"last_loan_date\""));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL because `CustomerMapRecordVO` does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the VO**
|
||||
|
||||
Create `CustomerMapRecordVO.java`:
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CustomerMapRecordVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonProperty("cust_id")
|
||||
private String custId;
|
||||
|
||||
@JsonProperty("cust_isn")
|
||||
private String custIsn;
|
||||
|
||||
@JsonProperty("cust_name")
|
||||
private String custName;
|
||||
|
||||
@JsonProperty("faith_day")
|
||||
private String faithDay;
|
||||
|
||||
@JsonProperty("balance_avg")
|
||||
private String balanceAvg;
|
||||
|
||||
@JsonProperty("loan_count_his")
|
||||
private String loanCountHis;
|
||||
|
||||
@JsonProperty("last_loan_date")
|
||||
private String lastLoanDate;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the test to verify it passes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
## Task 2: Add Customer Map Service
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapService.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapServiceTest.java`
|
||||
|
||||
- [ ] **Step 1: Write service tests first**
|
||||
|
||||
Create tests that use `MockRestServiceServer` and the package-private test constructor:
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
|
||||
class LoanPricingCustomerMapServiceTest
|
||||
{
|
||||
@Test
|
||||
void shouldQueryPersonalCustomerMapWithCustIdParam()
|
||||
{
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
|
||||
LoanPricingCustomerMapService service = new LoanPricingCustomerMapService(
|
||||
restTemplate,
|
||||
"http://mock/personal",
|
||||
"http://mock/corporate");
|
||||
|
||||
server.expect(requestTo("http://mock/personal?cust_id=P001"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"msg\":\"操作成功\",\"data\":[{\"cust_id\":\"P001\",\"cust_isn\":\"81033011438\",\"cust_name\":\"张三\"}]}",
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
List<CustomerMapRecordVO> result = service.queryPersonal("P001");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("81033011438", result.get(0).getCustIsn());
|
||||
assertEquals("张三", result.get(0).getCustName());
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldQueryCorporateCustomerMapWithCustIdParam()
|
||||
{
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);
|
||||
LoanPricingCustomerMapService service = new LoanPricingCustomerMapService(
|
||||
restTemplate,
|
||||
"http://mock/personal",
|
||||
"http://mock/corporate");
|
||||
|
||||
server.expect(requestTo("http://mock/corporate?cust_id=C001"))
|
||||
.andRespond(withSuccess("{\"code\":200,\"data\":[{\"cust_id\":\"C001\",\"cust_isn\":\"82002469287\",\"cust_name\":\"测试企业\"}]}",
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
List<CustomerMapRecordVO> result = service.queryCorporate("C001");
|
||||
|
||||
assertEquals("82002469287", result.get(0).getCustIsn());
|
||||
assertEquals("测试企业", result.get(0).getCustName());
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectBlankCustId()
|
||||
{
|
||||
LoanPricingCustomerMapService service = new LoanPricingCustomerMapService(
|
||||
new RestTemplate(),
|
||||
"http://mock/personal",
|
||||
"http://mock/corporate");
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class, () -> service.queryPersonal(" "));
|
||||
|
||||
assertEquals("客户号不能为空", ex.getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL because `LoanPricingCustomerMapService` does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the service**
|
||||
|
||||
Create the service with a default Spring constructor and a package-private test constructor:
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
@Service
|
||||
public class LoanPricingCustomerMapService
|
||||
{
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Value("${customer-map.personal-url}")
|
||||
private String personalUrl;
|
||||
|
||||
@Value("${customer-map.corporate-url}")
|
||||
private String corporateUrl;
|
||||
|
||||
public LoanPricingCustomerMapService()
|
||||
{
|
||||
this(new RestTemplate());
|
||||
}
|
||||
|
||||
LoanPricingCustomerMapService(RestTemplate restTemplate)
|
||||
{
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
LoanPricingCustomerMapService(RestTemplate restTemplate, String personalUrl, String corporateUrl)
|
||||
{
|
||||
this.restTemplate = restTemplate;
|
||||
this.personalUrl = personalUrl;
|
||||
this.corporateUrl = corporateUrl;
|
||||
}
|
||||
|
||||
public List<CustomerMapRecordVO> queryPersonal(String custId)
|
||||
{
|
||||
return query(personalUrl, custId);
|
||||
}
|
||||
|
||||
public List<CustomerMapRecordVO> queryCorporate(String custId)
|
||||
{
|
||||
return query(corporateUrl, custId);
|
||||
}
|
||||
|
||||
private List<CustomerMapRecordVO> query(String url, String custId)
|
||||
{
|
||||
if (!StringUtils.hasText(custId))
|
||||
{
|
||||
throw new ServiceException("客户号不能为空");
|
||||
}
|
||||
URI uri = UriComponentsBuilder.fromHttpUrl(url)
|
||||
.queryParam("cust_id", custId)
|
||||
.build()
|
||||
.toUri();
|
||||
CustomerMapResponse response = restTemplate.getForObject(uri, CustomerMapResponse.class);
|
||||
if (response == null)
|
||||
{
|
||||
throw new ServiceException("客户号映射接口无返回");
|
||||
}
|
||||
if (response.getCode() != null && response.getCode() != 200)
|
||||
{
|
||||
throw new ServiceException(StringUtils.hasText(response.getMsg()) ? response.getMsg() : "客户号映射查询失败");
|
||||
}
|
||||
return response.getData() == null ? Collections.emptyList() : response.getData();
|
||||
}
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static class CustomerMapResponse
|
||||
{
|
||||
private Integer code;
|
||||
private String msg;
|
||||
private List<CustomerMapRecordVO> data;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run service tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingCustomerMapServiceTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
## Task 3: Add Mock Endpoints
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java`
|
||||
|
||||
- [ ] **Step 1: Write controller tests for mock endpoints**
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class LoanRatePricingMockControllerCustomerMapTest
|
||||
{
|
||||
@Test
|
||||
void shouldReturnPersonalCustomerMapRecords() throws Exception
|
||||
{
|
||||
LoanRatePricingMockController controller = new LoanRatePricingMockController();
|
||||
|
||||
AjaxResult result = controller.queryPersonalCustomerMap("P001");
|
||||
|
||||
List<?> rows = (List<?>) result.get("data");
|
||||
assertFalse(rows.isEmpty());
|
||||
String json = new ObjectMapper().writeValueAsString(rows.get(0));
|
||||
assertTrue(json.contains("\"cust_id\""));
|
||||
assertTrue(json.contains("\"cust_isn\""));
|
||||
assertTrue(json.contains("\"cust_name\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnCorporateCustomerMapRecords() throws Exception
|
||||
{
|
||||
LoanRatePricingMockController controller = new LoanRatePricingMockController();
|
||||
|
||||
AjaxResult result = controller.queryCorporateCustomerMap("C001");
|
||||
|
||||
List<?> rows = (List<?>) result.get("data");
|
||||
assertFalse(rows.isEmpty());
|
||||
String json = new ObjectMapper().writeValueAsString(rows.get(0));
|
||||
assertTrue(json.contains("\"cust_id\""));
|
||||
assertTrue(json.contains("\"loan_count_his\""));
|
||||
assertTrue(json.contains("\"last_loan_date\""));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRatePricingMockControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL because the mock methods do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement mock methods**
|
||||
|
||||
Modify `LoanRatePricingMockController`:
|
||||
|
||||
```java
|
||||
@Anonymous
|
||||
@Operation(summary = "模拟个人客户号映射查询")
|
||||
@GetMapping("/customer-map/personal")
|
||||
public AjaxResult queryPersonalCustomerMap(@RequestParam("cust_id") String custId)
|
||||
{
|
||||
return success(randomCustomerMapRecords(custId, "个人客户"));
|
||||
}
|
||||
|
||||
@Anonymous
|
||||
@Operation(summary = "模拟企业客户号映射查询")
|
||||
@GetMapping("/customer-map/corporate")
|
||||
public AjaxResult queryCorporateCustomerMap(@RequestParam("cust_id") String custId)
|
||||
{
|
||||
return success(randomCustomerMapRecords(custId, "企业客户"));
|
||||
}
|
||||
```
|
||||
|
||||
Add a private helper in the same controller:
|
||||
|
||||
```java
|
||||
private List<CustomerMapRecordVO> randomCustomerMapRecords(String custId, String namePrefix)
|
||||
{
|
||||
if (!StringUtils.hasText(custId))
|
||||
{
|
||||
throw new ServiceException("客户号不能为空");
|
||||
}
|
||||
int count = ThreadLocalRandom.current().nextInt(1, 4);
|
||||
List<CustomerMapRecordVO> records = new ArrayList<>();
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
CustomerMapRecordVO record = new CustomerMapRecordVO();
|
||||
record.setCustId(custId);
|
||||
record.setCustIsn(String.valueOf(81000000000L + ThreadLocalRandom.current().nextInt(1000000)));
|
||||
record.setCustName(namePrefix + i);
|
||||
record.setFaithDay(String.valueOf(ThreadLocalRandom.current().nextInt(1, 365)));
|
||||
record.setBalanceAvg(String.valueOf(ThreadLocalRandom.current().nextInt(10000, 900000)));
|
||||
record.setLoanCountHis(String.valueOf(ThreadLocalRandom.current().nextInt(0, 10)));
|
||||
record.setLastLoanDate(LocalDate.now().minusDays(ThreadLocalRandom.current().nextInt(1, 800)).toString());
|
||||
records.add(record);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
```
|
||||
|
||||
Required imports:
|
||||
|
||||
```java
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run mock controller tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanRatePricingMockControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
## Task 4: Add Business Endpoints
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
|
||||
- Create: `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerCustomerMapTest.java`
|
||||
|
||||
- [ ] **Step 1: Write controller delegation tests**
|
||||
|
||||
```java
|
||||
package com.ruoyi.loanpricing.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
|
||||
import com.ruoyi.loanpricing.service.LoanPricingCustomerMapService;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class LoanPricingWorkflowControllerCustomerMapTest
|
||||
{
|
||||
@Test
|
||||
void shouldReturnPersonalCustomerMapFromService() throws Exception
|
||||
{
|
||||
LoanPricingCustomerMapService service = Mockito.mock(LoanPricingCustomerMapService.class);
|
||||
CustomerMapRecordVO row = new CustomerMapRecordVO();
|
||||
row.setCustIsn("81033011438");
|
||||
when(service.queryPersonal("P001")).thenReturn(List.of(row));
|
||||
|
||||
LoanPricingWorkflowController controller = new LoanPricingWorkflowController();
|
||||
setField(controller, "customerMapService", service);
|
||||
|
||||
AjaxResult result = controller.queryPersonalCustomerMap("P001");
|
||||
|
||||
List<?> rows = (List<?>) result.get("data");
|
||||
assertEquals(1, rows.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnCorporateCustomerMapFromService() throws Exception
|
||||
{
|
||||
LoanPricingCustomerMapService service = Mockito.mock(LoanPricingCustomerMapService.class);
|
||||
CustomerMapRecordVO row = new CustomerMapRecordVO();
|
||||
row.setCustIsn("82002469287");
|
||||
when(service.queryCorporate("C001")).thenReturn(List.of(row));
|
||||
|
||||
LoanPricingWorkflowController controller = new LoanPricingWorkflowController();
|
||||
setField(controller, "customerMapService", service);
|
||||
|
||||
AjaxResult result = controller.queryCorporateCustomerMap("C001");
|
||||
|
||||
List<?> rows = (List<?>) result.get("data");
|
||||
assertEquals(1, rows.size());
|
||||
}
|
||||
|
||||
private static void setField(Object target, String fieldName, Object value) throws Exception
|
||||
{
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: FAIL because controller methods and field do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement controller methods**
|
||||
|
||||
In `LoanPricingWorkflowController`, add:
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private LoanPricingCustomerMapService customerMapService;
|
||||
|
||||
@Operation(summary = "查询个人客户号映射")
|
||||
@GetMapping("/customer-map/personal")
|
||||
public AjaxResult queryPersonalCustomerMap(@RequestParam("custId") String custId)
|
||||
{
|
||||
return success(customerMapService.queryPersonal(custId));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询企业客户号映射")
|
||||
@GetMapping("/customer-map/corporate")
|
||||
public AjaxResult queryCorporateCustomerMap(@RequestParam("custId") String custId)
|
||||
{
|
||||
return success(customerMapService.queryCorporate(custId));
|
||||
}
|
||||
```
|
||||
|
||||
Required import:
|
||||
|
||||
```java
|
||||
import com.ruoyi.loanpricing.service.LoanPricingCustomerMapService;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run controller tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingWorkflowControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
## Task 5: Add Profile Configuration
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-dev.yml`
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-uat.yml`
|
||||
- Modify: `ruoyi-admin/src/main/resources/application-pro.yml`
|
||||
|
||||
- [ ] **Step 1: Add `customer-map` to every active profile**
|
||||
|
||||
Place this block next to the existing `model:` block in each profile file:
|
||||
|
||||
```yaml
|
||||
customer-map:
|
||||
personal-url: http://localhost:63310/rate/pricing/mock/customer-map/personal
|
||||
corporate-url: http://localhost:63310/rate/pricing/mock/customer-map/corporate
|
||||
```
|
||||
|
||||
Keep the existing `model:` URLs unchanged.
|
||||
|
||||
- [ ] **Step 2: Verify config keys exist**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n "customer-map:|personal-url: http://localhost:63310/rate/pricing/mock/customer-map/personal|corporate-url: http://localhost:63310/rate/pricing/mock/customer-map/corporate" ruoyi-admin/src/main/resources/application-dev.yml ruoyi-admin/src/main/resources/application-uat.yml ruoyi-admin/src/main/resources/application-pro.yml
|
||||
```
|
||||
|
||||
Expected: each profile contains one `customer-map` block with both mock URLs.
|
||||
|
||||
## Task 6: Backend Verification
|
||||
|
||||
**Files:**
|
||||
- Verify only, no new files.
|
||||
|
||||
- [ ] **Step 1: Run focused backend tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest,LoanPricingCustomerMapServiceTest,LoanRatePricingMockControllerCustomerMapTest,LoanPricingWorkflowControllerCustomerMapTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run affected existing model/workflow tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Manual API verification after backend restart**
|
||||
|
||||
Restart backend so config and routes are active, then call:
|
||||
|
||||
```bash
|
||||
curl -sS 'http://localhost:63310/rate/pricing/mock/customer-map/personal?cust_id=P001'
|
||||
curl -sS 'http://localhost:63310/rate/pricing/mock/customer-map/corporate?cust_id=C001'
|
||||
TOKEN=$(curl -sS -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' 'http://localhost:63310/login/test' | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
|
||||
curl -sS -H "Authorization: Bearer ${TOKEN}" 'http://localhost:63310/loanPricing/workflow/customer-map/personal?custId=P001'
|
||||
curl -sS -H "Authorization: Bearer ${TOKEN}" 'http://localhost:63310/loanPricing/workflow/customer-map/corporate?custId=C001'
|
||||
```
|
||||
|
||||
Expected: each successful response uses the RuoYi response wrapper and `data` contains one or more records with underscore fields. If the local admin password differs, replace `admin/admin123` with a valid local test account before calling the authenticated workflow endpoints.
|
||||
|
||||
- [ ] **Step 4: Commit backend work**
|
||||
|
||||
Use a Chinese commit message and do not include unrelated dirty files:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVO.java \
|
||||
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapService.java \
|
||||
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java \
|
||||
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockController.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/vo/CustomerMapRecordVOTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/LoanPricingCustomerMapServiceTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanRatePricingMockControllerCustomerMapTest.java \
|
||||
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowControllerCustomerMapTest.java \
|
||||
ruoyi-admin/src/main/resources/application-dev.yml \
|
||||
ruoyi-admin/src/main/resources/application-uat.yml \
|
||||
ruoyi-admin/src/main/resources/application-pro.yml
|
||||
git commit -m "新增客户号映射后端接口"
|
||||
```
|
||||
|
||||
Do not commit temporary curl output, screenshots, or generated test data.
|
||||
@@ -0,0 +1,662 @@
|
||||
# Customer Map Selection Frontend Implementation Plan
|
||||
|
||||
> **For agentic workers:** Steps use checkbox (`- [ ]`) syntax for tracking. Follow this repository's AGENTS.md rule: do not enable subagents or superpowers execution modes unless the user explicitly requests them for the implementation session.
|
||||
|
||||
**Goal:** Add the frontend customer-id query and customer-internal-code selection step before opening personal or corporate workflow creation dialogs.
|
||||
|
||||
**Architecture:** Keep the existing list page and personal/corporate creation dialogs. Insert a shared customer-map selector dialog between customer-type selection and workflow creation, then pass the selected underscore-field record into the existing create dialogs. Existing workflow create APIs stay unchanged and still receive `custIsn` and `custName` in camelCase.
|
||||
|
||||
**Tech Stack:** Vue 2, Element UI, existing RuoYi request wrapper, Node static assertion tests, nvm-managed frontend runtime, Playwright/browser verification after implementation.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
|
||||
- Adds personal/corporate customer-map query functions.
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue`
|
||||
- Owns customer-id input, query action, result table, loading state, and row selection.
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
|
||||
- Opens customer-map selector after customer type selection and then opens the correct create dialog after row selection.
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
|
||||
- Accepts selected customer-map record, fills `custIsn` and `custName`, and makes both fields read-only.
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
|
||||
- Same selected-record behavior for corporate creation.
|
||||
- Modify: `ruoyi-ui/package.json`
|
||||
- Adds a focused test script for the new customer-map selection checks.
|
||||
- Create: `ruoyi-ui/tests/customer-map-selection.test.js`
|
||||
- Static test coverage for API paths, selector fields, parent wiring, selected underscore fields, and read-only create dialog inputs.
|
||||
|
||||
## Task 1: Add Frontend API Methods
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/api/loanPricing/workflow.js`
|
||||
- Create: `ruoyi-ui/tests/customer-map-selection.test.js`
|
||||
- Modify: `ruoyi-ui/package.json`
|
||||
|
||||
- [ ] **Step 1: Write failing API assertions**
|
||||
|
||||
Create the first version of `ruoyi-ui/tests/customer-map-selection.test.js`:
|
||||
|
||||
```js
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const assert = require('assert')
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(__dirname, '..', relativePath), 'utf8')
|
||||
}
|
||||
|
||||
const workflowApi = read('src/api/loanPricing/workflow.js')
|
||||
|
||||
assert(
|
||||
workflowApi.includes('export function queryPersonalCustomerMap') &&
|
||||
workflowApi.includes("url: '/loanPricing/workflow/customer-map/personal'") &&
|
||||
workflowApi.includes('params: { custId: custId }'),
|
||||
'缺少个人客户号映射查询 API'
|
||||
)
|
||||
|
||||
assert(
|
||||
workflowApi.includes('export function queryCorporateCustomerMap') &&
|
||||
workflowApi.includes("url: '/loanPricing/workflow/customer-map/corporate'") &&
|
||||
workflowApi.includes('params: { custId: custId }'),
|
||||
'缺少企业客户号映射查询 API'
|
||||
)
|
||||
|
||||
console.log('customer map selection assertions passed')
|
||||
```
|
||||
|
||||
Add a script in `ruoyi-ui/package.json`:
|
||||
|
||||
```json
|
||||
"test:customer-map-selection": "node tests/customer-map-selection.test.js"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run with the project Node version:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: FAIL because the API methods do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the API methods**
|
||||
|
||||
Append to `workflow.js`:
|
||||
|
||||
```js
|
||||
// 查询个人客户号映射
|
||||
export function queryPersonalCustomerMap(custId) {
|
||||
return request({
|
||||
url: '/loanPricing/workflow/customer-map/personal',
|
||||
method: 'get',
|
||||
params: { custId: custId }
|
||||
})
|
||||
}
|
||||
|
||||
// 查询企业客户号映射
|
||||
export function queryCorporateCustomerMap(custId) {
|
||||
return request({
|
||||
url: '/loanPricing/workflow/customer-map/corporate',
|
||||
method: 'get',
|
||||
params: { custId: custId }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the API test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: PASS for API assertions.
|
||||
|
||||
## Task 2: Create Customer Map Selector Dialog
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue`
|
||||
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`
|
||||
|
||||
- [ ] **Step 1: Extend the static test for selector requirements**
|
||||
|
||||
Add these assertions to `customer-map-selection.test.js`:
|
||||
|
||||
```js
|
||||
const selector = read('src/views/loanPricing/workflow/components/CustomerMapSelector.vue')
|
||||
|
||||
assert(
|
||||
selector.includes('title="客户号查询"') &&
|
||||
selector.includes('v-model="queryForm.custId"') &&
|
||||
selector.includes('handleQuery'),
|
||||
'客户号查询弹窗缺少客户号输入或查询动作'
|
||||
)
|
||||
|
||||
assert(
|
||||
selector.includes("queryPersonalCustomerMap") &&
|
||||
selector.includes("queryCorporateCustomerMap") &&
|
||||
selector.includes("this.customerType === 'personal'"),
|
||||
'客户号查询弹窗未按客户类型调用个人/企业接口'
|
||||
)
|
||||
|
||||
;['cust_id', 'cust_isn', 'cust_name', 'faith_day', 'balance_avg', 'loan_count_his', 'last_loan_date'].forEach((field) => {
|
||||
assert(selector.includes(`prop="${field}"`) || selector.includes(`row.${field}`), `查询结果表格缺少字段 ${field}`)
|
||||
})
|
||||
|
||||
assert(
|
||||
selector.includes("this.$emit('select', row)") &&
|
||||
selector.includes('未查询到客户信息') &&
|
||||
selector.includes('请输入客户号'),
|
||||
'客户号查询弹窗缺少选择事件或关键提示'
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: FAIL because `CustomerMapSelector.vue` does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement `CustomerMapSelector.vue`**
|
||||
|
||||
Create the component:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-dialog title="客户号查询" :visible.sync="dialogVisible" width="900px" append-to-body
|
||||
:close-on-click-modal="false" @close="handleClose">
|
||||
<el-form :model="queryForm" inline size="small">
|
||||
<el-form-item label="客户号">
|
||||
<el-input v-model="queryForm.custId" placeholder="请输入客户号" clearable @keyup.enter.native="handleQuery"/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleQuery">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="customerList">
|
||||
<el-table-column label="客户号" prop="cust_id" align="center" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="客户内码" prop="cust_isn" align="center" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="客户名称" prop="cust_name" align="center" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="用信天数" prop="faith_day" align="center"/>
|
||||
<el-table-column label="存款年日均" prop="balance_avg" align="center"/>
|
||||
<el-table-column label="历史贷款次数" prop="loan_count_his" align="center"/>
|
||||
<el-table-column label="上次贷款日期" prop="last_loan_date" align="center" width="130"/>
|
||||
<el-table-column label="操作" align="center" width="90">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="handleSelect(scope.row)">选择</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {queryPersonalCustomerMap, queryCorporateCustomerMap} from "@/api/loanPricing/workflow"
|
||||
|
||||
export default {
|
||||
name: "CustomerMapSelector",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
customerType: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
queryForm: {
|
||||
custId: undefined
|
||||
},
|
||||
customerList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleQuery() {
|
||||
if (!this.queryForm.custId) {
|
||||
this.$modal.msgWarning("请输入客户号")
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
const request = this.customerType === 'personal'
|
||||
? queryPersonalCustomerMap
|
||||
: queryCorporateCustomerMap
|
||||
request(this.queryForm.custId).then(response => {
|
||||
this.customerList = response.data || []
|
||||
if (this.customerList.length === 0) {
|
||||
this.$modal.msgWarning("未查询到客户信息")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleSelect(row) {
|
||||
this.$emit('select', row)
|
||||
this.dialogVisible = false
|
||||
},
|
||||
handleClose() {
|
||||
this.queryForm.custId = undefined
|
||||
this.customerList = []
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the selector test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: PASS through selector assertions.
|
||||
|
||||
## Task 3: Wire Selector Into Workflow List Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/index.vue`
|
||||
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`
|
||||
|
||||
- [ ] **Step 1: Extend the test for parent wiring**
|
||||
|
||||
Add assertions:
|
||||
|
||||
```js
|
||||
const workflowIndex = read('src/views/loanPricing/workflow/index.vue')
|
||||
|
||||
assert(
|
||||
workflowIndex.includes('CustomerMapSelector') &&
|
||||
workflowIndex.includes('<customer-map-selector') &&
|
||||
workflowIndex.includes(':customer-type="selectedCustomerType"') &&
|
||||
workflowIndex.includes('@select="handleCustomerMapSelect"'),
|
||||
'流程列表页未接入客户号查询选择弹窗'
|
||||
)
|
||||
|
||||
assert(
|
||||
workflowIndex.includes('selectedCustomerType') &&
|
||||
workflowIndex.includes('selectedCustomerMap') &&
|
||||
workflowIndex.includes('showCustomerMapSelector'),
|
||||
'流程列表页缺少客户类型、客户映射选择状态'
|
||||
)
|
||||
|
||||
assert(
|
||||
workflowIndex.includes("this.selectedCustomerType = type") &&
|
||||
workflowIndex.includes('this.showCustomerMapSelector = true') &&
|
||||
!workflowIndex.includes("if (type === 'personal') {\\n this.showPersonalDialog = true"),
|
||||
'选择客户类型后应先打开客户号查询弹窗,而不是直接打开新增弹窗'
|
||||
)
|
||||
|
||||
assert(
|
||||
workflowIndex.includes('handleCustomerMapSelect') &&
|
||||
workflowIndex.includes('this.selectedCustomerMap = row') &&
|
||||
workflowIndex.includes('this.showPersonalDialog = true') &&
|
||||
workflowIndex.includes('this.showCorporateDialog = true'),
|
||||
'选择客户内码后未打开对应新增弹窗'
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: FAIL because the list page does not yet use the selector.
|
||||
|
||||
- [ ] **Step 3: Update `index.vue` template and script**
|
||||
|
||||
Add component usage after the customer type selector:
|
||||
|
||||
```vue
|
||||
<customer-map-selector
|
||||
:visible.sync="showCustomerMapSelector"
|
||||
:customer-type="selectedCustomerType"
|
||||
@select="handleCustomerMapSelect"
|
||||
/>
|
||||
```
|
||||
|
||||
Pass the selected record into both create dialogs:
|
||||
|
||||
```vue
|
||||
<personal-create-dialog
|
||||
:visible.sync="showPersonalDialog"
|
||||
:customer-map="selectedCustomerMap"
|
||||
@success="handleCreateSuccess"
|
||||
/>
|
||||
|
||||
<corporate-create-dialog
|
||||
:visible.sync="showCorporateDialog"
|
||||
:customer-map="selectedCustomerMap"
|
||||
@success="handleCreateSuccess"
|
||||
/>
|
||||
```
|
||||
|
||||
Import and register the selector:
|
||||
|
||||
```js
|
||||
import CustomerMapSelector from "./components/CustomerMapSelector"
|
||||
```
|
||||
|
||||
Add state:
|
||||
|
||||
```js
|
||||
showCustomerMapSelector: false,
|
||||
selectedCustomerType: undefined,
|
||||
selectedCustomerMap: null,
|
||||
```
|
||||
|
||||
Change `handleSelectType`:
|
||||
|
||||
```js
|
||||
handleSelectType(type) {
|
||||
this.selectedCustomerType = type
|
||||
this.selectedCustomerMap = null
|
||||
this.showCustomerMapSelector = true
|
||||
}
|
||||
```
|
||||
|
||||
Add selected-row handler:
|
||||
|
||||
```js
|
||||
handleCustomerMapSelect(row) {
|
||||
this.selectedCustomerMap = row
|
||||
if (this.selectedCustomerType === 'personal') {
|
||||
this.showPersonalDialog = true
|
||||
} else if (this.selectedCustomerType === 'corporate') {
|
||||
this.showCorporateDialog = true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Clear selected state when the create flow completes:
|
||||
|
||||
```js
|
||||
handleCreateSuccess() {
|
||||
this.selectedCustomerMap = null
|
||||
this.selectedCustomerType = undefined
|
||||
this.getList()
|
||||
}
|
||||
```
|
||||
|
||||
Add watchers for dialog close so cancellation also clears the selected record:
|
||||
|
||||
```js
|
||||
watch: {
|
||||
showPersonalDialog(val) {
|
||||
if (!val && !this.showCorporateDialog) {
|
||||
this.selectedCustomerMap = null
|
||||
this.selectedCustomerType = undefined
|
||||
}
|
||||
},
|
||||
showCorporateDialog(val) {
|
||||
if (!val && !this.showPersonalDialog) {
|
||||
this.selectedCustomerMap = null
|
||||
this.selectedCustomerType = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the parent wiring test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: PASS through parent wiring assertions.
|
||||
|
||||
## Task 4: Make Create Dialog Customer Fields Read-Only and Auto-Filled
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue`
|
||||
- Modify: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue`
|
||||
- Modify: `ruoyi-ui/tests/customer-map-selection.test.js`
|
||||
|
||||
- [ ] **Step 1: Extend test for create dialog behavior**
|
||||
|
||||
Add assertions:
|
||||
|
||||
```js
|
||||
const personalCreateDialog = read('src/views/loanPricing/workflow/components/PersonalCreateDialog.vue')
|
||||
const corporateCreateDialog = read('src/views/loanPricing/workflow/components/CorporateCreateDialog.vue')
|
||||
|
||||
;[
|
||||
['个人新增弹窗', personalCreateDialog],
|
||||
['企业新增弹窗', corporateCreateDialog]
|
||||
].forEach(([name, source]) => {
|
||||
assert(source.includes('customerMap'), `${name} 缺少 customerMap 入参`)
|
||||
assert(source.includes(':readonly="true"') || source.includes('readonly'), `${name} 客户内码和客户名称应只读`)
|
||||
assert(source.includes('this.customerMap.cust_isn'), `${name} 未从 cust_isn 自动带入客户内码`)
|
||||
assert(source.includes('this.customerMap.cust_name'), `${name} 未从 cust_name 自动带入客户名称`)
|
||||
assert(source.includes('clearValidate'), `${name} 应清空校验而不是用 resetFields 覆盖已选客户`)
|
||||
assert(!source.includes('this.resetForm("form")'), `${name} 不应在 reset() 中调用 resetForm("form") 覆盖只读客户字段`)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: FAIL because dialogs do not yet consume `customerMap`.
|
||||
|
||||
- [ ] **Step 3: Update personal create dialog**
|
||||
|
||||
Add prop:
|
||||
|
||||
```js
|
||||
customerMap: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
```
|
||||
|
||||
Make customer fields read-only:
|
||||
|
||||
```vue
|
||||
<el-input v-model="form.custIsn" placeholder="请选择客户内码" :readonly="true"/>
|
||||
<el-input v-model="form.custName" placeholder="请选择客户名称" :readonly="true"/>
|
||||
```
|
||||
|
||||
In `reset()`, initialize from selected row and do not call `this.resetForm("form")`. The existing RuoYi helper delegates to Element UI `resetFields()`, which can restore stale initial values and overwrite the selected read-only customer fields. After assigning the new form object, only clear validation:
|
||||
|
||||
```js
|
||||
reset() {
|
||||
this.form = {
|
||||
orgCode: '892000',
|
||||
runType: '1',
|
||||
custIsn: this.customerMap ? this.customerMap.cust_isn : undefined,
|
||||
custName: this.customerMap ? this.customerMap.cust_name : undefined,
|
||||
idType: undefined,
|
||||
idNum: undefined,
|
||||
guarType: undefined,
|
||||
applyAmt: undefined,
|
||||
loanPurpose: undefined,
|
||||
loanTerm: undefined,
|
||||
bizProof: false,
|
||||
loanLoop: false,
|
||||
collType: undefined,
|
||||
collThirdParty: false
|
||||
}
|
||||
this.submitting = false
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.clearValidate()
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Keep existing required validation for `custIsn` and `custName`.
|
||||
|
||||
- [ ] **Step 4: Update corporate create dialog**
|
||||
|
||||
Apply the same prop, read-only inputs, and `reset()` initialization to `CorporateCreateDialog.vue`. Do not call `this.resetForm("form")` in the corporate dialog reset path; assign the new form object with `custIsn` and `custName` from `customerMap`, then use `this.$refs.form.clearValidate()` inside `$nextTick()`.
|
||||
|
||||
- [ ] **Step 5: Run the create dialog test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: PASS through create dialog assertions.
|
||||
|
||||
## Task 5: Frontend Verification
|
||||
|
||||
**Files:**
|
||||
- Verify only, no new source files.
|
||||
|
||||
- [ ] **Step 1: Run focused customer-map test**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection'
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 2: Run affected existing frontend tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
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 && node ruoyi-ui/tests/workflow-index-refresh.test.js'
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Build frontend**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run build:prod'
|
||||
```
|
||||
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 4: Browser verification after backend and frontend are running**
|
||||
|
||||
Use the real application page, not a prototype page:
|
||||
|
||||
1. Open the workflow list page.
|
||||
2. Click “新增”.
|
||||
3. Select “个人客户”.
|
||||
4. Confirm the customer-id query dialog opens.
|
||||
5. Query any customer number.
|
||||
6. Select one result row.
|
||||
7. Confirm the personal create dialog opens and `客户内码` / `客户名称` are auto-filled and read-only.
|
||||
8. Fill the remaining required fields and submit.
|
||||
9. Repeat the same flow for “企业客户”.
|
||||
10. Close and reopen the create flow at least once with a second selected row, and confirm the new row's `cust_isn` / `cust_name` replace the previous values in the create dialog.
|
||||
11. Confirm both created records appear in the list or detail page.
|
||||
|
||||
Expected: both personal and corporate flows pass through customer-map selection before creation.
|
||||
|
||||
- [ ] **Step 5: Cleanup test processes**
|
||||
|
||||
Stop any backend or frontend processes started for verification before ending the task.
|
||||
|
||||
- [ ] **Step 6: Commit frontend work**
|
||||
|
||||
Use a Chinese commit message and avoid unrelated dirty files:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add ruoyi-ui/src/api/loanPricing/workflow.js \
|
||||
ruoyi-ui/src/views/loanPricing/workflow/components/CustomerMapSelector.vue \
|
||||
ruoyi-ui/src/views/loanPricing/workflow/index.vue \
|
||||
ruoyi-ui/src/views/loanPricing/workflow/components/PersonalCreateDialog.vue \
|
||||
ruoyi-ui/src/views/loanPricing/workflow/components/CorporateCreateDialog.vue \
|
||||
ruoyi-ui/package.json \
|
||||
ruoyi-ui/tests/customer-map-selection.test.js
|
||||
git commit -m "新增客户号查询选择前端流程"
|
||||
```
|
||||
|
||||
Do not commit screenshots, browser traces, temporary spreadsheets, or generated test data.
|
||||
|
||||
## Task 6: Final End-to-End Verification and Implementation Record
|
||||
|
||||
**Files:**
|
||||
- Create: `doc/implementation-report-2026-04-29-customer-map-selection.md`
|
||||
|
||||
- [ ] **Step 1: Run backend and frontend verification together**
|
||||
|
||||
After backend and frontend implementation are both complete:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-loan-pricing -am -Dtest=CustomerMapRecordVOTest,LoanPricingCustomerMapServiceTest,LoanRatePricingMockControllerCustomerMapTest,LoanPricingWorkflowControllerCustomerMapTest,LoanPricingModelServiceTest,LoanPricingWorkflowServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
zsh -lic 'nvm use 14.21.3 >/dev/null && npm --prefix ruoyi-ui run test:customer-map-selection && npm --prefix ruoyi-ui run test:personal-create-input-params && npm --prefix ruoyi-ui run test:corporate-create-input-params && node ruoyi-ui/tests/workflow-index-refresh.test.js'
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 2: Complete browser verification**
|
||||
|
||||
Run the browser flow from Task 5 for both personal and corporate customers. Confirm the backend API response wrapper is still the existing RuoYi wrapper while records inside `data` keep underscore fields.
|
||||
|
||||
- [ ] **Step 3: Write implementation report**
|
||||
|
||||
Create `doc/implementation-report-2026-04-29-customer-map-selection.md` with:
|
||||
|
||||
```markdown
|
||||
# 2026-04-29 客户号查询选择客户内码实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 后端新增个人/企业客户号映射业务接口。
|
||||
- 后端新增个人/企业客户号映射 mock 接口。
|
||||
- 配置文件新增 `customer-map` 个人/企业地址并指向本项目 mock。
|
||||
- 前端新增客户号查询选择弹窗。
|
||||
- 个人/企业新增流程改为先查询客户号、选择客户内码,再打开新增弹窗。
|
||||
- 新增弹窗客户内码和客户名称由选中记录自动带入并只读。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 后端测试:填写实际执行命令和通过/失败结果。
|
||||
- 前端测试:填写实际执行命令和通过/失败结果。
|
||||
- 真实页面验证:填写个人、企业两条浏览器验证流程和结果。
|
||||
- 进程清理:填写本次启动的前后端进程是否已关闭。
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit implementation report**
|
||||
|
||||
```bash
|
||||
git add doc/implementation-report-2026-04-29-customer-map-selection.md
|
||||
git commit -m "补充客户号映射选择实施记录"
|
||||
```
|
||||
@@ -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 "记录上虞字段调整后端实现"
|
||||
```
|
||||
@@ -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 "记录上虞字段调整前端验证"
|
||||
```
|
||||
@@ -0,0 +1,284 @@
|
||||
# 业务种类与历史贷款利率设计
|
||||
|
||||
## 背景
|
||||
|
||||
利率定价流程新增时,当前页面已经支持先选择客户类型,再通过客户号查询选择客户内码,最后打开个人或企业新增弹窗。新需求要求在新增流程时增加两个字段:
|
||||
|
||||
- 业务种类
|
||||
- 历史贷款利率
|
||||
|
||||
字段和接口规则以 `doc/上虞_客户内码客户_历史利率_映射表.xlsx` 的“历史贷款合同查询选择”sheet 以及 `doc/利率定价接口.txt` 的历史合同查询接口为准。
|
||||
|
||||
## 已确认规则
|
||||
|
||||
- 个人客户和企业客户新增流程同时增加业务种类和历史贷款利率。
|
||||
- 业务种类选项为:新客、存量新增、存量转贷。
|
||||
- 业务种类需要保存到流程表,并在详情页展示。
|
||||
- 业务种类只用于前端判断是否触发历史合同查询,不作为模型入参上传。
|
||||
- 仅当业务种类选择“存量转贷”时,查询历史贷款合同。
|
||||
- 存量转贷必须选择一条历史合同记录;未查询到历史合同、未选择历史合同或历史贷款利率为空时,禁止提交。
|
||||
- 历史贷款利率需要保存到流程表。
|
||||
- 发起利率定价的模型调用入参需要新增历史贷款利率字段。
|
||||
- 历史合同查询列表展示 Excel 中“历史贷款合同查询选择”sheet 的全部返回字段,并由用户单选一条。
|
||||
|
||||
## 推荐方案
|
||||
|
||||
采用后端代理历史合同查询、前端弹窗单选的方案。
|
||||
|
||||
前端负责业务种类交互、触发查询、展示历史合同列表、单选回填历史贷款利率和提交前拦截。后端负责代理外部历史合同接口、统一错误处理、保存新增字段,以及在模型调用中传递历史贷款利率。
|
||||
|
||||
不采用前端直接调用外部网关地址的方案,避免把外部地址和 `appCode` 暴露到前端,也避免错误处理分散。
|
||||
|
||||
不采用进入新增弹窗前先查询历史合同的方案,因为只有存量转贷需要历史合同,前置查询会让新客和存量新增多走不必要步骤。
|
||||
|
||||
## 数据流
|
||||
|
||||
新增流程保持现有主路径:
|
||||
|
||||
1. 列表页点击新增。
|
||||
2. 选择个人客户或企业客户。
|
||||
3. 客户号查询并选择客户内码。
|
||||
4. 打开个人或企业新增弹窗。
|
||||
5. 在贷款信息中选择业务种类。
|
||||
6. 若业务种类为新客或存量新增,直接填写其他字段并提交。
|
||||
7. 若业务种类为存量转贷,前端用当前客户内码查询历史合同。
|
||||
8. 前端弹出历史贷款合同选择列表。
|
||||
9. 用户单选一条历史合同。
|
||||
10. 前端回填该行历史贷款利率。
|
||||
11. 提交创建接口。
|
||||
12. 后端保存业务种类和历史贷款利率。
|
||||
13. 后端调用模型时只把历史贷款利率带入模型请求。
|
||||
|
||||
## 历史合同查询接口设计
|
||||
|
||||
新增后端业务接口:
|
||||
|
||||
```text
|
||||
GET /loanPricing/workflow/history-contract?custIsn=...
|
||||
```
|
||||
|
||||
接口职责:
|
||||
|
||||
1. 校验 `custIsn` 非空。
|
||||
2. 读取历史合同外部接口配置。
|
||||
3. 使用 GET 调用外部接口,请求参数名为 `cust_isn`。
|
||||
4. 解析外部接口返回列表。
|
||||
5. 返回给前端的字段保持下划线命名。
|
||||
|
||||
新增配置:
|
||||
|
||||
```yaml
|
||||
loan-rate-history:
|
||||
url: http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_loan_rate_history?appCode=1a89fa84abda480ba93ed73fd01ffd07
|
||||
```
|
||||
|
||||
配置值只保存接口基址和固定 `appCode`,不包含 `cust_isn=`。代码统一通过 `UriComponentsBuilder.queryParam("cust_isn", custIsn)` 追加客户内码,避免重复参数或拼接错误。
|
||||
|
||||
开发和测试环境可指向本项目 mock 接口;生产配置使用真实外部接口地址。
|
||||
|
||||
## 历史合同返回字段
|
||||
|
||||
新增 `HistoryLoanContractVO`,JSON 字段按 Excel 保持下划线命名:
|
||||
|
||||
| 字段 | 名称 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `cust_isn` | 客户内码 | 前端展示 |
|
||||
| `loan_contract_history` | 历史贷款合同号 | 前端展示 |
|
||||
| `guar_type_history` | 历史贷款担保方式 | 前端展示 |
|
||||
| `product_code_history` | 历史贷款产品代码 | 前端展示 |
|
||||
| `loan_rate_history` | 历史贷款利率 | 前端展示,用户选择后作为后续参数 |
|
||||
| `loan_amount_history` | 历史贷款金额 | 前端展示 |
|
||||
| `loan_sign_date_history` | 历史贷款签订时间 | 前端展示 |
|
||||
|
||||
## 创建接口和数据模型
|
||||
|
||||
个人创建 DTO `PersonalLoanPricingCreateDTO` 新增:
|
||||
|
||||
- `businessType`
|
||||
- `loanRateHistory`
|
||||
|
||||
企业创建 DTO `CorporateLoanPricingCreateDTO` 新增:
|
||||
|
||||
- `businessType`
|
||||
- `loanRateHistory`
|
||||
|
||||
校验规则:
|
||||
|
||||
- `businessType` 必填。
|
||||
- `businessType` 只能是新客、存量新增、存量转贷。
|
||||
- `businessType=存量转贷` 时,`loanRateHistory` 必填。
|
||||
- `businessType` 为新客或存量新增时,不要求 `loanRateHistory`。
|
||||
|
||||
流程实体 `LoanPricingWorkflow` 新增:
|
||||
|
||||
- `businessType`
|
||||
- `loanRateHistory`
|
||||
|
||||
转换器 `LoanPricingConverter` 需要同步更新:
|
||||
|
||||
- `toEntity(PersonalLoanPricingCreateDTO dto)` 映射 `businessType` 和 `loanRateHistory`。
|
||||
- `toEntity(CorporateLoanPricingCreateDTO dto)` 映射 `businessType` 和 `loanRateHistory`。
|
||||
|
||||
这两个字段必须先写入 `LoanPricingWorkflow`,后续详情展示和模型入参才会闭环。
|
||||
|
||||
数据库 `loan_pricing_workflow` 新增:
|
||||
|
||||
```sql
|
||||
ALTER TABLE `loan_pricing_workflow`
|
||||
ADD COLUMN `business_type` varchar(20) DEFAULT NULL COMMENT '业务种类' AFTER `loan_purpose`,
|
||||
ADD COLUMN `loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率' AFTER `business_type`;
|
||||
```
|
||||
|
||||
需要同步更新独立 schema SQL 和生产初始化 SQL 中的 `loan_pricing_workflow` 表结构。
|
||||
|
||||
跨字段校验放在创建服务层统一处理,不只依赖 DTO 注解:
|
||||
|
||||
- 创建个人流程前校验业务种类。
|
||||
- 创建企业流程前校验业务种类。
|
||||
- `businessType` 必须是新客、存量新增、存量转贷之一。
|
||||
- `businessType=存量转贷` 时,`loanRateHistory` 必须非空。
|
||||
- 校验通过后才允许入库和触发模型调用。
|
||||
|
||||
这样可以拦截绕过前端直接调用创建接口的请求。
|
||||
|
||||
## 模型入参
|
||||
|
||||
模型入参 `ModelInvokeDTO` 新增:
|
||||
|
||||
- `loanRateHistory`
|
||||
|
||||
`LoanPricingModelService` 仍按现有方式从 `LoanPricingWorkflow` 复制属性到 `ModelInvokeDTO`。由于 `LoanPricingWorkflow` 和 `ModelInvokeDTO` 字段同名,历史贷款利率会进入模型请求。
|
||||
|
||||
`businessType` 不添加到 `ModelInvokeDTO`,不进入模型请求。
|
||||
|
||||
`ModelService` 仍使用现有 `application/x-www-form-urlencoded` 请求方式,不调整模型调用协议。
|
||||
|
||||
## 前端交互
|
||||
|
||||
个人和企业新增弹窗都在“贷款信息”区域增加:
|
||||
|
||||
- 业务种类:下拉框,选项为新客、存量新增、存量转贷。
|
||||
- 历史贷款利率:只读输入框,仅当业务种类为存量转贷时展示或启用。
|
||||
|
||||
业务种类选择逻辑:
|
||||
|
||||
- 选择新客:清空已选历史合同和历史贷款利率。
|
||||
- 选择存量新增:清空已选历史合同和历史贷款利率。
|
||||
- 选择存量转贷:使用当前 `custIsn` 查询历史贷款合同,并打开历史合同选择弹窗。
|
||||
|
||||
历史贷款合同选择弹窗:
|
||||
|
||||
- 宽度沿用客户号查询弹窗的 80%。
|
||||
- 表格首列为单选框。
|
||||
- 表格展示客户内码、历史贷款合同号、历史贷款担保方式、历史贷款产品代码、历史贷款利率、历史贷款金额、历史贷款签订时间。
|
||||
- 用户选择一条记录后点击确定。
|
||||
- 确定后关闭弹窗,并将该行 `loan_rate_history` 回填到新增表单的 `loanRateHistory`。
|
||||
|
||||
提交逻辑:
|
||||
|
||||
- 业务种类始终必填。
|
||||
- 存量转贷时必须已选择历史合同。
|
||||
- 存量转贷时 `loanRateHistory` 为空则禁止提交。
|
||||
- 新客和存量新增提交时不需要历史贷款利率。
|
||||
|
||||
## 详情展示
|
||||
|
||||
个人详情和企业详情都需要展示:
|
||||
|
||||
- 业务种类
|
||||
- 历史贷款利率
|
||||
|
||||
展示位置放在现有贷款信息或基础信息区域,字段值来自 `loanPricingWorkflow.businessType` 和 `loanPricingWorkflow.loanRateHistory`。
|
||||
|
||||
若历史贷款利率为空,详情页按现有详情页空值展示方式显示。
|
||||
|
||||
## 异常处理
|
||||
|
||||
- 前端业务种类为空:提示“请选择业务种类”。
|
||||
- 存量转贷未选择历史合同:提示“请选择历史贷款合同”,禁止提交。
|
||||
- 存量转贷历史合同返回空列表:提示“未查询到历史贷款合同”,禁止提交。
|
||||
- 历史合同查询失败:展示后端返回错误,保留当前新增弹窗,不提交。
|
||||
- 后端历史合同接口 `custIsn` 为空:返回“客户内码不能为空”。
|
||||
- 外部历史合同接口无返回或返回错误:后端抛出业务异常。
|
||||
- 创建接口收到存量转贷但历史贷款利率为空:后端返回业务错误。
|
||||
|
||||
## Mock 接口
|
||||
|
||||
为本地开发和真实页面测试新增历史合同 mock:
|
||||
|
||||
```text
|
||||
GET /rate/pricing/mock/history-contract?cust_isn=...
|
||||
```
|
||||
|
||||
mock 接口返回 1 条或多条历史合同记录,字段使用历史合同返回字段中的下划线名称。
|
||||
|
||||
mock 需要提供固定测试场景:
|
||||
|
||||
- 常规 `cust_isn` 返回 1 条或多条带 `loan_rate_history` 的历史合同。
|
||||
- 固定 `cust_isn` 返回空列表,用于验证“未查询到历史贷款合同”。
|
||||
- 固定 `cust_isn` 返回记录但 `loan_rate_history` 为空,用于验证前端和后端禁止提交。
|
||||
|
||||
开发和测试 profile 的 `loan-rate-history.url` 可配置为本项目 mock 地址。
|
||||
|
||||
## 测试方案
|
||||
|
||||
### 后端验证
|
||||
|
||||
- 历史合同查询接口正常返回下划线字段。
|
||||
- 历史合同查询接口缺少 `custIsn` 时返回错误。
|
||||
- 历史合同外部接口返回空列表时,前端可收到空列表并拦截提交。
|
||||
- 个人创建接口 `businessType=存量转贷` 且缺少 `loanRateHistory` 时返回错误。
|
||||
- 企业创建接口 `businessType=存量转贷` 且缺少 `loanRateHistory` 时返回错误。
|
||||
- 个人创建接口 `businessType` 缺失时返回错误。
|
||||
- 企业创建接口 `businessType` 缺失时返回错误。
|
||||
- 个人创建接口 `businessType` 非法值时返回错误。
|
||||
- 企业创建接口 `businessType` 非法值时返回错误。
|
||||
- 个人创建接口 `businessType=新客` 且无 `loanRateHistory` 时正常保存。
|
||||
- 企业创建接口 `businessType=新客` 且无 `loanRateHistory` 时正常保存。
|
||||
- 个人创建接口 `businessType=存量新增` 且无 `loanRateHistory` 时正常保存。
|
||||
- 企业创建接口 `businessType=存量新增` 且无 `loanRateHistory` 时正常保存。
|
||||
- 个人创建接口 `businessType=存量转贷` 且 `loanRateHistory` 为空字符串时返回错误。
|
||||
- 企业创建接口 `businessType=存量转贷` 且 `loanRateHistory` 为空字符串时返回错误。
|
||||
- 个人创建接口带业务种类和历史贷款利率时正常保存。
|
||||
- 企业创建接口带业务种类和历史贷款利率时正常保存。
|
||||
- 模型调用请求包含 `loanRateHistory`。
|
||||
- 模型调用请求不包含 `businessType`。
|
||||
|
||||
### 前端验证
|
||||
|
||||
- 个人新增弹窗展示业务种类和历史贷款利率。
|
||||
- 企业新增弹窗展示业务种类和历史贷款利率。
|
||||
- 业务种类为新客时不触发历史合同查询。
|
||||
- 业务种类为存量新增时不触发历史合同查询。
|
||||
- 业务种类为存量转贷时触发历史合同查询。
|
||||
- 历史合同弹窗展示全部 7 个返回字段。
|
||||
- 历史合同弹窗只允许单选一条。
|
||||
- 选择历史合同后回填历史贷款利率。
|
||||
- 历史合同返回空列表时提示“未查询到历史贷款合同”,并禁止提交。
|
||||
- 历史合同返回记录但 `loan_rate_history` 为空时,不能把空值作为有效选择提交。
|
||||
- 业务种类从存量转贷切换到新客或存量新增时,清空历史贷款利率。
|
||||
- 存量转贷未选择历史合同时禁止提交。
|
||||
|
||||
### 真实页面验证
|
||||
|
||||
按照项目规则,页面功能开发完成后必须启动前端页面,并用浏览器打开真实利率定价流程页面验证,禁止打开 prototype 页面。
|
||||
|
||||
真实页面至少覆盖:
|
||||
|
||||
1. 个人新客新增并提交。
|
||||
2. 个人存量新增新增并提交。
|
||||
3. 个人存量转贷查询历史合同、单选、回填历史贷款利率、提交、详情展示。
|
||||
4. 企业新客新增并提交。
|
||||
5. 企业存量新增新增并提交。
|
||||
6. 企业存量转贷查询历史合同、单选、回填历史贷款利率、提交、详情展示。
|
||||
7. 存量转贷不选择历史合同直接提交时被拦截。
|
||||
|
||||
测试结束后关闭本次测试启动的前端和后端进程。
|
||||
|
||||
## 不在本次范围
|
||||
|
||||
- 不修改客户号与客户内码映射流程。
|
||||
- 不改变模型调用协议。
|
||||
- 不把业务种类作为模型入参。
|
||||
- 不新增历史合同落库明细表。
|
||||
- 不保存完整历史合同记录,只保存用户选择后的历史贷款利率。
|
||||
@@ -0,0 +1,164 @@
|
||||
# 客户号查询选择客户内码设计
|
||||
|
||||
## 背景
|
||||
|
||||
在利率定价流程新增时,现有页面流程为:
|
||||
|
||||
1. 列表页点击“新增”。
|
||||
2. 选择个人客户或企业客户。
|
||||
3. 直接打开对应新增弹窗。
|
||||
4. 用户手工输入客户内码和客户名称。
|
||||
|
||||
新需求要求在选择个人或企业后,先根据客户号调用映射接口查询客户信息,由用户从返回结果中选择一条客户内码,再自动带入新增弹窗。映射接口请求和返回字段以 `doc/上虞_客户内码客户_历史利率_映射表.xlsx` 的“客户号与客户内码映射”sheet 为准。
|
||||
|
||||
本次设计只覆盖客户号映射查询、选择和自动带入,不调整模型测算接口调用逻辑,不新增数据库字段,不改变现有流程创建接口的请求结构。
|
||||
|
||||
## 已确认规则
|
||||
|
||||
- 个人和企业分别有客户号映射接口地址。
|
||||
- 当前先在本项目内新增两个 mock 接口,不直接调用生产网关。
|
||||
- 配置文件新增客户号映射接口地址,当前配置指向本项目 mock 接口。
|
||||
- mock 接口随机返回 1 条或多条客户映射数据。
|
||||
- 后端业务接口返回给前端的字段保持下划线命名。
|
||||
- 用户选择客户映射记录后,新增弹窗自动填入客户内码和客户名称。
|
||||
- 新增弹窗中的客户内码和客户名称改为只读。
|
||||
- 其余新增流程字段和提交接口保持现状。
|
||||
|
||||
## 接口字段
|
||||
|
||||
客户号映射请求参数:
|
||||
|
||||
| 字段 | 名称 |
|
||||
| --- | --- |
|
||||
| `cust_id` | 客户号 |
|
||||
|
||||
客户号映射返回字段:
|
||||
|
||||
| 字段 | 名称 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `cust_id` | 客户号 | 前端展示 |
|
||||
| `cust_isn` | 客户内码 | 用户选择后作为流程创建入参 `custIsn` |
|
||||
| `cust_name` | 客户名称 | 用户选择后作为流程创建入参 `custName` |
|
||||
| `faith_day` | 用信天数 | 前端展示,辅助判断 |
|
||||
| `balance_avg` | 存款年日均 | 前端展示,辅助判断 |
|
||||
| `loan_count_his` | 历史贷款次数 | 前端展示,辅助判断 |
|
||||
| `last_loan_date` | 上次贷款日期 | 前端展示,辅助判断 |
|
||||
|
||||
## 后端设计
|
||||
|
||||
新增客户号映射查询业务接口:
|
||||
|
||||
- `GET /loanPricing/workflow/customer-map/personal?custId=...`
|
||||
- `GET /loanPricing/workflow/customer-map/corporate?custId=...`
|
||||
|
||||
业务接口职责:
|
||||
|
||||
1. 校验 `custId` 非空。
|
||||
2. 根据个人或企业读取对应配置地址。
|
||||
3. 使用 GET 调用配置地址,请求 mock 时传参名为 `cust_id`。
|
||||
4. 解析 mock 返回结果。
|
||||
5. 返回给前端的客户映射数据字段保持下划线,不转 camelCase。
|
||||
|
||||
新增 mock 接口:
|
||||
|
||||
- `GET /rate/pricing/mock/customer-map/personal?cust_id=...`
|
||||
- `GET /rate/pricing/mock/customer-map/corporate?cust_id=...`
|
||||
|
||||
mock 接口职责:
|
||||
|
||||
1. 校验 `cust_id` 非空。
|
||||
2. 随机生成 1 条或多条客户映射记录。
|
||||
3. 返回字段使用 `cust_id`、`cust_isn`、`cust_name`、`faith_day`、`balance_avg`、`loan_count_his`、`last_loan_date`。
|
||||
|
||||
新增配置节点:
|
||||
|
||||
```yaml
|
||||
customer-map:
|
||||
personal-url: http://localhost:63310/rate/pricing/mock/customer-map/personal
|
||||
corporate-url: http://localhost:63310/rate/pricing/mock/customer-map/corporate
|
||||
```
|
||||
|
||||
配置节点与现有 `model.personal-url`、`model.corporate-url` 分开,避免客户号映射接口和模型测算接口混用。需要启动的 profile 均应包含该配置,当前统一指向本项目 mock 地址。
|
||||
|
||||
## 前端设计
|
||||
|
||||
新增一个客户号查询选择弹窗组件,个人和企业共用,通过 `customerType` 区分调用个人或企业查询接口。
|
||||
|
||||
弹窗内容:
|
||||
|
||||
- 输入框:客户号。
|
||||
- 按钮:查询。
|
||||
- 表格列:客户号、客户内码、客户名称、用信天数、存款年日均、历史贷款次数、上次贷款日期。
|
||||
- 操作列:选择。
|
||||
|
||||
新增流程:
|
||||
|
||||
1. 列表页点击“新增”。
|
||||
2. 打开现有客户类型选择弹窗。
|
||||
3. 用户选择个人客户或企业客户。
|
||||
4. 打开客户号查询选择弹窗。
|
||||
5. 用户输入客户号并点击查询。
|
||||
6. 前端调用对应业务接口。
|
||||
7. 用户从结果表格选择一条客户映射记录。
|
||||
8. 前端关闭查询选择弹窗,打开对应新增弹窗。
|
||||
9. 新增弹窗将 `cust_isn` 带入 `custIsn`,将 `cust_name` 带入 `custName`。
|
||||
10. 新增弹窗中客户内码和客户名称只读。
|
||||
11. 用户继续填写其余字段并提交。
|
||||
|
||||
个人和企业新增弹窗需要增加一个入参,用于接收选中的客户映射记录。弹窗重置时清空本次客户选择,下一次新增必须重新查询选择。
|
||||
|
||||
现有创建接口保持不变:
|
||||
|
||||
- `POST /loanPricing/workflow/create/personal`
|
||||
- `POST /loanPricing/workflow/create/corporate`
|
||||
|
||||
提交时仍只提交现有流程创建字段,不提交 `faith_day`、`balance_avg`、`loan_count_his`、`last_loan_date`。
|
||||
|
||||
## 异常处理
|
||||
|
||||
- 客户号为空:前端拦截,提示“请输入客户号”。
|
||||
- 后端业务接口收到空 `custId`:返回参数错误。
|
||||
- mock 接口收到空 `cust_id`:返回参数错误。
|
||||
- 映射接口调用失败:后端返回错误,前端展示接口错误提示。
|
||||
- 映射接口返回空列表:前端提示“未查询到客户信息”。
|
||||
- 用户没有选择客户映射记录:不打开新增弹窗。
|
||||
- 新增弹窗中的客户内码、客户名称虽然只读,但保留必填校验,防止异常状态提交空值。
|
||||
|
||||
## 测试方案
|
||||
|
||||
后端验证:
|
||||
|
||||
- 个人客户映射查询接口正常返回下划线字段。
|
||||
- 企业客户映射查询接口正常返回下划线字段。
|
||||
- 缺少 `custId` 时返回参数错误。
|
||||
- mock 个人接口返回随机客户映射列表。
|
||||
- mock 企业接口返回随机客户映射列表。
|
||||
- 配置读取个人和企业映射地址,不复用模型测算地址。
|
||||
|
||||
前端验证:
|
||||
|
||||
- 客户号为空时点击查询,提示“请输入客户号”。
|
||||
- 个人客户类型下调用个人映射查询接口。
|
||||
- 企业客户类型下调用企业映射查询接口。
|
||||
- 查询结果表格展示全部返回字段。
|
||||
- 选择一条结果后,个人新增弹窗自动带入 `custIsn`、`custName`,且两项只读。
|
||||
- 选择一条结果后,企业新增弹窗自动带入 `custIsn`、`custName`,且两项只读。
|
||||
- 关闭新增弹窗后再次新增,需要重新查询客户号。
|
||||
|
||||
真实页面验证:
|
||||
|
||||
1. 启动后端进程,确保最新代码生效。
|
||||
2. 启动前端页面。
|
||||
3. 使用浏览器进入利率定价流程列表。
|
||||
4. 跑通个人客户新增:新增 -> 选择个人 -> 查询客户号 -> 选择客户内码 -> 新增弹窗只读带入 -> 填写其余字段 -> 提交。
|
||||
5. 跑通企业客户新增:新增 -> 选择企业 -> 查询客户号 -> 选择客户内码 -> 新增弹窗只读带入 -> 填写其余字段 -> 提交。
|
||||
6. 验证列表或详情中新增流程存在。
|
||||
7. 测试结束后关闭本次启动的前后端进程。
|
||||
|
||||
## 不在本次范围
|
||||
|
||||
- 不新增数据库字段。
|
||||
- 不修改现有流程创建接口请求结构。
|
||||
- 不把 `faith_day`、`balance_avg`、`loan_count_his`、`last_loan_date` 写入流程表。
|
||||
- 不调整模型测算个人/企业接口。
|
||||
- 不直接调用生产网关地址。
|
||||
@@ -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
BIN
ruoyi-admin/.DS_Store
vendored
Binary file not shown.
@@ -15,6 +15,20 @@
|
||||
web服务入口
|
||||
</description>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>tongweb-releases</id>
|
||||
<name>TongWeb Maven Releases</name>
|
||||
<url>https://mvn.elitescloud.com/nexus/repository/maven-releases/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- spring-boot-devtools -->
|
||||
@@ -40,6 +54,12 @@
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-framework</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 定时任务-->
|
||||
@@ -58,6 +78,18 @@
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-loan-pricing</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.tongweb.springboot</groupId>
|
||||
<artifactId>tongweb-spring-boot-starter-3.x</artifactId>
|
||||
<version>7.0.E.7</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -79,7 +79,18 @@ spring:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
model:
|
||||
url: http://localhost:63310/rate/pricing/mock/invokeModel
|
||||
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
|
||||
|
||||
loan-rate-history:
|
||||
url: http://localhost:63310/rate/pricing/mock/history-contract
|
||||
|
||||
security:
|
||||
password-transfer:
|
||||
|
||||
@@ -79,7 +79,18 @@ spring:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
model:
|
||||
url: http://64.202.32.40:8083/api/service/interface/invokeService/syllcs
|
||||
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
|
||||
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
|
||||
|
||||
security:
|
||||
password-transfer:
|
||||
|
||||
@@ -79,7 +79,18 @@ spring:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
model:
|
||||
url: http://localhost:63310/rate/pricing/mock/invokeModel
|
||||
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
|
||||
|
||||
loan-rate-history:
|
||||
url: http://localhost:63310/rate/pricing/mock/history-contract
|
||||
|
||||
security:
|
||||
password-transfer:
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
# 项目相关配置
|
||||
server:
|
||||
tongweb:
|
||||
license:
|
||||
path: classpath:license.dat
|
||||
|
||||
ruoyi:
|
||||
# 名称
|
||||
name: RuoYi
|
||||
|
||||
1
ruoyi-admin/src/main/resources/license.dat
Executable file
1
ruoyi-admin/src/main/resources/license.dat
Executable file
@@ -0,0 +1 @@
|
||||
uc3Y29XJfVtZtZTbmF72t3V405cxamrXBnM0P0vqrrLnJjQ0T0Mt93avL/euwcmvgpWN09qZhbWX25eO9U91ptOrcWNK1XJz6z9waqNC5L40d09ybfrmrDP352Ny76fqyPauv06+ru7f+bTwG99zvHOS8bQvJub/rL3JkoKbfbnZXJmVyVtYwMjPTIjEyQtMsaWMQpnNlNlbkPTX2lTE5EwNsaWOApnNlNlb5cGX3RmVsU9czZQZWFmVhpjcfZGdGVT0yF0Z0LTMDITEwEyLuZFCmVXRl9kYxClPS01ByRXX1Y3b2RmFtRfTUb2ZT12Vi5nVXX1ClRnNpZlcfTnb25mVyVtYuMCPTclRX5FCQVVX0N1VO9DTKYmVD0GlwluZUV1PQpXJk9IYyZVd2FD0K9JZfTWVFd051F4XlcjbWJQpU0tMFZGV19W9ul0atYmPUVk5FVkCWRVV19U9OJTSJQ0X0x0U9VOQLWUWFlTU1lLbSMmSmhkNHlBRrcVdG8kNtUxYCT2RHFTc5lperM2WUFkkvU3M3MzTDBldOlqeTb3YUVGx3VWT1WkaERHhilxOTM0T0l1FrdBS1aUWG4GE5FtaLMyUXZUlz8zM0UnSCs0lDM5RDVFRzJDBzZmOoRkNFdEt6YwNKTkTXA1ZFVXXJT0UlNElD5fTDRTRU5TdU1YdROEL2xUhvV3OLY3bTVmhMpZUJU2QXF2I1VYdxTjWVVm9jZKUPWUMXFHNrFJeJZDU29mM2hKQpUmUVJGIxwwOVSUaXFXQy9JU4cFdlkGJQY4SCYWYjFkJndiaCVFMlNk1QZQTwQWRDJ0th1YMwaHYmEzQrB2aWTTRmpgpOA5dfVkRVd0lPVSUMSUTl9kNFNFTyaWPU0G5mNwaieUdUJ0NiZjNVbnOC90tNYyb2S3djNmh4BidmNlRnBk1PdCShdDYUdS9mNMSiMnYzVml2pSameEY2NStCZtRvU3dloGgvQyU0TmcTlTlMJVYhc3VEp01EpYRwUGNUUWZ5daVxU3blZzg3dnR2UncnR2U4RkU2CkSUc19W5FVTSURVJ0xJ9OXOQ0Q0VG1iU9WFcDYWNktah2M3blbjNlZsZndjOTdXFTFHNQS0SzUE4FVpdqR1dEL2RWgvpMSabVZkJlhRJFRMZUTUJ0NGdwUGbGZitjFE01bZSlRzZHZ4RMUFd2cHhEZqtZbwR2clQUgxFsb0Z1ZFVW5tJvUwWEd2gjVog1eKYUaUdHYK5JUXX1TkVlNJZFUfTET05U5DlDRTVURV9jJi49SmbHR2pU5UFwVUK0ZTBFE5pmd3ZGOEdXppU1VTNEb09UtntCRycUaHUmR1ovVYNGUU1HFy1vVsZXYlBmt1lsNVZDVUNUNyJQeHUXZlF2pzVMVlVjVmNWx2VxWZaUSEh1FlJ0bmRmbENkRV9VWiRVT3R01apMUIMkWjdW8K5JTXX1TkVlNJZFUfTET05U5DlDRTVURV9Ulw49bhMWcW5HdRdZNaK0UEp3AyU0TwY0Wm1md6tDMpN0cDRVBK11aKR0VTZkI3RocKUXRm5Vl6llcpMWaXJXRLZOOaZDOGZ1R0gychWURVJk5JR5VNYXQTl250dGYheFOG1FZog4RFZWMkRmtK1QRCaUclJlNYNFRHSWTFpFUKxYRXX1TkVlNJZFUfTET05U5DlDRTVURV9Hho49T5aTempVMwFhU1b1Vi9HR4YzO1dDNks05HhEQxY2VUZXlMNybyVmVEl3dNlLbYSGbGlWxMVNWUcEZXpVN0w5NVZDVGFUw4NMSUOWbkRjV21QaMbESVhGx1w3MiY1WmJXB6o0NjS1T2tWxjNSeRY0UzV0g2VhR5Z0RWlzkKRMdXX1TkVlNJZFUfTET05U5DlDRTVURV9Hdt49apUHZVNnhlpxQ5MENGNnh1VYN3aDQ2QW5qRqd4K1cXYk9ZdHW4VzeE9XVHB6YmM3Wk1DYwVLdqS1aTNUtjhINicVeUV1JBZRZxTGYWdTVytuepR1QVVXZlNoSVOFdVlVkzRqdPcjOW9HBll6Ota2dHFGV6dtN6c1ekN2UKdwc
|
||||
@@ -21,6 +21,8 @@ import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONWriter;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import org.springframework.http.*;
|
||||
@@ -75,7 +77,7 @@ public class HttpUtils
|
||||
try
|
||||
{
|
||||
String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url;
|
||||
log.info("sendGet - {}", urlNameString);
|
||||
log.info("后端外部接口调用开始\n请求URL:{}\n请求参数:\n{}", urlNameString, formatLogValue(param));
|
||||
URL realUrl = new URL(urlNameString);
|
||||
URLConnection connection = realUrl.openConnection();
|
||||
connection.setRequestProperty("accept", "*/*");
|
||||
@@ -88,7 +90,8 @@ public class HttpUtils
|
||||
{
|
||||
result.append(line);
|
||||
}
|
||||
log.info("recv - {}", result);
|
||||
log.info("后端外部接口调用完成\n请求URL:{}\n请求参数:\n{}\n返回参数:\n{}",
|
||||
urlNameString, formatLogValue(param), formatLogValue(result));
|
||||
}
|
||||
catch (ConnectException e)
|
||||
{
|
||||
@@ -150,7 +153,7 @@ public class HttpUtils
|
||||
StringBuilder result = new StringBuilder();
|
||||
try
|
||||
{
|
||||
log.info("sendPost - {}", url);
|
||||
log.info("后端外部接口调用开始\n请求URL:{}\n请求参数:\n{}", url, formatLogValue(param));
|
||||
URL realUrl = new URL(url);
|
||||
URLConnection conn = realUrl.openConnection();
|
||||
conn.setRequestProperty("accept", "*/*");
|
||||
@@ -169,7 +172,8 @@ public class HttpUtils
|
||||
{
|
||||
result.append(line);
|
||||
}
|
||||
log.info("recv - {}", result);
|
||||
log.info("后端外部接口调用完成\n请求URL:{}\n请求参数:\n{}\n返回参数:\n{}",
|
||||
url, formatLogValue(param), formatLogValue(result));
|
||||
}
|
||||
catch (ConnectException e)
|
||||
{
|
||||
@@ -219,7 +223,7 @@ public class HttpUtils
|
||||
String urlNameString = url + "?" + param;
|
||||
try
|
||||
{
|
||||
log.info("sendSSLPost - {}", urlNameString);
|
||||
log.info("后端外部接口调用开始\n请求URL:{}\n请求参数:\n{}", urlNameString, formatLogValue(param));
|
||||
SSLContext sc = SSLContext.getInstance("SSL");
|
||||
sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());
|
||||
URL console = new URL(urlNameString);
|
||||
@@ -245,7 +249,8 @@ public class HttpUtils
|
||||
result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
log.info("recv - {}", result);
|
||||
log.info("后端外部接口调用完成\n请求URL:{}\n请求参数:\n{}\n返回参数:\n{}",
|
||||
urlNameString, formatLogValue(param), formatLogValue(result));
|
||||
conn.disconnect();
|
||||
br.close();
|
||||
}
|
||||
@@ -327,7 +332,8 @@ public class HttpUtils
|
||||
requestEntity,
|
||||
responseType
|
||||
);
|
||||
log.info("---------------------->POST(form-urlencoded) 请求成功,URL:{},响应结果:{}", url, response.getBody());
|
||||
log.info("后端外部接口调用完成\n请求URL:{}\n请求参数:\n{}\n返回参数:\n{}",
|
||||
url, formatLogValue(params), formatLogValue(response.getBody()));
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("POST(form-urlencoded) 请求失败,URL:" + url + ",异常信息:" + e.getMessage(), e);
|
||||
@@ -364,10 +370,31 @@ public class HttpUtils
|
||||
requestEntity,
|
||||
responseType
|
||||
);
|
||||
log.info("---------------------->POST(JSON) 请求成功,URL:{},响应结果:{}", url, response.getBody());
|
||||
log.info("后端外部接口调用完成\n请求URL:{}\n请求参数:\n{}\n返回参数:\n{}",
|
||||
url, formatLogValue(requestBody), formatLogValue(response.getBody()));
|
||||
return response.getBody();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("POST(JSON) 请求失败,URL:" + url + ",异常信息:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatLogValue(Object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
if (value instanceof CharSequence)
|
||||
{
|
||||
return value.toString();
|
||||
}
|
||||
try
|
||||
{
|
||||
return JSON.toJSONString(value, JSONWriter.Feature.PrettyFormat);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return String.valueOf(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowListVO;
|
||||
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
|
||||
import com.ruoyi.loanpricing.service.ILoanPricingWorkflowService;
|
||||
import com.ruoyi.loanpricing.service.LoanPricingCustomerMapService;
|
||||
import com.ruoyi.loanpricing.service.LoanRateHistoryService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -38,6 +40,12 @@ public class LoanPricingWorkflowController extends BaseController
|
||||
@Autowired
|
||||
private ILoanPricingWorkflowService loanPricingWorkflowService;
|
||||
|
||||
@Autowired
|
||||
private LoanPricingCustomerMapService customerMapService;
|
||||
|
||||
@Autowired
|
||||
private LoanRateHistoryService loanRateHistoryService;
|
||||
|
||||
/**
|
||||
* 发起个人客户利率定价流程
|
||||
*/
|
||||
@@ -61,6 +69,36 @@ public class LoanPricingWorkflowController extends BaseController
|
||||
return success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询个人客户号映射
|
||||
*/
|
||||
@Operation(summary = "查询个人客户号映射")
|
||||
@GetMapping("/customer-map/personal")
|
||||
public AjaxResult queryPersonalCustomerMap(@RequestParam("custId") String custId)
|
||||
{
|
||||
return success(customerMapService.queryPersonal(custId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询企业客户号映射
|
||||
*/
|
||||
@Operation(summary = "查询企业客户号映射")
|
||||
@GetMapping("/customer-map/corporate")
|
||||
public AjaxResult queryCorporateCustomerMap(@RequestParam("custId") String custId)
|
||||
{
|
||||
return success(customerMapService.queryCorporate(custId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询历史贷款合同
|
||||
*/
|
||||
@Operation(summary = "查询历史贷款合同")
|
||||
@GetMapping("/history-contract")
|
||||
public AjaxResult queryHistoryContract(@RequestParam("custIsn") String custIsn)
|
||||
{
|
||||
return success(loanRateHistoryService.query(custIsn));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询利率定价流程列表
|
||||
*/
|
||||
|
||||
@@ -6,15 +6,24 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
|
||||
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
|
||||
import com.ruoyi.loanpricing.domain.vo.CustomerMapRecordVO;
|
||||
import com.ruoyi.loanpricing.domain.vo.HistoryLoanContractVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
|
||||
/**
|
||||
@@ -27,17 +36,60 @@ import java.io.InputStream;
|
||||
public class LoanRatePricingMockController extends BaseController {
|
||||
|
||||
@Anonymous
|
||||
@Operation(summary = "调用模型获取测算利率")
|
||||
@PostMapping("/invokeModel")
|
||||
public AjaxResult invokeModel( ModelInvokeDTO modelInvokeDTO) {
|
||||
ObjectNode jsonNodes;
|
||||
if (modelInvokeDTO.getCustType().equals("个人")) {
|
||||
jsonNodes = loadJsonFromResource("data/retail_output.json");
|
||||
} else {
|
||||
jsonNodes = loadJsonFromResource("data/corp_output.json");
|
||||
}
|
||||
@Operation(summary = "调用个人模型获取测算利率")
|
||||
@PostMapping("/invokeModel/personal")
|
||||
public AjaxResult invokePersonalModel(ModelInvokeDTO modelInvokeDTO) {
|
||||
return new AjaxResult(10000, "success", loadJsonFromResource("data/retail_output.json"));
|
||||
}
|
||||
|
||||
return new AjaxResult(10000, "success", jsonNodes);
|
||||
@Anonymous
|
||||
@Operation(summary = "调用企业模型获取测算利率")
|
||||
@PostMapping("/invokeModel/corporate")
|
||||
public AjaxResult invokeCorporateModel(ModelInvokeDTO modelInvokeDTO) {
|
||||
return new AjaxResult(10000, "success", loadJsonFromResource("data/corp_output.json"));
|
||||
}
|
||||
|
||||
@Anonymous
|
||||
@Operation(summary = "模拟个人客户号映射查询")
|
||||
@GetMapping("/customer-map/personal")
|
||||
public AjaxResult queryPersonalCustomerMap(@RequestParam("cust_id") String custId)
|
||||
{
|
||||
return success(randomCustomerMapRecords(custId, "个人客户"));
|
||||
}
|
||||
|
||||
@Anonymous
|
||||
@Operation(summary = "模拟企业客户号映射查询")
|
||||
@GetMapping("/customer-map/corporate")
|
||||
public AjaxResult queryCorporateCustomerMap(@RequestParam("cust_id") String custId)
|
||||
{
|
||||
return success(randomCustomerMapRecords(custId, "企业客户"));
|
||||
}
|
||||
|
||||
@Anonymous
|
||||
@Operation(summary = "模拟历史贷款合同查询")
|
||||
@GetMapping("/history-contract")
|
||||
public AjaxResult queryHistoryContract(@RequestParam("cust_isn") String custIsn)
|
||||
{
|
||||
String normalizedCustIsn = StringUtils.trimWhitespace(custIsn);
|
||||
if (!StringUtils.hasText(normalizedCustIsn))
|
||||
{
|
||||
throw new ServiceException("客户内码不能为空");
|
||||
}
|
||||
if ("EMPTY_HISTORY".equals(normalizedCustIsn))
|
||||
{
|
||||
return success(Collections.emptyList());
|
||||
}
|
||||
List<HistoryLoanContractVO> records = new ArrayList<>();
|
||||
HistoryLoanContractVO record = new HistoryLoanContractVO();
|
||||
record.setCustIsn(normalizedCustIsn);
|
||||
record.setLoanContractHistory("HT" + normalizedCustIsn);
|
||||
record.setGuarTypeHistory("信用");
|
||||
record.setProductCodeHistory("P001");
|
||||
record.setLoanRateHistory("EMPTY_RATE".equals(normalizedCustIsn) ? "" : "3.65");
|
||||
record.setLoanAmountHistory("100000");
|
||||
record.setLoanSignDateHistory(LocalDate.now().minusMonths(6).toString());
|
||||
records.add(record);
|
||||
return success(records);
|
||||
}
|
||||
|
||||
private ObjectNode loadJsonFromResource(String resourcePath){
|
||||
@@ -51,5 +103,52 @@ public class LoanRatePricingMockController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
private List<CustomerMapRecordVO> randomCustomerMapRecords(String custId, String namePrefix)
|
||||
{
|
||||
String normalizedCustId = StringUtils.trimWhitespace(custId);
|
||||
if (!StringUtils.hasText(normalizedCustId))
|
||||
{
|
||||
throw new ServiceException("客户号不能为空");
|
||||
}
|
||||
if ("HISTORY_EMPTY".equals(normalizedCustId))
|
||||
{
|
||||
CustomerMapRecordVO record = new CustomerMapRecordVO();
|
||||
record.setCustId(normalizedCustId);
|
||||
record.setCustIsn("EMPTY_HISTORY");
|
||||
record.setCustName(namePrefix + "空历史合同");
|
||||
record.setFaithDay("0");
|
||||
record.setBalanceAvg("0");
|
||||
record.setLoanCountHis("0");
|
||||
record.setLastLoanDate("");
|
||||
return Collections.singletonList(record);
|
||||
}
|
||||
if ("HISTORY_EMPTY_RATE".equals(normalizedCustId))
|
||||
{
|
||||
CustomerMapRecordVO record = new CustomerMapRecordVO();
|
||||
record.setCustId(normalizedCustId);
|
||||
record.setCustIsn("EMPTY_RATE");
|
||||
record.setCustName(namePrefix + "空历史利率");
|
||||
record.setFaithDay("30");
|
||||
record.setBalanceAvg("10000");
|
||||
record.setLoanCountHis("1");
|
||||
record.setLastLoanDate(LocalDate.now().minusMonths(3).toString());
|
||||
return Collections.singletonList(record);
|
||||
}
|
||||
int count = ThreadLocalRandom.current().nextInt(1, 4);
|
||||
List<CustomerMapRecordVO> records = new ArrayList<>();
|
||||
for (int i = 1; i <= count; i++)
|
||||
{
|
||||
CustomerMapRecordVO record = new CustomerMapRecordVO();
|
||||
record.setCustId(normalizedCustId);
|
||||
record.setCustIsn(String.valueOf(81000000000L + ThreadLocalRandom.current().nextInt(1000000)));
|
||||
record.setCustName(namePrefix + i);
|
||||
record.setFaithDay(String.valueOf(ThreadLocalRandom.current().nextInt(1, 365)));
|
||||
record.setBalanceAvg(String.valueOf(ThreadLocalRandom.current().nextInt(10000, 900000)));
|
||||
record.setLoanCountHis(String.valueOf(ThreadLocalRandom.current().nextInt(0, 10)));
|
||||
record.setLastLoanDate(LocalDate.now().minusDays(ThreadLocalRandom.current().nextInt(1, 800)).toString());
|
||||
records.add(record);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,24 +41,34 @@ public class CorporateLoanPricingCreateDTO implements Serializable {
|
||||
@NotBlank(message = "申请金额不能为空")
|
||||
private String applyAmt;
|
||||
|
||||
@Schema(description = "贷款期限", example = "36")
|
||||
@Schema(description = "业务种类", requiredMode = Schema.RequiredMode.REQUIRED, example = "存量转贷", allowableValues = {"新增", "存量新增", "存量转贷"})
|
||||
@NotBlank(message = "业务种类不能为空")
|
||||
@Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一")
|
||||
private String businessType;
|
||||
|
||||
@Schema(description = "历史贷款利率", example = "3.65")
|
||||
private String loanRateHistory;
|
||||
|
||||
@Schema(description = "还款方式", example = "分期", allowableValues = {"分期", "不分期"})
|
||||
private String repayMethod;
|
||||
|
||||
@Schema(description = "借款期限(年)", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
|
||||
@NotBlank(message = "贷款期限不能为空")
|
||||
private String loanTerm;
|
||||
|
||||
@Schema(description = "省农担担保贷款", example = "false")
|
||||
private String isAgriGuar;
|
||||
|
||||
@Schema(description = "绿色贷款", example = "true")
|
||||
@Schema(description = "绿色贷款", example = "0")
|
||||
private String isGreenLoan;
|
||||
|
||||
@Schema(description = "科技型企业", example = "true")
|
||||
private String isTechEnt;
|
||||
@Schema(description = "贸易和建筑业企业标识", example = "0")
|
||||
private String isTradeBuildEnt;
|
||||
|
||||
@Schema(description = "贸易和建筑业企业标识", example = "false")
|
||||
private String isTradeConstruction;
|
||||
|
||||
@Schema(description = "抵质押类型", example = "一类")
|
||||
@Schema(description = "抵质押类型", example = "一类", allowableValues = {"一类", "二类", "三类", "四类", "排污权抵押", "设备等其他不动产抵押", "存单质押", "股权质押", "其他质押"})
|
||||
@Pattern(regexp = "^(一类|二类|三类|四类|排污权抵押|设备等其他不动产抵押|存单质押|股权质押|其他质押)$", message = "抵质押类型必须是:一类、二类、三类、四类、排污权抵押、设备等其他不动产抵押、存单质押、股权质押、其他质押之一")
|
||||
private String collType;
|
||||
|
||||
@Schema(description = "抵质押物是否三方所有", example = "false")
|
||||
@Schema(description = "抵质押物是否三方所有", example = "0")
|
||||
private String collThirdParty;
|
||||
|
||||
@Schema(description = "存单票面利率", example = "2.15")
|
||||
private String couponRate;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,12 @@ public class ModelInvokeDTO {
|
||||
*/
|
||||
private String loanTerm;
|
||||
|
||||
/**
|
||||
* 还款方式(非必填)
|
||||
* 可选值:分期/不分期
|
||||
*/
|
||||
private String repayMethod;
|
||||
|
||||
/**
|
||||
* 净身企业(非必填)
|
||||
* 可选值:true/false
|
||||
@@ -98,10 +104,10 @@ public class ModelInvokeDTO {
|
||||
private String isManufacturing;
|
||||
|
||||
/**
|
||||
* 省农担担保贷款(非必填)
|
||||
* 可选值:true/false
|
||||
* 绿色贷款(非必填)
|
||||
* 可选值:0/1
|
||||
*/
|
||||
private String isAgriGuar;
|
||||
private String isGreenLoan;
|
||||
|
||||
/**
|
||||
* 是否纳税信用等级A级(非必填)
|
||||
@@ -137,7 +143,7 @@ public class ModelInvokeDTO {
|
||||
|
||||
/**
|
||||
* 抵质押类型(非必填)
|
||||
* 可选值:一类/二类/三类
|
||||
* 可选值:一类/二类/三类/四类
|
||||
*/
|
||||
private String collType;
|
||||
|
||||
@@ -147,6 +153,16 @@ public class ModelInvokeDTO {
|
||||
*/
|
||||
private String collThirdParty;
|
||||
|
||||
/**
|
||||
* 历史贷款利率
|
||||
*/
|
||||
private String loanRateHistory;
|
||||
|
||||
/**
|
||||
* 存单票面利率
|
||||
*/
|
||||
private String couponRate;
|
||||
|
||||
// /**
|
||||
// * 贷款利率(必填)
|
||||
// */
|
||||
@@ -167,4 +183,10 @@ public class ModelInvokeDTO {
|
||||
*/
|
||||
private String idNum;
|
||||
|
||||
/**
|
||||
* 贸易和建筑业企业(非必填)
|
||||
* 可选值:0/1
|
||||
*/
|
||||
private String isTradeBuildEnt;
|
||||
|
||||
}
|
||||
|
||||
@@ -41,24 +41,27 @@ 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 = {"新增", "存量新增", "存量转贷"})
|
||||
@NotBlank(message = "业务种类不能为空")
|
||||
@Pattern(regexp = "^(新增|存量新增|存量转贷)$", message = "业务种类必须是:新增、存量新增、存量转贷之一")
|
||||
private String businessType;
|
||||
|
||||
@Schema(description = "历史贷款利率", example = "3.65")
|
||||
private String loanRateHistory;
|
||||
|
||||
@Schema(description = "借款期限(年)", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
|
||||
@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;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,11 @@ public class LoanPricingWorkflow implements Serializable
|
||||
*/
|
||||
private String loanTerm;
|
||||
|
||||
/**
|
||||
* 还款方式: 分期/不分期
|
||||
*/
|
||||
private String repayMethod;
|
||||
|
||||
/** 净身企业: true/false */
|
||||
private String isCleanEnt;
|
||||
|
||||
@@ -78,24 +83,17 @@ public class LoanPricingWorkflow implements Serializable
|
||||
/** 制造业企业: true/false */
|
||||
private String isManufacturing;
|
||||
|
||||
/** 省农担担保贷款: true/false */
|
||||
private String isAgriGuar;
|
||||
|
||||
/**
|
||||
* 贸易和建筑业企业标识: true/false
|
||||
*/
|
||||
private String isTradeConstruction;
|
||||
@TableField("is_trade_construction")
|
||||
private String isTradeBuildEnt;
|
||||
|
||||
/**
|
||||
* 绿色贷款: true/false
|
||||
*/
|
||||
private String isGreenLoan;
|
||||
|
||||
/**
|
||||
* 科技型企业: true/false
|
||||
*/
|
||||
private String isTechEnt;
|
||||
|
||||
/** 是否纳税信用等级A级: true/false */
|
||||
private String isTaxA;
|
||||
|
||||
@@ -105,13 +103,22 @@ public class LoanPricingWorkflow implements Serializable
|
||||
/** 贷款用途: consumer-消费/business-经营 */
|
||||
private String loanPurpose;
|
||||
|
||||
/** 业务种类: 新增/存量新增/存量转贷 */
|
||||
private String businessType;
|
||||
|
||||
/** 历史贷款利率 */
|
||||
private String loanRateHistory;
|
||||
|
||||
/** 存单票面利率 */
|
||||
private String couponRate;
|
||||
|
||||
/** 是否有经营佐证: true/false */
|
||||
private String bizProof;
|
||||
|
||||
/** 循环功能: true/false */
|
||||
private String loanLoop;
|
||||
|
||||
/** 抵质押类型: 一线/一类/二类 */
|
||||
/** 抵质押类型: 一类/二类/三类/四类 */
|
||||
private String collType;
|
||||
|
||||
/** 抵质押物是否三方所有: true/false */
|
||||
@@ -143,6 +150,10 @@ public class LoanPricingWorkflow implements Serializable
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/** 列表数据权限创建者过滤条件 */
|
||||
@TableField(exist = false)
|
||||
private String dataScopeCreateBy;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
|
||||
@@ -31,6 +31,8 @@ public class ModelCorpOutputFields {
|
||||
private String idType;
|
||||
// 证件号码
|
||||
private String idNum;
|
||||
// 还款方式
|
||||
private String repayMethod;
|
||||
// 基准利率
|
||||
private String baseLoanRate;
|
||||
// 我行首贷客户
|
||||
@@ -65,8 +67,6 @@ public class ModelCorpOutputFields {
|
||||
private String midEntEleDdc;
|
||||
// 中间业务_企业_水费代扣
|
||||
private String midEntWaterDdc;
|
||||
// 中间业务_企业_税务代扣
|
||||
private String midEntTax;
|
||||
// BP_中间业务
|
||||
private String bpMid;
|
||||
// 代发工资户数
|
||||
@@ -79,12 +79,12 @@ public class ModelCorpOutputFields {
|
||||
private String isCleanEnt;
|
||||
// 开立基本结算账户
|
||||
private String hasSettleAcct;
|
||||
// 省农担担保贷款
|
||||
private String isAgriGuar;
|
||||
// 绿色贷款
|
||||
private String isGreenLoan;
|
||||
// 科技型企业
|
||||
private String isTechEnt;
|
||||
// 贸易和建筑业企业
|
||||
private String isTradeBuildEnt;
|
||||
// BP_企业客户类别
|
||||
private String bpEntType;
|
||||
// TOTAL_BP_关联度
|
||||
@@ -109,8 +109,6 @@ public class ModelCorpOutputFields {
|
||||
private String prinOverdue;
|
||||
// 利息逾期
|
||||
private String interestOverdue;
|
||||
// 信用卡逾期
|
||||
private String cardOverdue;
|
||||
// BP_灰名单与逾期
|
||||
private String bpGreyOverdue;
|
||||
// TOTAL_BP_风险度
|
||||
@@ -119,6 +117,16 @@ public class ModelCorpOutputFields {
|
||||
private String totalBp;
|
||||
// 测算利率
|
||||
private String calculateRate;
|
||||
// 历史利率
|
||||
private String loanRateHistory;
|
||||
// 产品最低利率下限
|
||||
private String minRateProduct;
|
||||
// 平滑幅度
|
||||
private String smoothRange;
|
||||
// 最终测算利率
|
||||
private String finalCalculateRate;
|
||||
// 参考利率
|
||||
private String referenceRate;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
@@ -41,6 +41,9 @@ public class ModelRetailOutputFields {
|
||||
// 基准利率
|
||||
private String baseLoanRate;
|
||||
|
||||
// 灰黑名单客户
|
||||
private String greyBlackCust;
|
||||
|
||||
// 我行首贷客户
|
||||
private String isFirstLoan;
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.ruoyi.loanpricing.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 客户号与客户内码映射记录。
|
||||
*/
|
||||
@Data
|
||||
public class CustomerMapRecordVO implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonProperty("cust_id")
|
||||
private String custId;
|
||||
|
||||
@JsonProperty("cust_isn")
|
||||
private String custIsn;
|
||||
|
||||
@JsonProperty("cust_name")
|
||||
private String custName;
|
||||
|
||||
@JsonProperty("faith_day")
|
||||
private String faithDay;
|
||||
|
||||
@JsonProperty("balance_avg")
|
||||
private String balanceAvg;
|
||||
|
||||
@JsonProperty("loan_count_his")
|
||||
private String loanCountHis;
|
||||
|
||||
@JsonProperty("last_loan_date")
|
||||
private String lastLoanDate;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ruoyi.loanpricing.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class HistoryLoanContractVO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonProperty("cust_isn")
|
||||
private String custIsn;
|
||||
|
||||
@JsonProperty("loan_contract_history")
|
||||
private String loanContractHistory;
|
||||
|
||||
@JsonProperty("guar_type_history")
|
||||
private String guarTypeHistory;
|
||||
|
||||
@JsonProperty("product_code_history")
|
||||
private String productCodeHistory;
|
||||
|
||||
@JsonProperty("loan_rate_history")
|
||||
private String loanRateHistory;
|
||||
|
||||
@JsonProperty("loan_amount_history")
|
||||
private String loanAmountHistory;
|
||||
|
||||
@JsonProperty("loan_sign_date_history")
|
||||
private String loanSignDateHistory;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
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;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* 客户号与客户内码映射查询服务。
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class LoanPricingCustomerMapService
|
||||
{
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Value("${customer-map.personal-url}")
|
||||
private String personalUrl;
|
||||
|
||||
@Value("${customer-map.corporate-url}")
|
||||
private String corporateUrl;
|
||||
|
||||
@Value("${loan-pricing-external.app-code:}")
|
||||
private String appCode;
|
||||
|
||||
public LoanPricingCustomerMapService()
|
||||
{
|
||||
this(new RestTemplate());
|
||||
}
|
||||
|
||||
LoanPricingCustomerMapService(RestTemplate restTemplate)
|
||||
{
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return query(personalUrl, custId);
|
||||
}
|
||||
|
||||
public List<CustomerMapRecordVO> queryCorporate(String custId)
|
||||
{
|
||||
return query(corporateUrl, custId);
|
||||
}
|
||||
|
||||
private List<CustomerMapRecordVO> query(String url, String custId)
|
||||
{
|
||||
String normalizedCustId = StringUtils.trimWhitespace(custId);
|
||||
if (!StringUtils.hasText(normalizedCustId))
|
||||
{
|
||||
throw new ServiceException("客户号不能为空");
|
||||
}
|
||||
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(requestParams),
|
||||
HttpUtils.formatLogValue(response));
|
||||
if (response == null)
|
||||
{
|
||||
throw new ServiceException("客户号映射接口无返回");
|
||||
}
|
||||
if (response.getCode() != null && response.getCode() != 200)
|
||||
{
|
||||
throw new ServiceException(StringUtils.hasText(response.getMsg()) ? response.getMsg() : "客户号映射查询失败");
|
||||
}
|
||||
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
|
||||
{
|
||||
private Integer code;
|
||||
private String msg;
|
||||
private List<CustomerMapRecordVO> data;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.common.utils.bean.BeanUtils;
|
||||
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
@@ -12,7 +10,6 @@ import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
|
||||
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -64,10 +61,13 @@ public class LoanPricingModelService {
|
||||
{
|
||||
normalizePersonalModelInvokeDTO(modelInvokeDTO);
|
||||
}
|
||||
JSONObject response = modelService.invokeModel(modelInvokeDTO);
|
||||
if ("企业".equals(loanPricingWorkflow.getCustType()))
|
||||
{
|
||||
normalizeCorporateModelInvokeDTO(modelInvokeDTO);
|
||||
}
|
||||
if (loanPricingWorkflow.getCustType().equals("个人")){
|
||||
// 个人模型
|
||||
ModelRetailOutputFields modelRetailOutputFields = JSON.parseObject(response.toJSONString(), ModelRetailOutputFields.class);
|
||||
ModelRetailOutputFields modelRetailOutputFields = modelService.invokePersonalModel(modelInvokeDTO);
|
||||
modelRetailOutputFieldsMapper.insert(modelRetailOutputFields);
|
||||
log.info("个人模型调用成功");
|
||||
LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow();
|
||||
@@ -77,7 +77,7 @@ public class LoanPricingModelService {
|
||||
log.info("更新流程信息成功");
|
||||
}else if (loanPricingWorkflow.getCustType().equals("企业")){
|
||||
// 企业模型
|
||||
ModelCorpOutputFields modelCorpOutputFields = JSON.parseObject(response.toJSONString(), ModelCorpOutputFields.class);
|
||||
ModelCorpOutputFields modelCorpOutputFields = modelService.invokeCorporateModel(modelInvokeDTO);
|
||||
modelCorpOutputFieldsMapper.insert(modelCorpOutputFields);
|
||||
log.info("企业模型调用成功");
|
||||
LoanPricingWorkflow workflowToUpdate = new LoanPricingWorkflow();
|
||||
@@ -95,6 +95,13 @@ public class LoanPricingModelService {
|
||||
modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty()));
|
||||
}
|
||||
|
||||
private void normalizeCorporateModelInvokeDTO(ModelInvokeDTO modelInvokeDTO)
|
||||
{
|
||||
modelInvokeDTO.setCollThirdParty(toZeroOne(modelInvokeDTO.getCollThirdParty()));
|
||||
modelInvokeDTO.setIsGreenLoan(toZeroOne(modelInvokeDTO.getIsGreenLoan()));
|
||||
modelInvokeDTO.setIsTradeBuildEnt(toZeroOne(modelInvokeDTO.getIsTradeBuildEnt()));
|
||||
}
|
||||
|
||||
private String toZeroOne(String value)
|
||||
{
|
||||
if ("true".equals(value) || "1".equals(value))
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
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;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class LoanRateHistoryService
|
||||
{
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
@Value("${loan-rate-history.url}")
|
||||
private String historyUrl;
|
||||
|
||||
@Value("${loan-pricing-external.app-code:}")
|
||||
private String appCode;
|
||||
|
||||
public LoanRateHistoryService()
|
||||
{
|
||||
this(new RestTemplate());
|
||||
}
|
||||
|
||||
LoanRateHistoryService(RestTemplate restTemplate)
|
||||
{
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
String normalizedCustIsn = StringUtils.trimWhitespace(custIsn);
|
||||
if (!StringUtils.hasText(normalizedCustIsn))
|
||||
{
|
||||
throw new ServiceException("客户内码不能为空");
|
||||
}
|
||||
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(requestParams),
|
||||
HttpUtils.formatLogValue(response));
|
||||
if (response == null)
|
||||
{
|
||||
throw new ServiceException("历史贷款合同接口无返回");
|
||||
}
|
||||
if (response.getCode() != null && response.getCode() != 200)
|
||||
{
|
||||
throw new ServiceException(StringUtils.hasText(response.getMsg()) ? response.getMsg() : "历史贷款合同查询失败");
|
||||
}
|
||||
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
|
||||
{
|
||||
private Integer code;
|
||||
private String msg;
|
||||
private List<HistoryLoanContractVO> data;
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,11 @@ package com.ruoyi.loanpricing.service;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.alibaba.fastjson2.TypeReference;
|
||||
import com.ruoyi.common.core.domain.entity.SysDictData;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.http.HttpUtils;
|
||||
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
|
||||
@@ -28,19 +27,29 @@ import java.util.Objects;
|
||||
@EnableAsync
|
||||
public class ModelService {
|
||||
|
||||
@Value("${model.url}")
|
||||
private String modelUrl;
|
||||
@Value("${model.personal-url}")
|
||||
private String personalModelUrl;
|
||||
|
||||
@Value("${model.corporate-url}")
|
||||
private String corporateModelUrl;
|
||||
|
||||
|
||||
public ModelRetailOutputFields invokePersonalModel(ModelInvokeDTO modelInvokeDTO) {
|
||||
JSONObject mappingOutputFields = invokeModel(personalModelUrl, modelInvokeDTO);
|
||||
return JSON.parseObject(mappingOutputFields.toJSONString(), ModelRetailOutputFields.class);
|
||||
}
|
||||
|
||||
public JSONObject invokeModel(ModelInvokeDTO modelInvokeDTO) {
|
||||
public ModelCorpOutputFields invokeCorporateModel(ModelInvokeDTO modelInvokeDTO) {
|
||||
JSONObject mappingOutputFields = invokeModel(corporateModelUrl, modelInvokeDTO);
|
||||
return JSON.parseObject(mappingOutputFields.toJSONString(), ModelCorpOutputFields.class);
|
||||
}
|
||||
|
||||
private JSONObject invokeModel(String modelUrl, ModelInvokeDTO modelInvokeDTO) {
|
||||
Map<String, String> requestBody = entityToMap(modelInvokeDTO);
|
||||
JSONObject response = HttpUtils.doPostFormUrlEncoded(modelUrl, requestBody, null, JSONObject.class);
|
||||
log.info("------------------->调用模型返回结果:" + JSON.toJSONString(response));
|
||||
if(Objects.nonNull(response) && response.containsKey("code") && response.getInteger("code") == 10000){
|
||||
JSONObject mappingOutputFields = response.getJSONObject("data").getJSONObject("mappingOutputFields");
|
||||
// return JSON.parseObject(mappingOutputFields.toJSONString(), ModelOutputFields.class);
|
||||
return mappingOutputFields;
|
||||
return response.getJSONObject("data").getJSONObject("mappingOutputFields");
|
||||
}else{
|
||||
log.error("------------------->调用模型失败,失败原因为:" + response.getString("message"));
|
||||
throw new ServiceException("调用模型失败");
|
||||
|
||||
@@ -3,6 +3,12 @@ package com.ruoyi.loanpricing.service.impl;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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;
|
||||
@@ -37,6 +43,10 @@ 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";
|
||||
|
||||
@Resource
|
||||
private LoanPricingWorkflowMapper loanPricingWorkflowMapper;
|
||||
|
||||
@@ -65,6 +75,9 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public LoanPricingWorkflow createLoanPricing(LoanPricingWorkflow loanPricingWorkflow)
|
||||
{
|
||||
validateBusinessTypeAndHistoryRate(loanPricingWorkflow);
|
||||
validateCollateralTypeAndCouponRate(loanPricingWorkflow);
|
||||
|
||||
// 自动生成业务方流水号(时间戳)
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
|
||||
String serialNum = sdf.format(new Date());
|
||||
@@ -88,6 +101,72 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
|
||||
return loanPricingWorkflow;
|
||||
}
|
||||
|
||||
private void validateBusinessTypeAndHistoryRate(LoanPricingWorkflow workflow) {
|
||||
if (!StringUtils.hasText(workflow.getBusinessType())) {
|
||||
throw new ServiceException("业务种类不能为空");
|
||||
}
|
||||
if (!"新增".equals(workflow.getBusinessType())
|
||||
&& !"存量新增".equals(workflow.getBusinessType())
|
||||
&& !"存量转贷".equals(workflow.getBusinessType())) {
|
||||
throw new ServiceException("业务种类必须是:新增、存量新增、存量转贷之一");
|
||||
}
|
||||
if ("存量转贷".equals(workflow.getBusinessType())
|
||||
&& !StringUtils.hasText(workflow.getLoanRateHistory())) {
|
||||
throw new ServiceException("请选择历史贷款合同");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起个人客户利率定价流程
|
||||
*
|
||||
@@ -139,7 +218,8 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
|
||||
@Override
|
||||
public IPage<LoanPricingWorkflowListVO> selectLoanPricingPage(Page<LoanPricingWorkflowListVO> page, LoanPricingWorkflow loanPricingWorkflow)
|
||||
{
|
||||
IPage<LoanPricingWorkflowListVO> pageResult = loanPricingWorkflowMapper.selectWorkflowPageWithRates(page, loanPricingWorkflow);
|
||||
LoanPricingWorkflow scopedQuery = applyWorkflowListDataScope(loanPricingWorkflow);
|
||||
IPage<LoanPricingWorkflowListVO> pageResult = loanPricingWorkflowMapper.selectWorkflowPageWithRates(page, scopedQuery);
|
||||
pageResult.getRecords().forEach(row -> row.setCustName(
|
||||
loanPricingSensitiveDisplayService.maskCustName(
|
||||
sensitiveFieldCryptoService.decrypt(row.getCustName()))));
|
||||
@@ -181,7 +261,7 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
|
||||
if (Objects.nonNull(modelCorpOutputFields))
|
||||
{
|
||||
maskModelCorpOutputBasicInfo(modelCorpOutputFields);
|
||||
loanPricingWorkflow.setLoanRate(modelCorpOutputFields.getCalculateRate());
|
||||
loanPricingWorkflow.setLoanRate(modelCorpOutputFields.getFinalCalculateRate());
|
||||
}
|
||||
loanPricingWorkflowVO.setModelCorpOutputFields(modelCorpOutputFields);
|
||||
}
|
||||
@@ -222,6 +302,41 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private LoanPricingWorkflow applyWorkflowListDataScope(LoanPricingWorkflow query)
|
||||
{
|
||||
LoanPricingWorkflow scopedQuery = query == null ? new LoanPricingWorkflow() : query;
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (!canViewAllWorkflows(loginUser))
|
||||
{
|
||||
scopedQuery.setDataScopeCreateBy(buildCurrentCreateBy(loginUser));
|
||||
}
|
||||
return scopedQuery;
|
||||
}
|
||||
|
||||
private boolean canViewAllWorkflows(LoginUser loginUser)
|
||||
{
|
||||
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 String buildCurrentCreateBy(LoginUser loginUser)
|
||||
{
|
||||
SysUser user = loginUser.getUser();
|
||||
return user.getNickName() + "-" + loginUser.getUsername();
|
||||
}
|
||||
|
||||
private void maskModelRetailOutputBasicInfo(ModelRetailOutputFields modelRetailOutputFields)
|
||||
{
|
||||
modelRetailOutputFields.setCustName(
|
||||
|
||||
@@ -28,12 +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;
|
||||
}
|
||||
@@ -54,13 +55,14 @@ public class LoanPricingConverter {
|
||||
entity.setIdNum(dto.getIdNum());
|
||||
entity.setGuarType(dto.getGuarType());
|
||||
entity.setApplyAmt(dto.getApplyAmt());
|
||||
entity.setBusinessType(dto.getBusinessType());
|
||||
entity.setLoanRateHistory(dto.getLoanRateHistory());
|
||||
entity.setCouponRate(dto.getCouponRate());
|
||||
entity.setRepayMethod(dto.getRepayMethod());
|
||||
entity.setCollType(dto.getCollType());
|
||||
entity.setCollThirdParty(dto.getCollThirdParty());
|
||||
// 映射企业特有字段
|
||||
entity.setIsAgriGuar(dto.getIsAgriGuar());
|
||||
entity.setIsGreenLoan(dto.getIsGreenLoan());
|
||||
entity.setIsTechEnt(dto.getIsTechEnt());
|
||||
entity.setIsTradeConstruction(dto.getIsTradeConstruction());
|
||||
entity.setIsTradeBuildEnt(dto.getIsTradeBuildEnt());
|
||||
entity.setLoanTerm(dto.getLoanTerm());
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -3,66 +3,70 @@
|
||||
"cost": 2267,
|
||||
"tokenId": "17364055486305E7F4722M8IPFWNL8TOBEB",
|
||||
"mappingOutputFields": {
|
||||
"custIsn": "CUST20260121001",
|
||||
"custType": "企业客户",
|
||||
"guarType": "抵押担保",
|
||||
"custName": "北京智联科技有限公司",
|
||||
"idType": "营业执照",
|
||||
"idNum": "91110108MA00XXXXXX",
|
||||
"baseLoanRate": "3.45",
|
||||
"isFirstLoan": "N",
|
||||
"faithDay": "730",
|
||||
"bpFirstLoan": "0",
|
||||
"bpAgeLoan": "5.2",
|
||||
"totalBpLoyalty": "8.5",
|
||||
"balanceAvg": "5000000.00",
|
||||
"loanAvg": "3000000.00",
|
||||
"derivationRate": "1.8",
|
||||
"totalBpContribution": "12.3",
|
||||
"midEntConnect": "100000.00",
|
||||
"midEntEffect": "50000.00",
|
||||
"midEntInter": "80000.00",
|
||||
"midEntAccept": "200000.00",
|
||||
"midEntDiscount": "150000.00",
|
||||
"midEntEleDdc": "30000.00",
|
||||
"midEntWaterDdc": "10000.00",
|
||||
"midEntTax": "40000.00",
|
||||
"bpMid": "6.8",
|
||||
"payroll": "200",
|
||||
"invLoanAmount": "2500000.00",
|
||||
"bpPayroll": "4.1",
|
||||
"isCleanEnt": "Y",
|
||||
"hasSettleAcct": "Y",
|
||||
"isAgriGuar": "N",
|
||||
"isGreenLoan": "Y",
|
||||
"isTechEnt": "Y",
|
||||
"bpEntType": "7.5",
|
||||
"totoalBpRelevance": "9.2",
|
||||
"loanTerm": "36",
|
||||
"bpLoanTerm": "3.3",
|
||||
"applyAmt": "5000000.00",
|
||||
"bpLoanAmount": "5.8",
|
||||
"collType": "房产抵押",
|
||||
"collThirdParty": "N",
|
||||
"bpCollateral": "4.5",
|
||||
"greyCust": "N",
|
||||
"prinOverdue": "N",
|
||||
"interestOverdue": "N",
|
||||
"cardOverdue": "N",
|
||||
"bpGreyOverdue": "0",
|
||||
"totoalBpRisk": "1.2",
|
||||
"totalBp": "48.2",
|
||||
"calculateRate": "3.932"
|
||||
"custIsn": "CUST20260121001",
|
||||
"custType": "企业",
|
||||
"guarType": "抵押",
|
||||
"custName": "北京智联科技有限公司",
|
||||
"idType": "统一社会信用代码",
|
||||
"idNum": "91110108MA00XXXXXX",
|
||||
"repayMethod": "分期",
|
||||
"loanTerm": "3",
|
||||
"isFirstLoan": "0",
|
||||
"faithDay": "730",
|
||||
"bpFirstLoan": "0",
|
||||
"bpAgeLoan": "5.2",
|
||||
"totalBpLoyalty": "8.5",
|
||||
"balanceAvg": "5000000.00",
|
||||
"loanAvg": "3000000.00",
|
||||
"derivationRate": "1.8",
|
||||
"totalBpContribution": "12.3",
|
||||
"midEntConnect": "1",
|
||||
"midEntEffect": "1",
|
||||
"midEntInter": "1",
|
||||
"midEntAccept": "1",
|
||||
"midEntDiscount": "1",
|
||||
"midEntEleDdc": "1",
|
||||
"midEntWaterDdc": "1",
|
||||
"bpMid": "6.8",
|
||||
"payroll": "200",
|
||||
"invLoanAmount": "2500000.00",
|
||||
"bpPayroll": "4.1",
|
||||
"isCleanEnt": "1",
|
||||
"hasSettleAcct": "1",
|
||||
"isTradeBuildEnt": "0",
|
||||
"isGreenLoan": "1",
|
||||
"isTechEnt": "1",
|
||||
"bpEntType": "7.5",
|
||||
"totoalBpRelevance": "9.2",
|
||||
"bpLoanTerm": "3.3",
|
||||
"applyAmt": "5000000.00",
|
||||
"bpLoanAmount": "5.8",
|
||||
"collType": "一类",
|
||||
"collThirdParty": "0",
|
||||
"bpCollateral": "4.5",
|
||||
"greyCust": "0",
|
||||
"prinOverdue": "0",
|
||||
"interestOverdue": "0",
|
||||
"bpGreyOverdue": "0",
|
||||
"totoalBpRisk": "1.2",
|
||||
"totalBp": "48.2",
|
||||
"baseLoanRate": "3.45",
|
||||
"calculateRate": "3.932",
|
||||
"loanRateHistory": "4.20",
|
||||
"minRateProduct": "3.10",
|
||||
"smoothRange": "-0.20",
|
||||
"finalCalculateRate": "3.732",
|
||||
"referenceRate": "3.432"
|
||||
},
|
||||
"extensionMap": {},
|
||||
"reasonMessage": "Running successfully",
|
||||
"bizTime": 1736405548630,
|
||||
"outputFields": {},
|
||||
"workflowCode": "TBKH",
|
||||
"orgCode": "802000",
|
||||
"orgCode": "892000",
|
||||
"bizId": "2025010914345",
|
||||
"reasonCode": 200,
|
||||
"workflowVersion": 14,
|
||||
"callTime": 1736405548630,
|
||||
"status": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"idType": "身份证",
|
||||
"idNum": "330106199001011234",
|
||||
"baseLoanRate": "4.35",
|
||||
"greyBlackCust": "1",
|
||||
"isFirstLoan": "是",
|
||||
"faithDay": "365",
|
||||
"custAge": "36",
|
||||
|
||||
@@ -23,8 +23,11 @@
|
||||
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.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}, '%')
|
||||
|
||||
@@ -22,5 +22,6 @@ class ModelRetailOutputFieldsTest
|
||||
assertTrue(fieldNames.contains("smoothRange"), "缺少字段 smoothRange");
|
||||
assertTrue(fieldNames.contains("finalCalculateRate"), "缺少字段 finalCalculateRate");
|
||||
assertTrue(fieldNames.contains("referenceRate"), "缺少字段 referenceRate");
|
||||
assertTrue(fieldNames.contains("greyBlackCust"), "缺少字段 greyBlackCust");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +18,7 @@ class LoanPricingWorkflowMapperXmlTest
|
||||
|
||||
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("lpw.create_by = #{query.dataScopeCreateBy}"));
|
||||
assertTrue(xml.contains("SUBSTRING_INDEX(lpw.create_by, '-', -1) LIKE CONCAT('%', #{query.createBy}, '%')"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
|
||||
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
|
||||
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
|
||||
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
|
||||
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
|
||||
@@ -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,24 +92,22 @@ 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("一类");
|
||||
|
||||
JSONObject response = new JSONObject();
|
||||
response.put("calculateRate", "6.15");
|
||||
ModelRetailOutputFields response = new ModelRetailOutputFields();
|
||||
response.setCalculateRate("6.15");
|
||||
|
||||
when(loanPricingWorkflowMapper.selectById(1L)).thenReturn(workflow);
|
||||
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
|
||||
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
|
||||
when(modelService.invokeModel(any())).thenReturn(response);
|
||||
when(modelService.invokePersonalModel(any())).thenReturn(response);
|
||||
|
||||
loanPricingModelService.invokeModelAsync(1L);
|
||||
|
||||
verify(modelService).invokeModel(argThat((ModelInvokeDTO dto) ->
|
||||
verify(modelService).invokePersonalModel(argThat((ModelInvokeDTO dto) ->
|
||||
Objects.equals("202604090001", dto.getSerialNum())
|
||||
&& Objects.equals("892000", dto.getOrgCode())
|
||||
&& Objects.equals("1", dto.getRunType())
|
||||
@@ -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())));
|
||||
|
||||
@@ -1,90 +1,223 @@
|
||||
package com.ruoyi.loanpricing.service;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.loanpricing.domain.dto.ModelInvokeDTO;
|
||||
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
|
||||
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
|
||||
import com.ruoyi.loanpricing.mapper.LoanPricingWorkflowMapper;
|
||||
import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
|
||||
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LoanPricingModelServiceTest
|
||||
{
|
||||
@Mock
|
||||
private ModelService modelService;
|
||||
|
||||
@Mock
|
||||
private LoanPricingWorkflowMapper loanPricingWorkflowMapper;
|
||||
|
||||
@Mock
|
||||
private ModelRetailOutputFieldsMapper modelRetailOutputFieldsMapper;
|
||||
|
||||
@Mock
|
||||
private ModelCorpOutputFieldsMapper modelCorpOutputFieldsMapper;
|
||||
|
||||
@Mock
|
||||
private SensitiveFieldCryptoService sensitiveFieldCryptoService;
|
||||
|
||||
@InjectMocks
|
||||
private LoanPricingModelService loanPricingModelService;
|
||||
|
||||
@Test
|
||||
void shouldDecryptCustNameAndIdNumBeforeInvokeModel()
|
||||
void shouldDecryptCustNameAndIdNumBeforeInvokePersonalModel() throws Exception
|
||||
{
|
||||
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
|
||||
workflow.setId(1L);
|
||||
workflow.setCustType("个人");
|
||||
workflow.setCustName("cipher-name");
|
||||
workflow.setIdNum("cipher-id");
|
||||
TestContext context = createContext(personalWorkflow(1L));
|
||||
|
||||
JSONObject response = new JSONObject();
|
||||
response.put("calculateRate", "6.15");
|
||||
context.service.invokeModelAsync(1L);
|
||||
|
||||
when(loanPricingWorkflowMapper.selectById(1L)).thenReturn(workflow);
|
||||
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
|
||||
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
|
||||
when(modelService.invokeModel(any())).thenReturn(response);
|
||||
|
||||
loanPricingModelService.invokeModelAsync(1L);
|
||||
|
||||
verify(modelService).invokeModel(argThat((ModelInvokeDTO dto) ->
|
||||
Objects.equals("张三", dto.getCustName())
|
||||
&& Objects.equals("110101199001011234", dto.getIdNum())));
|
||||
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());
|
||||
assertEquals(0, context.corpInsertCount.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotWritePlainCustNameAndIdNumBackWhenUpdatingWorkflow()
|
||||
void shouldNotWritePlainCustNameAndIdNumBackWhenUpdatingWorkflow() throws Exception
|
||||
{
|
||||
TestContext context = createContext(personalWorkflow(2L));
|
||||
|
||||
context.service.invokeModelAsync(2L);
|
||||
|
||||
LoanPricingWorkflow updatedWorkflow = context.updatedWorkflow.get();
|
||||
assertNotEquals("张三", updatedWorkflow.getCustName());
|
||||
assertNotEquals("110101199001011234", updatedWorkflow.getIdNum());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNormalizeCorporateUploadParamsBeforeInvokeCorporateModel() throws Exception
|
||||
{
|
||||
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
|
||||
workflow.setId(2L);
|
||||
workflow.setId(3L);
|
||||
workflow.setCustType("企业");
|
||||
workflow.setCustName("cipher-name");
|
||||
workflow.setIdNum("cipher-id");
|
||||
workflow.setRepayMethod("分期");
|
||||
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.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());
|
||||
}
|
||||
|
||||
private static LoanPricingWorkflow personalWorkflow(Long id)
|
||||
{
|
||||
LoanPricingWorkflow workflow = new LoanPricingWorkflow();
|
||||
workflow.setId(id);
|
||||
workflow.setCustType("个人");
|
||||
workflow.setCustName("cipher-name");
|
||||
workflow.setIdNum("cipher-id");
|
||||
workflow.setCouponRate("2.15");
|
||||
return workflow;
|
||||
}
|
||||
|
||||
JSONObject response = new JSONObject();
|
||||
response.put("calculateRate", "6.15");
|
||||
private static TestContext createContext(LoanPricingWorkflow workflow) throws Exception
|
||||
{
|
||||
TestContext context = new TestContext();
|
||||
context.service = new LoanPricingModelService();
|
||||
context.modelService = new CapturingModelService();
|
||||
context.workflow = workflow;
|
||||
context.updatedWorkflow = new AtomicReference<>();
|
||||
context.retailInsertCount = new AtomicInteger();
|
||||
context.corpInsertCount = new AtomicInteger();
|
||||
|
||||
when(loanPricingWorkflowMapper.selectById(2L)).thenReturn(workflow);
|
||||
when(sensitiveFieldCryptoService.decrypt("cipher-name")).thenReturn("张三");
|
||||
when(sensitiveFieldCryptoService.decrypt("cipher-id")).thenReturn("110101199001011234");
|
||||
when(modelService.invokeModel(any())).thenReturn(response);
|
||||
setField(context.service, "modelService", context.modelService);
|
||||
setField(context.service, "loanPricingWorkflowMapper",
|
||||
workflowMapper(context.workflow, context.updatedWorkflow));
|
||||
setField(context.service, "modelRetailOutputFieldsMapper",
|
||||
insertCountingMapper(ModelRetailOutputFieldsMapper.class, context.retailInsertCount));
|
||||
setField(context.service, "modelCorpOutputFieldsMapper",
|
||||
insertCountingMapper(ModelCorpOutputFieldsMapper.class, context.corpInsertCount));
|
||||
setField(context.service, "sensitiveFieldCryptoService", new TestSensitiveFieldCryptoService());
|
||||
return context;
|
||||
}
|
||||
|
||||
loanPricingModelService.invokeModelAsync(2L);
|
||||
private static LoanPricingWorkflowMapper workflowMapper(
|
||||
LoanPricingWorkflow workflow, AtomicReference<LoanPricingWorkflow> updatedWorkflow)
|
||||
{
|
||||
return proxy(LoanPricingWorkflowMapper.class, (proxy, method, args) -> {
|
||||
if ("selectById".equals(method.getName()))
|
||||
{
|
||||
return workflow;
|
||||
}
|
||||
if ("updateById".equals(method.getName()))
|
||||
{
|
||||
updatedWorkflow.set((LoanPricingWorkflow) args[0]);
|
||||
return 1;
|
||||
}
|
||||
return defaultValue(method);
|
||||
});
|
||||
}
|
||||
|
||||
verify(loanPricingWorkflowMapper).updateById(argThat((LoanPricingWorkflow entity) ->
|
||||
!Objects.equals("张三", entity.getCustName())
|
||||
&& !Objects.equals("110101199001011234", entity.getIdNum())));
|
||||
private static <T> T insertCountingMapper(Class<T> mapperClass, AtomicInteger insertCount)
|
||||
{
|
||||
return proxy(mapperClass, (proxy, method, args) -> {
|
||||
if ("insert".equals(method.getName()))
|
||||
{
|
||||
insertCount.incrementAndGet();
|
||||
return 1;
|
||||
}
|
||||
return defaultValue(method);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T proxy(Class<T> mapperClass, InvocationHandler invocationHandler)
|
||||
{
|
||||
return (T) Proxy.newProxyInstance(
|
||||
mapperClass.getClassLoader(), new Class<?>[] { mapperClass }, invocationHandler);
|
||||
}
|
||||
|
||||
private static Object defaultValue(Method method)
|
||||
{
|
||||
if (method.getReturnType().equals(boolean.class))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (method.getReturnType().isPrimitive())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void setField(Object target, String fieldName, Object value) throws Exception
|
||||
{
|
||||
Field field = LoanPricingModelService.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
private static class TestContext
|
||||
{
|
||||
private LoanPricingModelService service;
|
||||
private CapturingModelService modelService;
|
||||
private LoanPricingWorkflow workflow;
|
||||
private AtomicReference<LoanPricingWorkflow> updatedWorkflow;
|
||||
private AtomicInteger retailInsertCount;
|
||||
private AtomicInteger corpInsertCount;
|
||||
}
|
||||
|
||||
private static class CapturingModelService extends ModelService
|
||||
{
|
||||
private final AtomicInteger personalCalls = new AtomicInteger();
|
||||
private final AtomicInteger corporateCalls = new AtomicInteger();
|
||||
private ModelInvokeDTO personalRequest;
|
||||
private ModelInvokeDTO corporateRequest;
|
||||
|
||||
@Override
|
||||
public ModelRetailOutputFields invokePersonalModel(ModelInvokeDTO modelInvokeDTO)
|
||||
{
|
||||
personalCalls.incrementAndGet();
|
||||
personalRequest = modelInvokeDTO;
|
||||
return new ModelRetailOutputFields();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelCorpOutputFields invokeCorporateModel(ModelInvokeDTO modelInvokeDTO)
|
||||
{
|
||||
corporateCalls.incrementAndGet();
|
||||
corporateRequest = modelInvokeDTO;
|
||||
return new ModelCorpOutputFields();
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestSensitiveFieldCryptoService extends SensitiveFieldCryptoService
|
||||
{
|
||||
private TestSensitiveFieldCryptoService()
|
||||
{
|
||||
super("1234567890abcdef");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String cipherText)
|
||||
{
|
||||
if ("cipher-name".equals(cipherText))
|
||||
{
|
||||
return "张三";
|
||||
}
|
||||
if ("cipher-id".equals(cipherText))
|
||||
{
|
||||
return "110101199001011234";
|
||||
}
|
||||
return cipherText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -12,6 +14,13 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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;
|
||||
@@ -24,12 +33,16 @@ 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;
|
||||
@@ -58,6 +71,18 @@ class LoanPricingWorkflowServiceImplTest
|
||||
@InjectMocks
|
||||
private LoanPricingWorkflowServiceImpl loanPricingWorkflowService;
|
||||
|
||||
@BeforeEach
|
||||
void setUpLoginUser()
|
||||
{
|
||||
setLoginUser(1L, "admin", "若依", role(1L, "超级管理员", "admin"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void clearLoginUser()
|
||||
{
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEncryptCustNameAndIdNumBeforeInsert()
|
||||
{
|
||||
@@ -65,6 +90,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");
|
||||
@@ -111,6 +137,64 @@ class LoanPricingWorkflowServiceImplTest
|
||||
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());
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCustIsnInsteadOfCustNameAsQueryCondition()
|
||||
{
|
||||
@@ -133,6 +217,134 @@ 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");
|
||||
|
||||
loanPricingWorkflowService.createCorporateLoanPricing(dto);
|
||||
|
||||
verify(loanPricingWorkflowMapper).insert(argThat((LoanPricingWorkflow entity) ->
|
||||
Objects.equals("企业", entity.getCustType())
|
||||
&& Objects.equals("新增", entity.getBusinessType())
|
||||
&& Objects.equals("2.35", entity.getCouponRate())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseRetailModelOutputFinalCalculateRateForWorkflowDetail()
|
||||
{
|
||||
@@ -216,14 +428,16 @@ class LoanPricingWorkflowServiceImplTest
|
||||
|
||||
ModelCorpOutputFields corpOutputFields = new ModelCorpOutputFields();
|
||||
corpOutputFields.setCalculateRate("3.932");
|
||||
corpOutputFields.setFinalCalculateRate("3.652");
|
||||
|
||||
when(loanPricingWorkflowMapper.selectOne(any())).thenReturn(workflow);
|
||||
when(modelCorpOutputFieldsMapper.selectById(22L)).thenReturn(corpOutputFields);
|
||||
|
||||
LoanPricingWorkflowVO result = loanPricingWorkflowService.selectLoanPricingBySerialNum("C20260328001");
|
||||
|
||||
assertEquals("3.932", result.getLoanPricingWorkflow().getLoanRate());
|
||||
assertEquals("3.652", result.getLoanPricingWorkflow().getLoanRate());
|
||||
assertEquals("3.932", result.getModelCorpOutputFields().getCalculateRate());
|
||||
assertEquals("3.652", result.getModelCorpOutputFields().getFinalCalculateRate());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -253,4 +467,44 @@ class LoanPricingWorkflowServiceImplTest
|
||||
assertEquals("测试****公司", result.getModelCorpOutputFields().getCustName());
|
||||
assertEquals("91*************00X", 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 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)
|
||||
{
|
||||
SysUser user = new SysUser();
|
||||
user.setUserId(userId);
|
||||
user.setUserName(username);
|
||||
user.setNickName(nickName);
|
||||
user.setRoles(java.util.Arrays.asList(roles));
|
||||
LoginUser loginUser = new LoginUser(userId, 100L, 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;
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -11,8 +11,12 @@
|
||||
"preview": "node build/index.js --preview",
|
||||
"test:password-transfer": "node tests/password-transfer-api.test.js",
|
||||
"test:retail-display-fields": "node tests/retail-display-fields.test.js",
|
||||
"test:model-output-flat-display": "node tests/model-output-flat-display.test.js",
|
||||
"test:personal-create-input-params": "node tests/personal-create-input-params.test.js",
|
||||
"test:id-number-validation-removal": "node tests/id-number-validation-removal.test.js"
|
||||
"test:id-number-validation-removal": "node tests/id-number-validation-removal.test.js",
|
||||
"test:corporate-create-input-params": "node tests/corporate-create-input-params.test.js",
|
||||
"test:corporate-display-fields": "node tests/corporate-display-fields.test.js",
|
||||
"test:business-type-history-rate": "node tests/business-type-history-rate.test.js"
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
|
||||
@@ -35,6 +35,33 @@ export function createCorporateWorkflow(data) {
|
||||
})
|
||||
}
|
||||
|
||||
// 查询个人客户号映射
|
||||
export function queryPersonalCustomerMap(custId) {
|
||||
return request({
|
||||
url: '/loanPricing/workflow/customer-map/personal',
|
||||
method: 'get',
|
||||
params: { custId: custId }
|
||||
})
|
||||
}
|
||||
|
||||
// 查询企业客户号映射
|
||||
export function queryCorporateCustomerMap(custId) {
|
||||
return request({
|
||||
url: '/loanPricing/workflow/customer-map/corporate',
|
||||
method: 'get',
|
||||
params: { custId: custId }
|
||||
})
|
||||
}
|
||||
|
||||
// 查询历史贷款合同
|
||||
export function queryHistoryContracts(custIsn) {
|
||||
return request({
|
||||
url: '/loanPricing/workflow/history-contract',
|
||||
method: 'get',
|
||||
params: { custIsn: custIsn }
|
||||
})
|
||||
}
|
||||
|
||||
// 设定执行利率
|
||||
export function setExecuteRate(serialNum, executeRate) {
|
||||
return request({
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 509 KiB After Width: | Height: | Size: 89 KiB |
@@ -72,10 +72,11 @@ export default {
|
||||
},
|
||||
isDashboard(route) {
|
||||
const name = route && route.name
|
||||
if (!name) {
|
||||
const path = route && route.path
|
||||
if (!name && !path) {
|
||||
return false
|
||||
}
|
||||
return name.trim() === 'Index'
|
||||
return (name && name.trim() === 'Index') || path === '/index'
|
||||
},
|
||||
handleLink(item) {
|
||||
const { redirect, path } = item
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户内码" prop="custIsn">
|
||||
<el-input v-model="form.custIsn" placeholder="请输入客户内码"/>
|
||||
<el-input v-model="form.custIsn" placeholder="请选择客户内码" :readonly="true"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户名称" prop="custName">
|
||||
<el-input v-model="form.custName" placeholder="请输入客户名称"/>
|
||||
<el-input v-model="form.custName" placeholder="请选择客户名称" :readonly="true"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -53,8 +53,26 @@
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="贷款期限(月)" prop="loanTerm">
|
||||
<el-input v-model.number="form.loanTerm" type="number" placeholder="请输入贷款期限"/>
|
||||
<el-form-item label="贷款期限(年)" prop="loanTerm">
|
||||
<el-select v-model="form.loanTerm" placeholder="请选择贷款期限" style="width: 100%">
|
||||
<el-option v-for="item in loanTermOptions" :key="item" :label="item" :value="item"/>
|
||||
</el-select>
|
||||
</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-select>
|
||||
</el-form-item>
|
||||
</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-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -62,49 +80,46 @@
|
||||
<!-- 企业标识 -->
|
||||
<el-divider content-position="left">企业标识</el-divider>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="省农担担保贷款" prop="isAgriGuar">
|
||||
<el-switch v-model="form.isAgriGuar"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="绿色贷款" prop="isGreenLoan">
|
||||
<el-switch v-model="form.isGreenLoan"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="科技型企业" prop="isTechEnt">
|
||||
<el-switch v-model="form.isTechEnt"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="贸易和建筑业企业" prop="isTradeConstruction">
|
||||
<el-switch v-model="form.isTradeConstruction"/>
|
||||
<el-form-item label="贸易和建筑业企业" prop="isTradeBuildEnt">
|
||||
<el-switch v-model="form.isTradeBuildEnt"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 抵质押信息 -->
|
||||
<el-divider content-position="left">抵质押信息</el-divider>
|
||||
<el-row>
|
||||
<el-divider v-if="isCollateralGuarantee" content-position="left">抵质押信息</el-divider>
|
||||
<el-row v-if="isCollateralGuarantee">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="抵质押类型" prop="collType">
|
||||
<el-select v-model="form.collType" placeholder="请选择抵质押类型" style="width: 100%">
|
||||
<el-option label="一线" value="一线"/>
|
||||
<el-option label="一类" value="一类"/>
|
||||
<el-option label="二类" value="二类"/>
|
||||
<el-option v-for="item in collateralTypeOptions" :key="item" :label="item" :value="item"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="抵质押物三方所有" prop="collThirdParty">
|
||||
<el-form-item label="抵质押物是否第三方所有" prop="collThirdParty">
|
||||
<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
|
||||
:visible.sync="showHistorySelector"
|
||||
:contracts="historyContracts"
|
||||
:loading="historyLoading"
|
||||
@select="handleHistoryContractSelect"
|
||||
/>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
@@ -113,14 +128,22 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {createCorporateWorkflow} from "@/api/loanPricing/workflow"
|
||||
import {createCorporateWorkflow, queryHistoryContracts} from "@/api/loanPricing/workflow"
|
||||
import HistoryContractSelector from "./HistoryContractSelector"
|
||||
|
||||
export default {
|
||||
name: "CorporateCreateDialog",
|
||||
components: {
|
||||
HistoryContractSelector
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
customerMap: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -140,6 +163,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) {
|
||||
@@ -148,8 +179,6 @@ export default {
|
||||
const num = parseInt(value)
|
||||
if (isNaN(num) || num <= 0) {
|
||||
callback(new Error('请输入有效的贷款期限'))
|
||||
} else if (num > 360) {
|
||||
callback(new Error('贷款期限不能超过 360 个月'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@@ -157,23 +186,31 @@ export default {
|
||||
}
|
||||
|
||||
return {
|
||||
loanTermOptions: [
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '10'
|
||||
],
|
||||
submitting: false,
|
||||
showHistorySelector: false,
|
||||
historyLoading: false,
|
||||
historyContracts: [],
|
||||
selectedHistoryContract: null,
|
||||
form: {
|
||||
orgCode: '892000',
|
||||
runType: '1',
|
||||
custIsn: undefined,
|
||||
custName: undefined,
|
||||
custIsn: this.customerMap ? this.customerMap.cust_isn : undefined,
|
||||
custName: this.customerMap ? this.customerMap.cust_name : undefined,
|
||||
idType: undefined,
|
||||
idNum: undefined,
|
||||
idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined,
|
||||
guarType: undefined,
|
||||
applyAmt: undefined,
|
||||
loanTerm: undefined,
|
||||
isAgriGuar: false,
|
||||
businessType: undefined,
|
||||
loanRateHistory: undefined,
|
||||
isGreenLoan: false,
|
||||
isTechEnt: false,
|
||||
isTradeConstruction: false,
|
||||
isTradeBuildEnt: false,
|
||||
collType: undefined,
|
||||
collThirdParty: false
|
||||
collThirdParty: false,
|
||||
couponRate: undefined
|
||||
},
|
||||
rules: {
|
||||
custIsn: [
|
||||
@@ -197,10 +234,19 @@ export default {
|
||||
{required: true, validator: validateApplyAmt, trigger: "blur"}
|
||||
],
|
||||
loanTerm: [
|
||||
{required: true, validator: validateLoanTerm, trigger: "blur"}
|
||||
{required: true, validator: validateLoanTerm, trigger: "change"}
|
||||
],
|
||||
businessType: [
|
||||
{required: true, message: "请选择业务种类", trigger: "change"}
|
||||
],
|
||||
loanRateHistory: [
|
||||
{required: true, message: "请选择历史贷款合同", trigger: "change"}
|
||||
],
|
||||
collType: [
|
||||
{required: true, message: "请选择抵质押类型", trigger: "change"}
|
||||
],
|
||||
couponRate: [
|
||||
{validator: validateCouponRate, trigger: "blur"}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -213,6 +259,24 @@ export default {
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
},
|
||||
isCollateralGuarantee() {
|
||||
return this.form.guarType === '抵押' || this.form.guarType === '质押'
|
||||
},
|
||||
isStockTransfer() {
|
||||
return this.form.businessType === '存量转贷'
|
||||
},
|
||||
isCertificatePledge() {
|
||||
return this.form.guarType === '质押' && this.form.collType === '存单质押'
|
||||
},
|
||||
collateralTypeOptions() {
|
||||
if (this.form.guarType === '抵押') {
|
||||
return ['一类', '二类', '三类', '四类', '排污权抵押', '设备等其他不动产抵押']
|
||||
}
|
||||
if (this.form.guarType === '质押') {
|
||||
return ['存单质押', '股权质押', '其他质押']
|
||||
}
|
||||
return []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -220,6 +284,14 @@ export default {
|
||||
if (val) {
|
||||
this.reset()
|
||||
}
|
||||
},
|
||||
'form.guarType'(val, oldVal) {
|
||||
if (val !== oldVal) {
|
||||
this.resetCollateralFields()
|
||||
}
|
||||
},
|
||||
'form.collType'() {
|
||||
this.resetCouponRateIfNotCertificatePledge()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -228,22 +300,31 @@ export default {
|
||||
this.form = {
|
||||
orgCode: '892000',
|
||||
runType: '1',
|
||||
custIsn: undefined,
|
||||
custName: undefined,
|
||||
custIsn: this.customerMap ? this.customerMap.cust_isn : undefined,
|
||||
custName: this.customerMap ? this.customerMap.cust_name : undefined,
|
||||
idType: undefined,
|
||||
idNum: undefined,
|
||||
idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined,
|
||||
guarType: undefined,
|
||||
applyAmt: undefined,
|
||||
loanTerm: undefined,
|
||||
isAgriGuar: false,
|
||||
businessType: undefined,
|
||||
loanRateHistory: undefined,
|
||||
isGreenLoan: false,
|
||||
isTechEnt: false,
|
||||
isTradeConstruction: false,
|
||||
isTradeBuildEnt: false,
|
||||
collType: undefined,
|
||||
collThirdParty: false
|
||||
collThirdParty: false,
|
||||
couponRate: undefined
|
||||
}
|
||||
this.submitting = false
|
||||
this.resetForm("form")
|
||||
this.showHistorySelector = false
|
||||
this.historyLoading = false
|
||||
this.historyContracts = []
|
||||
this.selectedHistoryContract = null
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.clearValidate()
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 对话框关闭处理 */
|
||||
handleClose() {
|
||||
@@ -254,19 +335,98 @@ export default {
|
||||
this.dialogVisible = false
|
||||
this.reset()
|
||||
},
|
||||
/** 清空抵质押字段 */
|
||||
resetCollateralFields() {
|
||||
this.form.collType = undefined
|
||||
this.form.collThirdParty = false
|
||||
this.resetCouponRateIfNotCertificatePledge()
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
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 === '存量转贷') {
|
||||
this.queryHistoryContracts()
|
||||
}
|
||||
},
|
||||
queryHistoryContracts() {
|
||||
if (!this.form.custIsn) {
|
||||
this.$modal.msgWarning("客户内码不能为空")
|
||||
return
|
||||
}
|
||||
this.historyLoading = true
|
||||
queryHistoryContracts(this.form.custIsn).then(response => {
|
||||
this.historyContracts = response.data || []
|
||||
if (this.historyContracts.length === 0) {
|
||||
this.$modal.msgWarning("未查询到历史贷款合同")
|
||||
return
|
||||
}
|
||||
this.showHistorySelector = true
|
||||
}).finally(() => {
|
||||
this.historyLoading = false
|
||||
})
|
||||
},
|
||||
handleHistoryContractSelect(row) {
|
||||
if (!row.loan_rate_history) {
|
||||
this.$modal.msgWarning("历史贷款利率不能为空")
|
||||
return
|
||||
}
|
||||
this.selectedHistoryContract = row
|
||||
this.form.loanRateHistory = row.loan_rate_history
|
||||
},
|
||||
clearHistoryContract() {
|
||||
this.selectedHistoryContract = null
|
||||
this.form.loanRateHistory = undefined
|
||||
this.historyContracts = []
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.clearValidate(['loanRateHistory'])
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.isStockTransfer && !this.form.loanRateHistory) {
|
||||
this.$modal.msgWarning("请选择历史贷款合同")
|
||||
return
|
||||
}
|
||||
if (this.isCertificatePledge && !this.form.couponRate) {
|
||||
this.$modal.msgWarning("存单票面利率不能为空")
|
||||
return
|
||||
}
|
||||
this.submitting = true
|
||||
// 转换开关值为字符串
|
||||
const data = {
|
||||
...this.form,
|
||||
isAgriGuar: this.form.isAgriGuar ? 'true' : 'false',
|
||||
isGreenLoan: this.form.isGreenLoan ? 'true' : 'false',
|
||||
isTechEnt: this.form.isTechEnt ? 'true' : 'false',
|
||||
isTradeConstruction: this.form.isTradeConstruction ? 'true' : 'false',
|
||||
collThirdParty: this.form.collThirdParty ? 'true' : 'false'
|
||||
isGreenLoan: this.form.isGreenLoan ? '1' : '0',
|
||||
isTradeBuildEnt: this.form.isTradeBuildEnt ? '1' : '0'
|
||||
}
|
||||
if (this.isCollateralGuarantee) {
|
||||
data.collThirdParty = this.form.collThirdParty ? '1' : '0'
|
||||
} else {
|
||||
delete data.collType
|
||||
delete data.collThirdParty
|
||||
}
|
||||
if (!this.isStockTransfer) {
|
||||
delete data.loanRateHistory
|
||||
}
|
||||
if (!this.isCertificatePledge) {
|
||||
delete data.couponRate
|
||||
}
|
||||
|
||||
createCorporateWorkflow(data).then(response => {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<el-descriptions-item label="浮动BP">
|
||||
<span class="total-bp-value">{{ getTotalBp() }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="测算利率">
|
||||
<el-descriptions-item label="最终测算利率">
|
||||
<span class="calculate-rate">{{ getCalculateRate() }}</span> %
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行利率">
|
||||
@@ -80,12 +80,10 @@
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="担保方式">{{ detailData.guarType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="申请金额">{{ detailData.applyAmt }} 元</el-descriptions-item>
|
||||
<el-descriptions-item label="省农担担保贷款">{{
|
||||
formatBoolean(detailData.isAgriGuar)
|
||||
}}
|
||||
</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="绿色贷款">{{ formatBoolean(detailData.isGreenLoan) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="科技型企业">{{ formatBoolean(detailData.isTechEnt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="贸易和建筑业企业">{{ formatBoolean(detailData.isTradeBuildEnt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="抵质押类型">{{ detailData.collType || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="抵质押物是否三方所有">{{
|
||||
formatBoolean(detailData.collThirdParty)
|
||||
@@ -162,9 +160,9 @@ export default {
|
||||
getTotalBp() {
|
||||
return this.corpOutput?.totalBp || '-'
|
||||
},
|
||||
/** 获取测算利率 */
|
||||
/** 获取最终测算利率 */
|
||||
getCalculateRate() {
|
||||
return this.corpOutput?.calculateRate || '-'
|
||||
return this.corpOutput?.finalCalculateRate || '-'
|
||||
},
|
||||
/** 设定执行利率 */
|
||||
handleSetExecuteRate() {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<el-dialog title="客户号查询" :visible.sync="dialogVisible" width="80%" append-to-body
|
||||
:close-on-click-modal="false" @close="handleClose">
|
||||
<el-form :model="queryForm" inline size="small">
|
||||
<el-form-item label="客户号">
|
||||
<el-input v-model="queryForm.custId" placeholder="请输入客户号" clearable @keyup.enter.native="handleQuery"/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" :loading="loading" @click="handleQuery">查询</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="customerList">
|
||||
<el-table-column label="客户号" prop="cust_id" align="center" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="客户内码" prop="cust_isn" align="center" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="客户名称" prop="cust_name" align="center" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="用信天数" prop="faith_day" align="center"/>
|
||||
<el-table-column label="存款年日均" prop="balance_avg" align="center"/>
|
||||
<el-table-column label="历史贷款次数" prop="loan_count_his" align="center"/>
|
||||
<el-table-column label="上次贷款日期" prop="last_loan_date" align="center" width="130"/>
|
||||
<el-table-column label="操作" align="center" width="90">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="handleSelect(scope.row)">选择</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {queryPersonalCustomerMap, queryCorporateCustomerMap} from "@/api/loanPricing/workflow"
|
||||
|
||||
export default {
|
||||
name: "CustomerMapSelector",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
customerType: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
queryForm: {
|
||||
custId: undefined
|
||||
},
|
||||
customerList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleQuery() {
|
||||
const custId = this.queryForm.custId ? this.queryForm.custId.trim() : ''
|
||||
this.queryForm.custId = custId
|
||||
if (!custId) {
|
||||
this.$modal.msgWarning("请输入客户号")
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
let request
|
||||
if (this.customerType === 'personal') {
|
||||
request = queryPersonalCustomerMap
|
||||
} else if (this.customerType === 'corporate') {
|
||||
request = queryCorporateCustomerMap
|
||||
} else {
|
||||
this.loading = false
|
||||
this.$modal.msgWarning("请选择客户类型")
|
||||
return
|
||||
}
|
||||
request(custId).then(response => {
|
||||
this.customerList = response.data || []
|
||||
if (this.customerList.length === 0) {
|
||||
this.$modal.msgWarning("未查询到客户信息")
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleSelect(row) {
|
||||
this.$emit('select', row)
|
||||
this.dialogVisible = false
|
||||
},
|
||||
handleClose() {
|
||||
this.queryForm.custId = undefined
|
||||
this.customerList = []
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<el-dialog title="历史贷款合同选择" :visible.sync="dialogVisible" width="80%" append-to-body
|
||||
:close-on-click-modal="false" @close="handleClose">
|
||||
<el-table :data="contracts" v-loading="loading" @row-click="handleRowClick">
|
||||
<el-table-column label="选择" align="center" width="70">
|
||||
<template slot-scope="scope">
|
||||
<el-radio
|
||||
class="history-contract-radio"
|
||||
v-model="selectedContractKey"
|
||||
:label="contractRadioValue(scope.row, scope.$index)"
|
||||
@change="handleRadioChange(scope.row)"
|
||||
>
|
||||
<span class="history-contract-radio-text">选择</span>
|
||||
</el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户内码" prop="cust_isn" align="center" :show-overflow-tooltip="true"/>
|
||||
<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_amount_history" align="center"/>
|
||||
<el-table-column label="历史贷款签订时间" prop="loan_sign_date_history" align="center" width="150"/>
|
||||
</el-table>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="confirmSelect">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "HistoryContractSelector",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
contracts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedContract: null,
|
||||
selectedContractKey: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dialogVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
contractRadioValue(row, index) {
|
||||
return row.loan_contract_history || `${row.cust_isn || ''}-${index}`
|
||||
},
|
||||
handleRowClick(row, column, event) {
|
||||
this.selectedContract = row
|
||||
this.selectedContractKey = this.contractRadioValue(row, this.contracts.indexOf(row))
|
||||
},
|
||||
handleRadioChange(row) {
|
||||
this.selectedContract = row
|
||||
},
|
||||
confirmSelect() {
|
||||
if (!this.selectedContract) {
|
||||
this.$modal.msgWarning("请选择历史贷款合同")
|
||||
return
|
||||
}
|
||||
if (!this.selectedContract.loan_rate_history) {
|
||||
this.$modal.msgWarning("历史贷款利率不能为空")
|
||||
return
|
||||
}
|
||||
this.$emit('select', this.selectedContract)
|
||||
this.dialogVisible = false
|
||||
},
|
||||
handleClose() {
|
||||
this.selectedContract = null
|
||||
this.selectedContractKey = null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.history-contract-radio ::v-deep .el-radio__label {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -3,22 +3,36 @@
|
||||
<div slot="header" class="card-header">
|
||||
<span class="card-title">模型输出</span>
|
||||
</div>
|
||||
<el-tabs v-model="activeTab">
|
||||
<div class="output-sections">
|
||||
<!-- 个人客户模型输出 -->
|
||||
<template v-if="custType === '个人' && retailOutput">
|
||||
<!-- 基本信息 -->
|
||||
<el-tab-pane label="基本信息" name="retail-basic">
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">基本信息</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="客户内码">{{ retailOutput.custIsn || '-' }}</el-descriptions-item>
|
||||
<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="灰黑名单客户">{{ formatOutputValue(retailOutput.greyBlackCust) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
|
||||
<!-- 忠诚度分析 -->
|
||||
<el-tab-pane label="忠诚度分析" name="retail-loyalty">
|
||||
<div class="output-section">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">忠诚度分析</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="我行首贷客户">{{ formatBoolean(retailOutput.isFirstLoan) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用信天数">{{ retailOutput.faithDay || '-' }}</el-descriptions-item>
|
||||
@@ -28,20 +42,20 @@
|
||||
<el-descriptions-item label="BP_年龄"><span class="bp-value">{{ retailOutput.bpAge || '-' }}</span></el-descriptions-item>
|
||||
<el-descriptions-item label="TOTAL_BP_忠诚度" :span="2"><span class="total-bp-value">{{ retailOutput.totalBpLoyalty || '-' }}</span></el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
|
||||
<!-- 贡献度分析 -->
|
||||
<el-tab-pane label="贡献度分析" name="retail-contribution">
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">贡献度分析</h4>
|
||||
<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="TOTAL_BP_贡献度"><span class="total-bp-value">{{ retailOutput.totalBpContribution || '-' }}</span></el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
|
||||
<!-- 关联度分析 -->
|
||||
<el-tab-pane label="关联度分析" name="retail-relevance">
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">关联度分析</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="中间业务_个人_信用卡">{{ formatBoolean(retailOutput.midPerCard) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="中间业务_个人_一码通">{{ formatBoolean(retailOutput.midPerPass) }}</el-descriptions-item>
|
||||
@@ -58,10 +72,10 @@
|
||||
<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>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
|
||||
<!-- 贷款特征 -->
|
||||
<el-tab-pane label="贷款特征" name="retail-loan">
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">贷款特征</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="申请金额">{{ retailOutput.applyAmt || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="BP_贷款额度"><span class="bp-value">{{ retailOutput.bpLoanAmount || '-' }}</span></el-descriptions-item>
|
||||
@@ -74,10 +88,10 @@
|
||||
<el-descriptions-item label="抵质押物三方所有">{{ formatBoolean(retailOutput.collThirdParty) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="BP_抵押物"><span class="bp-value">{{ retailOutput.bpCollateral || '-' }}</span></el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
|
||||
<!-- 风险度分析 -->
|
||||
<el-tab-pane label="风险度分析" name="retail-risk">
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">风险度分析</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="灰名单客户">{{ formatBoolean(retailOutput.greyCust) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="本金逾期">{{ formatBoolean(retailOutput.prinOverdue) }}</el-descriptions-item>
|
||||
@@ -86,26 +100,13 @@
|
||||
<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>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 测算结果 -->
|
||||
<el-tab-pane label="测算结果" name="retail-result">
|
||||
<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.finalCalculateRate || '-' }}</span> %</el-descriptions-item>
|
||||
<el-descriptions-item label="参考利率"><span class="calculate-rate">{{ retailOutput.referenceRate || '-' }}</span> %</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 企业客户模型输出 -->
|
||||
<template v-else-if="custType === '企业' && corpOutput">
|
||||
<!-- 基本信息 -->
|
||||
<el-tab-pane label="基本信息" name="corp-basic">
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">基本信息</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="客户内码">{{ corpOutput.custIsn || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户名称">{{ corpOutput.custName || '-' }}</el-descriptions-item>
|
||||
@@ -113,10 +114,23 @@
|
||||
<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>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
|
||||
<!-- 忠诚度分析 -->
|
||||
<el-tab-pane label="忠诚度分析" name="corp-loyalty">
|
||||
<div class="output-section">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">忠诚度分析</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="我行首贷客户">{{ formatBoolean(corpOutput.isFirstLoan) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用信天数">{{ corpOutput.faithDay || '-' }}</el-descriptions-item>
|
||||
@@ -124,20 +138,20 @@
|
||||
<el-descriptions-item label="BP_贷龄"><span class="bp-value">{{ corpOutput.bpAgeLoan || '-' }}</span></el-descriptions-item>
|
||||
<el-descriptions-item label="TOTAL_BP_忠诚度" :span="2"><span class="total-bp-value">{{ corpOutput.totalBpLoyalty || '-' }}</span></el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
|
||||
<!-- 贡献度分析 -->
|
||||
<el-tab-pane label="贡献度分析" name="corp-contribution">
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">贡献度分析</h4>
|
||||
<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="TOTAL_BP_贡献度"><span class="total-bp-value">{{ corpOutput.totalBpContribution || '-' }}</span></el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
|
||||
<!-- 关联度分析 -->
|
||||
<el-tab-pane label="关联度分析" name="corp-relevance">
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">关联度分析</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="中间业务_企业_企业互联">{{ formatBoolean(corpOutput.midEntConnect) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="中间业务_企业_有效价值客户">{{ formatBoolean(corpOutput.midEntEffect) }}</el-descriptions-item>
|
||||
@@ -146,31 +160,30 @@
|
||||
<el-descriptions-item label="中间业务_企业_贴现">{{ formatBoolean(corpOutput.midEntDiscount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="中间业务_企业_电费代扣">{{ formatBoolean(corpOutput.midEntEleDdc) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="中间业务_企业_水费代扣">{{ formatBoolean(corpOutput.midEntWaterDdc) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="中间业务_企业_税务代扣">{{ formatBoolean(corpOutput.midEntTax) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="BP_中间业务"><span class="bp-value">{{ corpOutput.bpMid || '-' }}</span></el-descriptions-item>
|
||||
<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>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
|
||||
<!-- 企业类别 -->
|
||||
<el-tab-pane label="企业类别" name="corp-category">
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">企业类别</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="净身企业">{{ formatBoolean(corpOutput.isCleanEnt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开立基本结算账户">{{ formatBoolean(corpOutput.hasSettleAcct) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="省农担担保贷款">{{ formatBoolean(corpOutput.isAgriGuar) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="绿色贷款">{{ formatBoolean(corpOutput.isGreenLoan) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="科技型企业">{{ formatBoolean(corpOutput.isTechEnt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="贸易和建筑业企业">{{ formatBoolean(corpOutput.isTradeBuildEnt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="BP_企业客户类别"><span class="bp-value">{{ corpOutput.bpEntType || '-' }}</span></el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
|
||||
<!-- 贷款特征 -->
|
||||
<el-tab-pane label="贷款特征" name="corp-loan">
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">贷款特征</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="贷款期限">{{ corpOutput.loanTerm || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="借款期限">{{ corpOutput.loanTerm || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="BP_贷款期限"><span class="bp-value">{{ corpOutput.bpLoanTerm || '-' }}</span></el-descriptions-item>
|
||||
<el-descriptions-item label="申请金额">{{ corpOutput.applyAmt || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="BP_贷款额度"><span class="bp-value">{{ corpOutput.bpLoanAmount || '-' }}</span></el-descriptions-item>
|
||||
@@ -178,29 +191,20 @@
|
||||
<el-descriptions-item label="抵质押物三方所有">{{ formatBoolean(corpOutput.collThirdParty) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="BP_抵押物"><span class="bp-value">{{ corpOutput.bpCollateral || '-' }}</span></el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
|
||||
<!-- 风险度分析 -->
|
||||
<el-tab-pane label="风险度分析" name="corp-risk">
|
||||
<div class="output-section">
|
||||
<h4 class="section-title">风险度分析</h4>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="灰名单客户">{{ formatBoolean(corpOutput.greyCust) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="本金逾期">{{ formatBoolean(corpOutput.prinOverdue) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="利息逾期">{{ formatBoolean(corpOutput.interestOverdue) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="信用卡逾期">{{ formatBoolean(corpOutput.cardOverdue) }}</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>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 测算结果 -->
|
||||
<el-tab-pane label="测算结果" name="corp-result">
|
||||
<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>
|
||||
</el-tab-pane>
|
||||
</div>
|
||||
</template>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
@@ -221,24 +225,6 @@ export default {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
custType: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
// 根据客户类型设置默认 tab
|
||||
if (val === '个人') {
|
||||
this.activeTab = 'retail-basic'
|
||||
} else if (val === '企业') {
|
||||
this.activeTab = 'corp-basic'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 格式化布尔值为中文 */
|
||||
formatBoolean(value) {
|
||||
@@ -251,6 +237,10 @@ export default {
|
||||
if (value === 'consumer') return '消费贷款'
|
||||
if (value === 'business') return '经营贷款'
|
||||
return value || '-'
|
||||
},
|
||||
formatOutputValue(value) {
|
||||
if (value === 0) return '0'
|
||||
return value || '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,8 +269,15 @@ export default {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__header {
|
||||
margin-bottom: 20px;
|
||||
.output-section + .output-section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
// BP 值样式
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户内码" prop="custIsn">
|
||||
<el-input v-model="form.custIsn" placeholder="请输入客户内码"/>
|
||||
<el-input v-model="form.custIsn" placeholder="请选择客户内码" :readonly="true"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户名称" prop="custName">
|
||||
<el-input v-model="form.custName" placeholder="请输入客户名称"/>
|
||||
<el-input v-model="form.custName" placeholder="请选择客户名称" :readonly="true"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -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%">
|
||||
@@ -70,10 +62,21 @@
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否有经营佐证" prop="bizProof">
|
||||
<el-switch v-model="form.bizProof"/>
|
||||
<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-select>
|
||||
</el-form-item>
|
||||
</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-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="循环功能" prop="loanLoop">
|
||||
<el-switch v-model="form.loanLoop"/>
|
||||
@@ -82,24 +85,33 @@
|
||||
</el-row>
|
||||
|
||||
<!-- 抵质押信息 -->
|
||||
<el-divider content-position="left">抵质押信息</el-divider>
|
||||
<el-row>
|
||||
<el-divider v-if="isCollateralGuarantee" content-position="left">抵质押信息</el-divider>
|
||||
<el-row v-if="isCollateralGuarantee">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="抵质押类型" prop="collType">
|
||||
<el-select v-model="form.collType" placeholder="请选择抵质押类型" style="width: 100%">
|
||||
<el-option label="一类" value="一类"/>
|
||||
<el-option label="二类" value="二类"/>
|
||||
<el-option label="三类" value="三类"/>
|
||||
<el-option v-for="item in collateralTypeOptions" :key="item" :label="item" :value="item"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="抵质押物三方所有" prop="collThirdParty">
|
||||
<el-form-item label="抵质押物是否第三方所有" prop="collThirdParty">
|
||||
<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
|
||||
:visible.sync="showHistorySelector"
|
||||
:contracts="historyContracts"
|
||||
:loading="historyLoading"
|
||||
@select="handleHistoryContractSelect"
|
||||
/>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
@@ -108,14 +120,22 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {createPersonalWorkflow} from "@/api/loanPricing/workflow"
|
||||
import {createPersonalWorkflow, queryHistoryContracts} from "@/api/loanPricing/workflow"
|
||||
import HistoryContractSelector from "./HistoryContractSelector"
|
||||
|
||||
export default {
|
||||
name: "PersonalCreateDialog",
|
||||
components: {
|
||||
HistoryContractSelector
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
customerMap: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -135,26 +155,39 @@ 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,
|
||||
showHistorySelector: false,
|
||||
historyLoading: false,
|
||||
historyContracts: [],
|
||||
selectedHistoryContract: null,
|
||||
form: {
|
||||
orgCode: '892000',
|
||||
runType: '1',
|
||||
custIsn: undefined,
|
||||
custName: undefined,
|
||||
custIsn: this.customerMap ? this.customerMap.cust_isn : undefined,
|
||||
custName: this.customerMap ? this.customerMap.cust_name : undefined,
|
||||
idType: undefined,
|
||||
idNum: undefined,
|
||||
idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined,
|
||||
guarType: undefined,
|
||||
applyAmt: undefined,
|
||||
loanPurpose: undefined,
|
||||
loanTerm: undefined,
|
||||
bizProof: false,
|
||||
businessType: undefined,
|
||||
loanRateHistory: undefined,
|
||||
loanLoop: false,
|
||||
collType: undefined,
|
||||
collThirdParty: false
|
||||
collThirdParty: false,
|
||||
couponRate: undefined
|
||||
},
|
||||
rules: {
|
||||
custIsn: [
|
||||
@@ -177,11 +210,20 @@ export default {
|
||||
applyAmt: [
|
||||
{required: true, validator: validateApplyAmt, trigger: "blur"}
|
||||
],
|
||||
loanPurpose: [
|
||||
{required: true, message: "请选择贷款用途", trigger: "change"}
|
||||
],
|
||||
loanTerm: [
|
||||
{required: true, message: "请选择借款期限", trigger: "change"}
|
||||
],
|
||||
businessType: [
|
||||
{required: true, message: "请选择业务种类", trigger: "change"}
|
||||
],
|
||||
loanRateHistory: [
|
||||
{required: true, message: "请选择历史贷款合同", trigger: "change"}
|
||||
],
|
||||
collType: [
|
||||
{required: true, message: "请选择抵质押类型", trigger: "change"}
|
||||
],
|
||||
couponRate: [
|
||||
{validator: validateCouponRate, trigger: "blur"}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -194,6 +236,24 @@ export default {
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
},
|
||||
isCollateralGuarantee() {
|
||||
return this.form.guarType === '抵押' || this.form.guarType === '质押'
|
||||
},
|
||||
isStockTransfer() {
|
||||
return this.form.businessType === '存量转贷'
|
||||
},
|
||||
isCertificatePledge() {
|
||||
return this.form.guarType === '质押' && this.form.collType === '存单质押'
|
||||
},
|
||||
collateralTypeOptions() {
|
||||
if (this.form.guarType === '抵押') {
|
||||
return ['一线', '一类', '二类', '三类']
|
||||
}
|
||||
if (this.form.guarType === '质押') {
|
||||
return ['存单质押', '其他质押']
|
||||
}
|
||||
return []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -201,6 +261,14 @@ export default {
|
||||
if (val) {
|
||||
this.reset()
|
||||
}
|
||||
},
|
||||
'form.guarType'(val, oldVal) {
|
||||
if (val !== oldVal) {
|
||||
this.resetCollateralFields()
|
||||
}
|
||||
},
|
||||
'form.collType'() {
|
||||
this.resetCouponRateIfNotCertificatePledge()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -209,21 +277,30 @@ export default {
|
||||
this.form = {
|
||||
orgCode: '892000',
|
||||
runType: '1',
|
||||
custIsn: undefined,
|
||||
custName: undefined,
|
||||
custIsn: this.customerMap ? this.customerMap.cust_isn : undefined,
|
||||
custName: this.customerMap ? this.customerMap.cust_name : undefined,
|
||||
idType: undefined,
|
||||
idNum: undefined,
|
||||
idNum: this.customerMap ? (this.customerMap.cust_id || '').substring(3) : undefined,
|
||||
guarType: undefined,
|
||||
applyAmt: undefined,
|
||||
loanPurpose: undefined,
|
||||
loanTerm: undefined,
|
||||
bizProof: false,
|
||||
businessType: undefined,
|
||||
loanRateHistory: undefined,
|
||||
loanLoop: false,
|
||||
collType: undefined,
|
||||
collThirdParty: false
|
||||
collThirdParty: false,
|
||||
couponRate: undefined
|
||||
}
|
||||
this.submitting = false
|
||||
this.resetForm("form")
|
||||
this.showHistorySelector = false
|
||||
this.historyLoading = false
|
||||
this.historyContracts = []
|
||||
this.selectedHistoryContract = null
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.clearValidate()
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 对话框关闭处理 */
|
||||
handleClose() {
|
||||
@@ -234,17 +311,97 @@ export default {
|
||||
this.dialogVisible = false
|
||||
this.reset()
|
||||
},
|
||||
/** 清空抵质押字段 */
|
||||
resetCollateralFields() {
|
||||
this.form.collType = undefined
|
||||
this.form.collThirdParty = false
|
||||
this.resetCouponRateIfNotCertificatePledge()
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
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 === '存量转贷') {
|
||||
this.queryHistoryContracts()
|
||||
}
|
||||
},
|
||||
queryHistoryContracts() {
|
||||
if (!this.form.custIsn) {
|
||||
this.$modal.msgWarning("客户内码不能为空")
|
||||
return
|
||||
}
|
||||
this.historyLoading = true
|
||||
queryHistoryContracts(this.form.custIsn).then(response => {
|
||||
this.historyContracts = response.data || []
|
||||
if (this.historyContracts.length === 0) {
|
||||
this.$modal.msgWarning("未查询到历史贷款合同")
|
||||
return
|
||||
}
|
||||
this.showHistorySelector = true
|
||||
}).finally(() => {
|
||||
this.historyLoading = false
|
||||
})
|
||||
},
|
||||
handleHistoryContractSelect(row) {
|
||||
if (!row.loan_rate_history) {
|
||||
this.$modal.msgWarning("历史贷款利率不能为空")
|
||||
return
|
||||
}
|
||||
this.selectedHistoryContract = row
|
||||
this.form.loanRateHistory = row.loan_rate_history
|
||||
},
|
||||
clearHistoryContract() {
|
||||
this.selectedHistoryContract = null
|
||||
this.form.loanRateHistory = undefined
|
||||
this.historyContracts = []
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.clearValidate(['loanRateHistory'])
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.isStockTransfer && !this.form.loanRateHistory) {
|
||||
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',
|
||||
collThirdParty: this.form.collThirdParty ? '1' : '0'
|
||||
loanLoop: this.form.loanLoop ? '1' : '0'
|
||||
}
|
||||
if (this.isCollateralGuarantee) {
|
||||
data.collThirdParty = this.form.collThirdParty ? '1' : '0'
|
||||
} else {
|
||||
delete data.collType
|
||||
delete data.collThirdParty
|
||||
}
|
||||
if (!this.isStockTransfer) {
|
||||
delete data.loanRateHistory
|
||||
}
|
||||
if (!this.isCertificatePledge) {
|
||||
delete data.couponRate
|
||||
}
|
||||
|
||||
createPersonalWorkflow(data).then(response => {
|
||||
|
||||
@@ -80,6 +80,8 @@
|
||||
<el-descriptions-item label="担保方式">{{ detailData.guarType }}</el-descriptions-item>
|
||||
<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="借款期限">{{ detailData.loanTerm || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否有经营佐证">{{
|
||||
formatBoolean(detailData.bizProof)
|
||||
|
||||
@@ -81,17 +81,33 @@
|
||||
<!-- 客户类型选择对话框 -->
|
||||
<customer-type-selector :visible.sync="showTypeSelector" @select="handleSelectType"/>
|
||||
|
||||
<!-- 客户号查询选择对话框 -->
|
||||
<customer-map-selector
|
||||
:visible.sync="showCustomerMapSelector"
|
||||
:customer-type="selectedCustomerType"
|
||||
@select="handleCustomerMapSelect"
|
||||
/>
|
||||
|
||||
<!-- 个人客户创建对话框 -->
|
||||
<personal-create-dialog :visible.sync="showPersonalDialog" @success="handleCreateSuccess"/>
|
||||
<personal-create-dialog
|
||||
:visible.sync="showPersonalDialog"
|
||||
:customer-map="selectedCustomerMap"
|
||||
@success="handleCreateSuccess"
|
||||
/>
|
||||
|
||||
<!-- 企业客户创建对话框 -->
|
||||
<corporate-create-dialog :visible.sync="showCorporateDialog" @success="handleCreateSuccess"/>
|
||||
<corporate-create-dialog
|
||||
:visible.sync="showCorporateDialog"
|
||||
:customer-map="selectedCustomerMap"
|
||||
@success="handleCreateSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {listWorkflow} from "@/api/loanPricing/workflow"
|
||||
import CustomerTypeSelector from "./components/CustomerTypeSelector"
|
||||
import CustomerMapSelector from "./components/CustomerMapSelector"
|
||||
import PersonalCreateDialog from "./components/PersonalCreateDialog"
|
||||
import CorporateCreateDialog from "./components/CorporateCreateDialog"
|
||||
|
||||
@@ -99,6 +115,7 @@ export default {
|
||||
name: "LoanPricingWorkflow",
|
||||
components: {
|
||||
CustomerTypeSelector,
|
||||
CustomerMapSelector,
|
||||
PersonalCreateDialog,
|
||||
CorporateCreateDialog
|
||||
},
|
||||
@@ -114,6 +131,12 @@ export default {
|
||||
workflowList: [],
|
||||
// 是否显示客户类型选择弹出层
|
||||
showTypeSelector: false,
|
||||
// 是否显示客户号查询选择弹出层
|
||||
showCustomerMapSelector: false,
|
||||
// 当前选择的客户类型
|
||||
selectedCustomerType: undefined,
|
||||
// 当前选择的客户号映射记录
|
||||
selectedCustomerMap: null,
|
||||
// 是否显示个人客户创建弹出层
|
||||
showPersonalDialog: false,
|
||||
// 是否显示企业客户创建弹出层
|
||||
@@ -134,6 +157,18 @@ export default {
|
||||
activated() {
|
||||
this.getList()
|
||||
},
|
||||
watch: {
|
||||
showPersonalDialog(val) {
|
||||
if (!val && !this.showCorporateDialog) {
|
||||
this.clearSelectedCustomer()
|
||||
}
|
||||
},
|
||||
showCorporateDialog(val) {
|
||||
if (!val && !this.showPersonalDialog) {
|
||||
this.clearSelectedCustomer()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 查询利率定价流程列表 */
|
||||
getList() {
|
||||
@@ -167,15 +202,28 @@ export default {
|
||||
},
|
||||
/** 选择客户类型回调 */
|
||||
handleSelectType(type) {
|
||||
if (type === 'personal') {
|
||||
this.selectedCustomerType = type
|
||||
this.selectedCustomerMap = null
|
||||
this.showCustomerMapSelector = true
|
||||
},
|
||||
/** 选择客户号映射记录回调 */
|
||||
handleCustomerMapSelect(row) {
|
||||
this.selectedCustomerMap = row
|
||||
if (this.selectedCustomerType === 'personal') {
|
||||
this.showPersonalDialog = true
|
||||
} else if (type === 'corporate') {
|
||||
} else if (this.selectedCustomerType === 'corporate') {
|
||||
this.showCorporateDialog = true
|
||||
}
|
||||
},
|
||||
/** 创建成功回调 */
|
||||
handleCreateSuccess() {
|
||||
this.clearSelectedCustomer()
|
||||
this.getList()
|
||||
},
|
||||
/** 清空已选客户信息 */
|
||||
clearSelectedCustomer() {
|
||||
this.selectedCustomerMap = null
|
||||
this.selectedCustomerType = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -10,8 +10,10 @@ 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('label="贷款用途"') &&
|
||||
!personalCreateDialog.includes('prop="loanPurpose"') &&
|
||||
!personalCreateDialog.includes('loanPurpose:'),
|
||||
'个人新增弹窗不应继续保留贷款用途字段'
|
||||
)
|
||||
|
||||
assert(
|
||||
@@ -20,8 +22,8 @@ assert(
|
||||
)
|
||||
|
||||
assert(
|
||||
personalCreateDialog.includes("value=\"consumer\"") && personalCreateDialog.includes("value=\"business\""),
|
||||
'个人新增弹窗缺少贷款用途选项'
|
||||
!personalCreateDialog.includes("value=\"consumer\"") && !personalCreateDialog.includes("value=\"business\""),
|
||||
'个人新增弹窗不应继续保留贷款用途选项'
|
||||
)
|
||||
|
||||
assert(
|
||||
@@ -33,23 +35,33 @@ assert(
|
||||
)
|
||||
|
||||
assert(
|
||||
personalCreateDialog.includes('label="一类"') &&
|
||||
personalCreateDialog.includes('label="二类"') &&
|
||||
personalCreateDialog.includes('label="三类"') &&
|
||||
!personalCreateDialog.includes('label="一线"'),
|
||||
'个人新增弹窗抵质押类型选项未按 Excel 对齐'
|
||||
personalCreateDialog.includes('v-if="isCollateralGuarantee"') &&
|
||||
personalCreateDialog.includes("this.form.guarType === '抵押' || this.form.guarType === '质押'") &&
|
||||
personalCreateDialog.includes('resetCollateralFields()'),
|
||||
'个人新增弹窗抵质押信息未按担保方式显示并清空'
|
||||
)
|
||||
|
||||
assert(
|
||||
!personalCreateDialog.includes('{required: true, message: "请选择抵质押类型", trigger: "change"}'),
|
||||
'个人新增弹窗仍将抵质押类型设为必填'
|
||||
personalCreateDialog.includes('collateralTypeOptions') &&
|
||||
personalCreateDialog.includes("return ['一线', '一类', '二类', '三类']") &&
|
||||
personalCreateDialog.includes("return ['存单质押', '其他质押']"),
|
||||
'个人新增弹窗抵质押类型选项未按担保方式动态切换'
|
||||
)
|
||||
|
||||
assert(
|
||||
personalCreateDialog.includes("bizProof: this.form.bizProof ? '1' : '0'") &&
|
||||
personalCreateDialog.includes('{required: true, message: "请选择抵质押类型", trigger: "change"}'),
|
||||
'个人新增弹窗抵质押类型应为必填'
|
||||
)
|
||||
|
||||
assert(
|
||||
!personalCreateDialog.includes('label="是否有经营佐证"') &&
|
||||
!personalCreateDialog.includes('prop="bizProof"') &&
|
||||
!personalCreateDialog.includes('bizProof:') &&
|
||||
personalCreateDialog.includes("loanLoop: this.form.loanLoop ? '1' : '0'") &&
|
||||
personalCreateDialog.includes("collThirdParty: this.form.collThirdParty ? '1' : '0'"),
|
||||
'个人新增弹窗开关字段未按 1/0 提交'
|
||||
personalCreateDialog.includes("data.collThirdParty = this.form.collThirdParty ? '1' : '0'") &&
|
||||
personalCreateDialog.includes('delete data.collType') &&
|
||||
personalCreateDialog.includes('delete data.collThirdParty'),
|
||||
'个人新增弹窗开关字段或非抵质押提交字段处理不正确'
|
||||
)
|
||||
|
||||
assert(
|
||||
|
||||
@@ -15,6 +15,7 @@ assert(
|
||||
)
|
||||
|
||||
const requiredRetailFields = [
|
||||
'retailOutput.greyBlackCust',
|
||||
'retailOutput.loanRateHistory',
|
||||
'retailOutput.minRateProduct',
|
||||
'retailOutput.smoothRange',
|
||||
@@ -26,4 +27,10 @@ requiredRetailFields.forEach((field) => {
|
||||
assert(modelOutput.includes(field), `模型输出缺少字段展示: ${field}`)
|
||||
})
|
||||
|
||||
assert(
|
||||
modelOutput.indexOf('label="灰黑名单客户"') > modelOutput.indexOf('<h4 class="section-title">基本信息</h4>')
|
||||
&& modelOutput.indexOf('label="灰黑名单客户"') < modelOutput.indexOf('<h4 class="section-title">测算结果</h4>'),
|
||||
'灰黑名单客户未展示在个人模型输出基础信息中'
|
||||
)
|
||||
|
||||
console.log('retail display fields assertions passed')
|
||||
|
||||
13
sql/2026-04-16-shangyu-corporate-alignment.sql
Normal file
13
sql/2026-04-16-shangyu-corporate-alignment.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- 上虞对公利率测算字段对齐
|
||||
|
||||
ALTER TABLE `loan_pricing_workflow`
|
||||
ADD COLUMN `repay_method` varchar(20) DEFAULT NULL COMMENT '还款方式: 分期/不分期' AFTER `loan_term`;
|
||||
|
||||
ALTER TABLE `model_corp_output_fields`
|
||||
ADD COLUMN `repay_method` varchar(100) DEFAULT NULL COMMENT '还款方式' AFTER `id_num`,
|
||||
ADD COLUMN `is_trade_build_ent` varchar(100) DEFAULT NULL COMMENT '贸易和建筑业企业' AFTER `is_tech_ent`,
|
||||
ADD COLUMN `loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史利率' AFTER `calculate_rate`,
|
||||
ADD COLUMN `min_rate_product` varchar(100) DEFAULT NULL COMMENT '产品最低利率下限' AFTER `loan_rate_history`,
|
||||
ADD COLUMN `smooth_range` varchar(100) DEFAULT NULL COMMENT '平滑幅度' AFTER `min_rate_product`,
|
||||
ADD COLUMN `final_calculate_rate` varchar(100) DEFAULT NULL COMMENT '最终测算利率' AFTER `smooth_range`,
|
||||
ADD COLUMN `reference_rate` varchar(100) DEFAULT NULL COMMENT '参考利率' AFTER `final_calculate_rate`;
|
||||
15
sql/2026-04-17-corporate.sql
Normal file
15
sql/2026-04-17-corporate.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- 对公展示指标字段对齐:补充还款方式与模型输出缺失字段
|
||||
-- 执行库:loan-pricing
|
||||
|
||||
ALTER TABLE `loan_pricing_workflow`
|
||||
ADD COLUMN `repay_method` varchar(20) DEFAULT NULL COMMENT '还款方式: 分期/不分期'
|
||||
AFTER `guar_type`;
|
||||
|
||||
ALTER TABLE `model_corp_output_fields`
|
||||
ADD COLUMN `repay_method` varchar(100) DEFAULT NULL COMMENT '还款方式' AFTER `id_num`,
|
||||
ADD COLUMN `is_trade_build_ent` varchar(100) DEFAULT NULL COMMENT '贸易和建筑业企业' AFTER `has_settle_acct`,
|
||||
ADD COLUMN `loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史利率' AFTER `calculate_rate`,
|
||||
ADD COLUMN `min_rate_product` varchar(100) DEFAULT NULL COMMENT '产品最低利率下限' AFTER `loan_rate_history`,
|
||||
ADD COLUMN `smooth_range` varchar(100) DEFAULT NULL COMMENT '平滑幅度' AFTER `min_rate_product`,
|
||||
ADD COLUMN `final_calculate_rate` varchar(100) DEFAULT NULL COMMENT '最终测算利率' AFTER `smooth_range`,
|
||||
ADD COLUMN `reference_rate` varchar(100) DEFAULT NULL COMMENT '参考利率' AFTER `final_calculate_rate`;
|
||||
4
sql/add_business_type_history_rate_20260429.sql
Normal file
4
sql/add_business_type_history_rate_20260429.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- 为利率定价流程添加业务种类和历史贷款利率字段
|
||||
ALTER TABLE `loan_pricing_workflow`
|
||||
ADD COLUMN `business_type` varchar(20) DEFAULT NULL COMMENT '业务种类' AFTER `loan_purpose`,
|
||||
ADD COLUMN `loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率' AFTER `business_type`;
|
||||
3
sql/add_coupon_rate_20260511.sql
Normal file
3
sql/add_coupon_rate_20260511.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- 上虞利率定价存单票面利率字段
|
||||
ALTER TABLE `loan_pricing_workflow`
|
||||
ADD COLUMN `coupon_rate` varchar(100) DEFAULT NULL COMMENT '存单票面利率' AFTER `loan_rate_history`;
|
||||
2
sql/add_model_retail_grey_black_cust_20260427.sql
Normal file
2
sql/add_model_retail_grey_black_cust_20260427.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `model_retail_output_fields`
|
||||
ADD COLUMN `grey_black_cust` varchar(100) DEFAULT '' COMMENT '灰黑名单客户' AFTER `base_loan_rate`;
|
||||
@@ -751,7 +751,8 @@ CREATE TABLE `loan_pricing_workflow` (
|
||||
`mid_ent_ele_ddc` varchar(10) DEFAULT NULL COMMENT '中间业务_企业_电费代扣: true/false',
|
||||
`mid_ent_water_ddc` varchar(10) DEFAULT NULL COMMENT '中间业务_企业_水费代扣: true/false',
|
||||
`apply_amt` varchar(50) NOT NULL COMMENT '申请金额(元)',
|
||||
`loan_term` varchar(50) DEFAULT NULL COMMENT '贷款期限',
|
||||
`loan_term` varchar(50) DEFAULT NULL COMMENT '借款期限(年)',
|
||||
`repay_method` varchar(20) DEFAULT NULL COMMENT '还款方式: 分期/不分期',
|
||||
`is_clean_ent` varchar(10) DEFAULT NULL COMMENT '净身企业: true/false',
|
||||
`has_settle_acct` varchar(10) DEFAULT NULL COMMENT '开立基本结算账户: true/false',
|
||||
`is_manufacturing` varchar(10) DEFAULT NULL COMMENT '制造业企业: true/false',
|
||||
@@ -762,9 +763,12 @@ CREATE TABLE `loan_pricing_workflow` (
|
||||
`is_tax_a` varchar(10) DEFAULT NULL COMMENT '是否纳税信用等级A级: true/false',
|
||||
`is_agri_leading` varchar(10) DEFAULT NULL COMMENT '是否县级及以上农业龙头企业: true/false',
|
||||
`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 '存单票面利率',
|
||||
`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 '抵质押类型: 一线/一类/二类',
|
||||
`coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一类/二类/三类/四类',
|
||||
`coll_third_party` varchar(10) DEFAULT NULL COMMENT '抵质押物是否三方所有: true/false',
|
||||
`loan_rate` varchar(20) DEFAULT NULL COMMENT '贷款利率',
|
||||
`execute_rate` varchar(20) DEFAULT NULL COMMENT '执行利率(%)',
|
||||
@@ -800,6 +804,7 @@ CREATE TABLE `model_corp_output_fields` (
|
||||
`cust_name` varchar(100) DEFAULT NULL COMMENT '客户名称',
|
||||
`id_type` varchar(100) DEFAULT NULL COMMENT '证件类型',
|
||||
`id_num` varchar(100) DEFAULT NULL COMMENT '证件号码',
|
||||
`repay_method` varchar(100) DEFAULT NULL COMMENT '还款方式',
|
||||
`base_loan_rate` varchar(100) DEFAULT NULL COMMENT '基准利率',
|
||||
`is_first_loan` varchar(100) DEFAULT NULL COMMENT '我行首贷客户',
|
||||
`faith_day` varchar(100) DEFAULT NULL COMMENT '用信天数',
|
||||
@@ -827,6 +832,7 @@ CREATE TABLE `model_corp_output_fields` (
|
||||
`is_agri_guar` varchar(100) DEFAULT NULL COMMENT '省农担担保贷款',
|
||||
`is_green_loan` varchar(100) DEFAULT NULL COMMENT '绿色贷款',
|
||||
`is_tech_ent` varchar(100) DEFAULT NULL COMMENT '科技型企业',
|
||||
`is_trade_build_ent` varchar(100) DEFAULT NULL COMMENT '贸易和建筑业企业',
|
||||
`bp_ent_type` varchar(100) DEFAULT NULL COMMENT 'BP_企业客户类别',
|
||||
`totoal_bp_relevance` varchar(100) DEFAULT NULL COMMENT 'TOTAL_BP_关联度',
|
||||
`loan_term` varchar(100) DEFAULT NULL COMMENT '贷款期限',
|
||||
@@ -844,6 +850,11 @@ CREATE TABLE `model_corp_output_fields` (
|
||||
`totoal_bp_risk` varchar(100) DEFAULT NULL COMMENT 'TOTAL_BP_风险度',
|
||||
`total_bp` varchar(100) DEFAULT NULL COMMENT '浮动BP',
|
||||
`calculate_rate` varchar(100) DEFAULT NULL COMMENT '测算利率',
|
||||
`loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史利率',
|
||||
`min_rate_product` varchar(100) DEFAULT NULL COMMENT '产品最低利率下限',
|
||||
`smooth_range` varchar(100) DEFAULT NULL COMMENT '平滑幅度',
|
||||
`final_calculate_rate` varchar(100) DEFAULT NULL COMMENT '最终测算利率',
|
||||
`reference_rate` varchar(100) DEFAULT NULL COMMENT '参考利率',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='客户贷款利率测算表';
|
||||
@@ -865,6 +876,7 @@ CREATE TABLE `model_retail_output_fields` (
|
||||
`id_type` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '证件类型',
|
||||
`id_num` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '证件号码',
|
||||
`base_loan_rate` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '基准利率',
|
||||
`grey_black_cust` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '灰黑名单客户',
|
||||
`is_first_loan` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '我行首贷客户',
|
||||
`faith_day` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '用信天数',
|
||||
`cust_age` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '客户年龄',
|
||||
|
||||
@@ -333,7 +333,8 @@ CREATE TABLE `loan_pricing_workflow` (
|
||||
`mid_ent_ele_ddc` varchar(10) DEFAULT NULL COMMENT '中间业务_企业_电费代扣: true/false',
|
||||
`mid_ent_water_ddc` varchar(10) DEFAULT NULL COMMENT '中间业务_企业_水费代扣: true/false',
|
||||
`apply_amt` varchar(50) NOT NULL COMMENT '申请金额(元)',
|
||||
`loan_term` varchar(50) DEFAULT NULL COMMENT '贷款期限',
|
||||
`loan_term` varchar(50) DEFAULT NULL COMMENT '借款期限(年)',
|
||||
`repay_method` varchar(20) DEFAULT NULL COMMENT '还款方式: 分期/不分期',
|
||||
`is_clean_ent` varchar(10) DEFAULT NULL COMMENT '净身企业: true/false',
|
||||
`has_settle_acct` varchar(10) DEFAULT NULL COMMENT '开立基本结算账户: true/false',
|
||||
`is_manufacturing` varchar(10) DEFAULT NULL COMMENT '制造业企业: true/false',
|
||||
@@ -344,9 +345,12 @@ CREATE TABLE `loan_pricing_workflow` (
|
||||
`is_tax_a` varchar(10) DEFAULT NULL COMMENT '是否纳税信用等级A级: true/false',
|
||||
`is_agri_leading` varchar(10) DEFAULT NULL COMMENT '是否县级及以上农业龙头企业: true/false',
|
||||
`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 '存单票面利率',
|
||||
`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 '抵质押类型: 一线/一类/二类',
|
||||
`coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一类/二类/三类/四类',
|
||||
`coll_third_party` varchar(10) DEFAULT NULL COMMENT '抵质押物是否三方所有: true/false',
|
||||
`loan_rate` varchar(20) DEFAULT NULL COMMENT '贷款利率',
|
||||
`execute_rate` varchar(20) DEFAULT NULL COMMENT '执行利率(%)',
|
||||
@@ -382,6 +386,7 @@ CREATE TABLE `model_corp_output_fields` (
|
||||
`cust_name` varchar(100) DEFAULT NULL COMMENT '客户名称',
|
||||
`id_type` varchar(100) DEFAULT NULL COMMENT '证件类型',
|
||||
`id_num` varchar(100) DEFAULT NULL COMMENT '证件号码',
|
||||
`repay_method` varchar(100) DEFAULT NULL COMMENT '还款方式',
|
||||
`base_loan_rate` varchar(100) DEFAULT NULL COMMENT '基准利率',
|
||||
`is_first_loan` varchar(100) DEFAULT NULL COMMENT '我行首贷客户',
|
||||
`faith_day` varchar(100) DEFAULT NULL COMMENT '用信天数',
|
||||
@@ -409,6 +414,7 @@ CREATE TABLE `model_corp_output_fields` (
|
||||
`is_agri_guar` varchar(100) DEFAULT NULL COMMENT '省农担担保贷款',
|
||||
`is_green_loan` varchar(100) DEFAULT NULL COMMENT '绿色贷款',
|
||||
`is_tech_ent` varchar(100) DEFAULT NULL COMMENT '科技型企业',
|
||||
`is_trade_build_ent` varchar(100) DEFAULT NULL COMMENT '贸易和建筑业企业',
|
||||
`bp_ent_type` varchar(100) DEFAULT NULL COMMENT 'BP_企业客户类别',
|
||||
`totoal_bp_relevance` varchar(100) DEFAULT NULL COMMENT 'TOTAL_BP_关联度',
|
||||
`loan_term` varchar(100) DEFAULT NULL COMMENT '贷款期限',
|
||||
@@ -426,6 +432,11 @@ CREATE TABLE `model_corp_output_fields` (
|
||||
`totoal_bp_risk` varchar(100) DEFAULT NULL COMMENT 'TOTAL_BP_风险度',
|
||||
`total_bp` varchar(100) DEFAULT NULL COMMENT '浮动BP',
|
||||
`calculate_rate` varchar(100) DEFAULT NULL COMMENT '测算利率',
|
||||
`loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史利率',
|
||||
`min_rate_product` varchar(100) DEFAULT NULL COMMENT '产品最低利率下限',
|
||||
`smooth_range` varchar(100) DEFAULT NULL COMMENT '平滑幅度',
|
||||
`final_calculate_rate` varchar(100) DEFAULT NULL COMMENT '最终测算利率',
|
||||
`reference_rate` varchar(100) DEFAULT NULL COMMENT '参考利率',
|
||||
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='客户贷款利率测算表';
|
||||
@@ -447,6 +458,7 @@ CREATE TABLE `model_retail_output_fields` (
|
||||
`id_type` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '证件类型',
|
||||
`id_num` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '证件号码',
|
||||
`base_loan_rate` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '基准利率',
|
||||
`grey_black_cust` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '灰黑名单客户',
|
||||
`is_first_loan` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '我行首贷客户',
|
||||
`faith_day` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '用信天数',
|
||||
`cust_age` varchar(100) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '客户年龄',
|
||||
|
||||
@@ -14,20 +14,30 @@ CREATE TABLE `loan_pricing_workflow` (
|
||||
`mid_ent_ele_ddc` varchar(10) DEFAULT NULL COMMENT '中间业务_企业_电费代扣: true/false',
|
||||
`mid_ent_water_ddc` varchar(10) DEFAULT NULL COMMENT '中间业务_企业_水费代扣: true/false',
|
||||
`apply_amt` varchar(50) NOT NULL COMMENT '申请金额(元)',
|
||||
`loan_term` varchar(50) DEFAULT NULL COMMENT '借款期限(年)',
|
||||
`repay_method` varchar(20) DEFAULT NULL COMMENT '还款方式: 分期/不分期',
|
||||
`is_clean_ent` varchar(10) DEFAULT NULL COMMENT '净身企业: true/false',
|
||||
`has_settle_acct` varchar(10) DEFAULT NULL COMMENT '开立基本结算账户: true/false',
|
||||
`is_manufacturing` varchar(10) DEFAULT NULL COMMENT '制造业企业: true/false',
|
||||
`is_agri_guar` varchar(10) DEFAULT NULL COMMENT '省农担担保贷款: true/false',
|
||||
`is_tech_ent` varchar(10) DEFAULT NULL COMMENT '科技型企业: true/false(科技型企业最多下降5BP)',
|
||||
`is_green_loan` varchar(10) DEFAULT NULL COMMENT '绿色贷款: true/false(绿色贷款最多下降5BP)',
|
||||
`is_trade_construction` varchar(10) DEFAULT NULL COMMENT '贸易和建筑业企业标识: true/false(抵质押类上调20BP)',
|
||||
`is_tax_a` varchar(10) DEFAULT NULL COMMENT '是否纳税信用等级A级: true/false',
|
||||
`is_agri_leading` varchar(10) DEFAULT NULL COMMENT '是否县级及以上农业龙头企业: true/false',
|
||||
`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 '存单票面利率',
|
||||
`biz_proof` varchar(10) DEFAULT NULL COMMENT '是否有经营佐证: true/false',
|
||||
`coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一线/一类/二类',
|
||||
`loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false(贷款合同是否开通循环功能)',
|
||||
`coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一类/二类/三类/四类',
|
||||
`coll_third_party` varchar(10) DEFAULT NULL COMMENT '抵质押物是否三方所有: true/false',
|
||||
`loan_rate` varchar(20) NOT NULL COMMENT '贷款利率',
|
||||
`loan_rate` varchar(20) DEFAULT NULL COMMENT '贷款利率',
|
||||
`execute_rate` varchar(20) DEFAULT NULL COMMENT '执行利率(%)',
|
||||
`cust_name` varchar(100) DEFAULT NULL COMMENT '客户名称',
|
||||
`id_type` varchar(50) DEFAULT NULL COMMENT '证件类型',
|
||||
`id_num` varchar(100) DEFAULT NULL COMMENT '证件号码',
|
||||
`is_inclusive_finance` varchar(10) DEFAULT NULL COMMENT '是否普惠小微借款人: true/false',
|
||||
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
|
||||
@@ -3,57 +3,64 @@
|
||||
*/
|
||||
DROP TABLE IF EXISTS model_corp_output_fields;
|
||||
CREATE TABLE `model_corp_output_fields` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
|
||||
`cust_isn` VARCHAR(100) COMMENT '客户内码',
|
||||
`cust_type` VARCHAR(100) COMMENT '客户类型',
|
||||
`guar_type` VARCHAR(100) COMMENT '担保方式',
|
||||
`cust_name` VARCHAR(100) COMMENT '客户名称',
|
||||
`id_type` VARCHAR(100) COMMENT '证件类型',
|
||||
`id_num` VARCHAR(100) COMMENT '证件号码',
|
||||
`base_loan_rate` VARCHAR(100) COMMENT '基准利率',
|
||||
`is_first_loan` VARCHAR(100) COMMENT '我行首贷客户',
|
||||
`faith_day` VARCHAR(100) COMMENT '用信天数',
|
||||
`bp_first_loan` VARCHAR(100) COMMENT 'BP_首贷',
|
||||
`bp_age_loan` VARCHAR(100) COMMENT 'BP_贷龄',
|
||||
`total_bp_loyalty` VARCHAR(100) COMMENT 'TOTAL_BP_忠诚度',
|
||||
`balance_avg` VARCHAR(100) COMMENT '存款年日均',
|
||||
`loan_avg` VARCHAR(100) COMMENT '贷款年日均',
|
||||
`derivation_rate` VARCHAR(100) COMMENT '派生率',
|
||||
`total_bp_contribution` VARCHAR(100) COMMENT 'TOTAL_BP_贡献度',
|
||||
`mid_ent_connect` VARCHAR(100) COMMENT '中间业务_企业_企业互联',
|
||||
`mid_ent_effect` VARCHAR(100) COMMENT '中间业务_企业_有效价值客户',
|
||||
`mid_ent_inter` VARCHAR(100) COMMENT '中间业务_企业_国际业务',
|
||||
`mid_ent_accept` VARCHAR(100) COMMENT '中间业务_企业_承兑',
|
||||
`mid_ent_discount` VARCHAR(100) COMMENT '中间业务_企业_贴现',
|
||||
`mid_ent_ele_ddc` VARCHAR(100) COMMENT '中间业务_企业_电费代扣',
|
||||
`mid_ent_water_ddc` VARCHAR(100) COMMENT '中间业务_企业_水费代扣',
|
||||
`mid_ent_tax` VARCHAR(100) COMMENT '中间业务_企业_税务代扣',
|
||||
`bp_mid` VARCHAR(100) COMMENT 'BP_中间业务',
|
||||
`payroll` VARCHAR(100) COMMENT '代发工资户数',
|
||||
`inv_loan_amount` VARCHAR(100) COMMENT '存量贷款余额',
|
||||
`bp_payroll` VARCHAR(100) COMMENT 'BP_代发工资',
|
||||
`is_clean_ent` VARCHAR(100) COMMENT '净身企业',
|
||||
`has_settle_acct` VARCHAR(100) COMMENT '开立基本结算账户',
|
||||
`is_agri_guar` VARCHAR(100) COMMENT '省农担担保贷款',
|
||||
`is_green_loan` VARCHAR(100) COMMENT '绿色贷款',
|
||||
`is_tech_ent` VARCHAR(100) COMMENT '科技型企业',
|
||||
`bp_ent_type` VARCHAR(100) COMMENT 'BP_企业客户类别',
|
||||
`totoal_bp_relevance` VARCHAR(100) COMMENT 'TOTAL_BP_关联度',
|
||||
`loan_term` VARCHAR(100) COMMENT '贷款期限',
|
||||
`bp_loan_term` VARCHAR(100) COMMENT 'BP_贷款期限',
|
||||
`apply_amt` VARCHAR(100) COMMENT '申请金额',
|
||||
`bp_loan_amount` VARCHAR(100) COMMENT 'BP_贷款额度',
|
||||
`coll_type` VARCHAR(100) COMMENT '抵质押类型',
|
||||
`coll_third_party` VARCHAR(100) COMMENT '抵质押物是否三方所有',
|
||||
`bp_collateral` VARCHAR(100) COMMENT 'BP_抵押物',
|
||||
`grey_cust` VARCHAR(100) COMMENT '灰名单客户',
|
||||
`prin_overdue` VARCHAR(100) COMMENT '本金逾期',
|
||||
`interest_overdue` VARCHAR(100) COMMENT '利息逾期',
|
||||
`card_overdue` VARCHAR(100) COMMENT '信用卡逾期',
|
||||
`bp_grey_overdue` VARCHAR(100) COMMENT 'BP_灰名单与逾期',
|
||||
`totoal_bp_risk` VARCHAR(100) COMMENT 'TOTAL_BP_风险度',
|
||||
`total_bp` VARCHAR(100) COMMENT '浮动BP',
|
||||
`calculate_rate` VARCHAR(100) COMMENT '测算利率',
|
||||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='客户贷款利率测算表';
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
|
||||
`cust_isn` VARCHAR(100) COMMENT '客户内码',
|
||||
`cust_type` VARCHAR(100) COMMENT '客户类型',
|
||||
`guar_type` VARCHAR(100) COMMENT '担保方式',
|
||||
`cust_name` VARCHAR(100) COMMENT '客户名称',
|
||||
`id_type` VARCHAR(100) COMMENT '证件类型',
|
||||
`id_num` VARCHAR(100) COMMENT '证件号码',
|
||||
`repay_method` VARCHAR(100) COMMENT '还款方式',
|
||||
`base_loan_rate` VARCHAR(100) COMMENT '基准利率',
|
||||
`is_first_loan` VARCHAR(100) COMMENT '我行首贷客户',
|
||||
`faith_day` VARCHAR(100) COMMENT '用信天数',
|
||||
`bp_first_loan` VARCHAR(100) COMMENT 'BP_首贷',
|
||||
`bp_age_loan` VARCHAR(100) COMMENT 'BP_贷龄',
|
||||
`total_bp_loyalty` VARCHAR(100) COMMENT 'TOTAL_BP_忠诚度',
|
||||
`balance_avg` VARCHAR(100) COMMENT '存款年日均',
|
||||
`loan_avg` VARCHAR(100) COMMENT '贷款年日均',
|
||||
`derivation_rate` VARCHAR(100) COMMENT '派生率',
|
||||
`total_bp_contribution` VARCHAR(100) COMMENT 'TOTAL_BP_贡献度',
|
||||
`mid_ent_connect` VARCHAR(100) COMMENT '中间业务_企业_企业互联',
|
||||
`mid_ent_effect` VARCHAR(100) COMMENT '中间业务_企业_有效价值客户',
|
||||
`mid_ent_inter` VARCHAR(100) COMMENT '中间业务_企业_国际业务',
|
||||
`mid_ent_accept` VARCHAR(100) COMMENT '中间业务_企业_承兑',
|
||||
`mid_ent_discount` VARCHAR(100) COMMENT '中间业务_企业_贴现',
|
||||
`mid_ent_ele_ddc` VARCHAR(100) COMMENT '中间业务_企业_电费代扣',
|
||||
`mid_ent_water_ddc` VARCHAR(100) COMMENT '中间业务_企业_水费代扣',
|
||||
`mid_ent_tax` VARCHAR(100) COMMENT '中间业务_企业_税务代扣',
|
||||
`bp_mid` VARCHAR(100) COMMENT 'BP_中间业务',
|
||||
`payroll` VARCHAR(100) COMMENT '代发工资户数',
|
||||
`inv_loan_amount` VARCHAR(100) COMMENT '存量贷款余额',
|
||||
`bp_payroll` VARCHAR(100) COMMENT 'BP_代发工资',
|
||||
`is_clean_ent` VARCHAR(100) COMMENT '净身企业',
|
||||
`has_settle_acct` VARCHAR(100) COMMENT '开立基本结算账户',
|
||||
`is_agri_guar` VARCHAR(100) COMMENT '省农担担保贷款',
|
||||
`is_green_loan` VARCHAR(100) COMMENT '绿色贷款',
|
||||
`is_tech_ent` VARCHAR(100) COMMENT '科技型企业',
|
||||
`is_trade_build_ent` VARCHAR(100) COMMENT '贸易和建筑业企业',
|
||||
`bp_ent_type` VARCHAR(100) COMMENT 'BP_企业客户类别',
|
||||
`totoal_bp_relevance` VARCHAR(100) COMMENT 'TOTAL_BP_关联度',
|
||||
`loan_term` VARCHAR(100) COMMENT '借款期限',
|
||||
`bp_loan_term` VARCHAR(100) COMMENT 'BP_贷款期限',
|
||||
`apply_amt` VARCHAR(100) COMMENT '申请金额',
|
||||
`bp_loan_amount` VARCHAR(100) COMMENT 'BP_贷款额度',
|
||||
`coll_type` VARCHAR(100) COMMENT '抵质押类型',
|
||||
`coll_third_party` VARCHAR(100) COMMENT '抵质押物是否三方所有',
|
||||
`bp_collateral` VARCHAR(100) COMMENT 'BP_抵押物',
|
||||
`grey_cust` VARCHAR(100) COMMENT '灰名单客户',
|
||||
`prin_overdue` VARCHAR(100) COMMENT '本金逾期',
|
||||
`interest_overdue` VARCHAR(100) COMMENT '利息逾期',
|
||||
`card_overdue` VARCHAR(100) COMMENT '信用卡逾期',
|
||||
`bp_grey_overdue` VARCHAR(100) COMMENT 'BP_灰名单与逾期',
|
||||
`totoal_bp_risk` VARCHAR(100) COMMENT 'TOTAL_BP_风险度',
|
||||
`total_bp` VARCHAR(100) COMMENT '浮动BP',
|
||||
`calculate_rate` VARCHAR(100) COMMENT '测算利率',
|
||||
`loan_rate_history` VARCHAR(100) COMMENT '历史利率',
|
||||
`min_rate_product` VARCHAR(100) COMMENT '产品最低利率下限',
|
||||
`smooth_range` VARCHAR(100) COMMENT '平滑幅度',
|
||||
`final_calculate_rate` VARCHAR(100) COMMENT '最终测算利率',
|
||||
`reference_rate` VARCHAR(100) COMMENT '参考利率',
|
||||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='客户贷款利率测算表';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user