Compare commits

..

11 Commits

Author SHA1 Message Date
wkc
bc7011313f 客户类型字段更新 2026-02-02 15:25:38 +08:00
wkc
6483a651c4 首页变更 2026-01-30 16:11:59 +08:00
wkc
e1df332a5b 环境变动 2026-01-30 16:10:14 +08:00
wkc
d75e6a9594 ui调整 2026-01-22 16:01:12 +08:00
wkc
c102714f92 添加执行利率接口 2026-01-22 15:04:40 +08:00
wkc
50178923f8 修改模型输出布局 2026-01-22 14:42:38 +08:00
wkc
b3c973df2d 修改模型输出布局 2026-01-22 14:42:01 +08:00
wkc
d813b68eed 移除权限控制 2026-01-22 14:02:33 +08:00
wkc
3b6220cd53 列表模糊查询 2026-01-22 14:02:08 +08:00
wkc
5fc10468cf 字段名称修改 2026-01-22 10:46:53 +08:00
wkc
ae8057b7a6 首页隐藏 2026-01-22 10:33:08 +08:00
61 changed files with 6171 additions and 1836 deletions

View File

@@ -17,7 +17,15 @@
"Bash(cd:*)", "Bash(cd:*)",
"mcp__zai-mcp-server__extract_text_from_screenshot", "mcp__zai-mcp-server__extract_text_from_screenshot",
"Bash(npx openspec validate:*)", "Bash(npx openspec validate:*)",
"Bash(npx openspec show:*)" "Bash(npx openspec show:*)",
"Bash(mvn test:*)",
"Bash(mvn install:*)",
"Bash(mvn clean install:*)",
"mcp__web-reader__webReader",
"Skill(openspec:apply)",
"Skill(superpowers:brainstorming)",
"Skill(superpowers:writing-plans)",
"Skill(superpowers:executing-plans)"
], ],
"additionalDirectories": [ "additionalDirectories": [
"d:\\利率定价\\loan-pricing-892\\loan-pricing-892-v2.0" "d:\\利率定价\\loan-pricing-892\\loan-pricing-892-v2.0"

View File

@@ -0,0 +1,135 @@
# 修复MySQL数据库注释乱码
## 问题描述
MySQL数据库表和字段的中文注释显示为乱码在Navicat等数据库管理工具中查看时出现 `??` 或其他乱码字符。
## 诊断方法
```bash
# 检查字段注释的十六进制编码
mysql -h <host> -P <port> -u <user> -p<password> "<database>" -e "
SELECT COLUMN_NAME, HEX(COLUMN_COMMENT) as comment_hex
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='<database>' AND TABLE_NAME='<table_name>';
"
```
**判断标准:**
- ✅ 正确的UTF-8中文`E4`-`E9` 开头(如 `E698AF` = `是`
- ❌ 错误编码:以 `C3` 开头(表示双重编码问题)
## 解决方案
### 方法1创建UTF-8编码的SQL文件推荐
1. **创建SQL文件**确保保存为UTF-8编码
```sql
-- fix_comments.sql
USE `<database>`;
SET NAMES utf8mb4;
ALTER TABLE `<table_name>` COMMENT = '正确的中文注释';
ALTER TABLE `<table_name>`
MODIFY COLUMN `column1` varchar(50) DEFAULT NULL COMMENT '字段1中文注释',
MODIFY COLUMN `column2` varchar(50) DEFAULT NULL COMMENT '字段2中文注释',
-- ... 更多字段
```
2. **使用utf8mb4字符集执行**
```bash
mysql -h <host> -P <port> -u <user> -p<password> \
--default-character-set=utf8mb4 "<database>" < fix_comments.sql
```
### 方法2验证SQL文件编码
```bash
# 检查文件是否为UTF-8编码
file fix_comments.sql
# 应该输出: Unicode text, UTF-8 text
```
### 方法3通过heredoc创建SQL文件跨平台
```bash
cat > fix_comments.sql << 'SQLEOF'
USE `your_database`;
SET NAMES utf8mb4;
ALTER TABLE your_table MODIFY COLUMN your_column varchar(10) DEFAULT NULL COMMENT '正确的中文注释';
SQLEOF
```
## 验证修复结果
```bash
# 查看表注释
mysql -h <host> -u <user> -p<password> "<database>" -e "
SELECT table_name, HEX(table_comment) as comment_hex
FROM information_schema.tables
WHERE table_schema='<database>' AND table_name='<table_name>';
"
# 查看字段注释
mysql -h <host> -u <user> -p<password> "<database>" -e "
SELECT COLUMN_NAME, COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='<database>' AND TABLE_NAME='<table_name>'
ORDER BY ORDINAL_POSITION;
"
# 检查是否还有乱码字段C3开头
mysql -h <host> -u <user> -p<password> "<database>" -e "
SELECT COUNT(*) as bad_comments
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='<database>' AND TABLE_NAME='<table_name>'
AND HEX(COLUMN_COMMENT) REGEXP '^C3';
"
```
## 常见错误及原因
| 错误现象 | 原因 | 解决方案 |
|----------------------|----------------------------|-----------------------|
| `C3A6CB9C...` (C3开头) | 双重编码UTF-8被当作GBK处理后再转UTF-8 | 使用UTF-8文件 + utf8mb4执行 |
| Windows命令行显示乱码 | 终端编码问题,数据库实际正确 | 用HEX()验证实际存储 |
| SQL文件执行后仍乱码 | 文件未保存为UTF-8 | 用`file`命令检查编码 |
## 最佳实践
1. **所有SQL文件使用UTF-8编码保存**
2. **始终使用 `--default-character-set=utf8mb4` 参数**
3. **在SQL开头添加 `SET NAMES utf8mb4;`**
4. **用HEX()验证而非肉眼判断**
5. **批量修复时用脚本生成SQL文件**
## 示例批量生成修复SQL
```bash
#!/bin/bash
# 为指定表生成修复SQL
DB_NAME="your_database"
TABLE_NAME="your_table"
cat > fix_${TABLE_NAME}_comments.sql << SQLEOF
USE \`${DB_NAME}\`;
SET NAMES utf8mb4;
ALTER TABLE \`${TABLE_NAME}\`
COMMENT = '表的中文名称';
ALTER TABLE \`${TABLE_NAME}\`
MODIFY COLUMN \`id\` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
MODIFY COLUMN \`name\` varchar(100) DEFAULT NULL COMMENT '名称',
-- 添加更多字段...
SQLEOF
# 执行修复
mysql -h localhost -u root -p${DB_PASS} \
--default-character-set=utf8mb4 "${DB_NAME}" < fix_${TABLE_NAME}_comments.sql
```

View File

@@ -14,56 +14,43 @@
## 接口列表 ## 接口列表
### 1. 发起利率定价流程 ### 1. 发起个人客户利率定价流程
创建的利率定价申请。 创建个人客户的利率定价申请。
**接口地址:** `POST /loanPricing/workflow/create` **接口地址:** `POST /loanPricing/workflow/create/personal`
**权限要求:** `loanPricing:workflow:create` **权限要求:** `loanPricing:workflow:create`
**请求参数:** **请求参数:**
| 参数名 | 类型 | 必填 | 说明 | | 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------| |----------------|--------|----|--------------------------|
| orgCode | String | 是 | 机构编码,固定值: 931000 |
| runType | String | 是 | 运行模式,固定值: 1(同步) |
| custIsn | String | 是 | 客户内码 | | custIsn | String | 是 | 客户内码 |
| custType | String | 是 | 客户类型,可选值: 个人/企业 |
| guarType | String | 是 | 担保方式,可选值: 信用/保证/抵押/质押 |
| midPerQuickPay | String | 否 | 中间业务_个人_快捷支付,值: true/false |
| midPerEleDdc | String | 否 | 中间业务_个人_电费代扣,值: true/false |
| midEntEleDdc | String | 否 | 中间业务_企业_电费代扣,值: true/false |
| midEntWaterDdc | String | 否 | 中间业务_企业_水费代扣,值: true/false |
| applyAmt | String | 是 | 申请金额,单位: 元 |
| isCleanEnt | String | 否 | 净身企业,值: true/false |
| hasSettleAcct | String | 否 | 开立基本结算账户,值: true/false |
| isManufacturing | String | 否 | 制造业企业,值: true/false |
| isAgriGuar | String | 否 | 省农担担保贷款,值: true/false |
| isTaxA | String | 否 | 是否纳税信用等级A级,值: true/false |
| isAgriLeading | String | 否 | 是否县级及以上农业龙头企业,值: true/false |
| loanPurpose | String | 否 | 贷款用途,可选值: consumer/business |
| bizProof | String | 否 | 是否有经营佐证,值: true/false |
| collType | String | 否 | 抵质押类型,可选值: 一线/一类/二类 |
| collThirdParty | String | 否 | 抵质押物是否三方所有,值: true/false |
| loanRate | String | 是 | 贷款利率 |
| custName | String | 否 | 客户名称 | | custName | String | 否 | 客户名称 |
| idType | String | 否 | 证件类型 | | idType | String | 否 | 证件类型 |
| isInclusiveFinance | String | 否 | 是否普惠小微借款人,值: true/false | | idNum | String | 否 | 证件号码 |
| guarType | String | 是 | 担保方式,可选值: 信用/保证/抵押/质押 |
| applyAmt | String | 是 | 申请金额,单位: 元 |
| bizProof | String | 否 | 是否有经营佐证,值: true/false |
| loanLoop | String | 否 | 循环功能,值: true/false |
| collType | String | 否 | 抵质押类型,可选值: 一线/一类/二类 |
| collThirdParty | String | 否 | 抵质押物是否三方所有,值: true/false |
**请求示例:** **请求示例:**
```json ```json
{ {
"orgCode": "931000",
"runType": "1",
"custIsn": "CUST001", "custIsn": "CUST001",
"custType": "企业", "custName": "张三",
"idType": "身份证",
"idNum": "110101199001011234",
"guarType": "抵押", "guarType": "抵押",
"applyAmt": "1000000", "applyAmt": "500000",
"loanRate": "4.35", "bizProof": "true",
"custName": "某某科技有限公司", "loanLoop": "false",
"loanPurpose": "business" "collType": "一类",
"collThirdParty": "false"
} }
``` ```
@@ -80,11 +67,11 @@
"orgCode": "931000", "orgCode": "931000",
"runType": "1", "runType": "1",
"custIsn": "CUST001", "custIsn": "CUST001",
"custType": "企业", "custType": "个人",
"guarType": "抵押", "guarType": "抵押",
"applyAmt": "1000000", "applyAmt": "500000",
"loanRate": "4.35", "custName": "张三",
"custName": "某某科技有限公司", "idType": "身份证",
"createTime": "2025-01-19 14:30:25", "createTime": "2025-01-19 14:30:25",
"createBy": "admin" "createBy": "admin"
} }
@@ -93,7 +80,79 @@
--- ---
### 2. 查询利率定价流程列表 ### 2. 发起企业客户利率定价流程
创建企业客户的利率定价申请。
**接口地址:** `POST /loanPricing/workflow/create/corporate`
**权限要求:** `loanPricing:workflow:create`
**请求参数:**
| 参数名 | 类型 | 必填 | 说明 |
|---------------------|--------|----|--------------------------|
| custIsn | String | 是 | 客户内码 |
| custName | String | 否 | 客户名称 |
| idType | String | 否 | 证件类型 |
| idNum | String | 否 | 证件号码 |
| guarType | String | 是 | 担保方式,可选值: 信用/保证/抵押/质押 |
| applyAmt | String | 是 | 申请金额,单位: 元 |
| loanTerm | String | 否 | 贷款期限(月) |
| isAgriGuar | String | 否 | 省农担担保贷款,值: true/false |
| isGreenLoan | String | 否 | 绿色贷款,值: true/false |
| isTechEnt | String | 否 | 科技型企业,值: true/false |
| isTradeConstruction | String | 否 | 贸易和建筑业企业标识,值: true/false |
| collType | String | 否 | 抵质押类型,可选值: 一线/一类/二类 |
| collThirdParty | String | 否 | 抵质押物是否三方所有,值: true/false |
**请求示例:**
```json
{
"custIsn": "CORP001",
"custName": "某某科技有限公司",
"idType": "统一社会信用代码",
"idNum": "91110000100000000X",
"guarType": "抵押",
"applyAmt": "1000000",
"loanTerm": "36",
"isAgriGuar": "false",
"isGreenLoan": "true",
"isTechEnt": "true",
"isTradeConstruction": "false",
"collType": "一类",
"collThirdParty": "false"
}
```
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"id": 2,
"modelOutputId": 101,
"serialNum": "20250119143125456",
"orgCode": "931000",
"runType": "1",
"custIsn": "CORP001",
"custType": "企业",
"guarType": "抵押",
"applyAmt": "1000000",
"custName": "某某科技有限公司",
"idType": "统一社会信用代码",
"createTime": "2025-01-19 14:31:25",
"createBy": "admin"
}
}
```
---
### 3. 查询利率定价流程列表
分页查询利率定价流程记录,支持多条件筛选。 分页查询利率定价流程记录,支持多条件筛选。
@@ -150,7 +209,7 @@ GET /loanPricing/workflow/list?pageNum=1&pageSize=10&custName=科技
--- ---
### 3. 查看利率定价流程详情 ### 4. 查看利率定价流程详情
根据业务方流水号查询流程的完整信息,包括模型输出字段。 根据业务方流水号查询流程的完整信息,包括模型输出字段。
@@ -202,6 +261,7 @@ GET /loanPricing/workflow/20250119143025123
"collType": "一类", "collType": "一类",
"collThirdParty": "false", "collThirdParty": "false",
"loanRate": "4.35", "loanRate": "4.35",
"executeRate": "4.20",
"custName": "某某科技有限公司", "custName": "某某科技有限公司",
"idType": "统一社会信用代码", "idType": "统一社会信用代码",
"isInclusiveFinance": "true", "isInclusiveFinance": "true",
@@ -325,6 +385,66 @@ GET /loanPricing/workflow/20250119143025123
--- ---
### 5. 设定执行利率
为利率定价流程设定或更新最终执行利率。
**接口地址:** `PUT /loanPricing/workflow/{serialNum}/executeRate`
**权限要求:** 无特殊权限要求
**路径参数:**
| 参数名 | 类型 | 必填 | 说明 |
|-----------|--------|----|--------|
| serialNum | String | 是 | 业务方流水号 |
**请求参数:**
| 参数名 | 类型 | 必填 | 说明 |
|-------------|--------|----|---------|
| executeRate | String | 是 | 执行利率(%) |
**请求示例:**
```bash
PUT /loanPricing/workflow/20250119143025123/executeRate
```
```json
{
"executeRate": "4.20"
}
```
**响应示例:**
成功 (200):
```json
{
"code": 200,
"msg": "操作成功"
}
```
失败 (记录不存在):
```json
{
"code": 500,
"msg": "设定失败"
}
```
**说明:**
- 该接口支持多次设定/更新执行利率
- 更新成功后会自动记录 `updateBy``updateTime`
- 如果指定的 `serialNum` 不存在,返回"设定失败"
---
## 错误码说明 ## 错误码说明
| 错误码 | 说明 | | 错误码 | 说明 |
@@ -379,12 +499,6 @@ GET /loanPricing/workflow/20250119143025123
| 一类 | 一类抵押 | | 一类 | 一类抵押 |
| 二类 | 二类抵押 | | 二类 | 二类抵押 |
### 运行模式 (runType)
| 值 | 说明 |
|----|------|
| 1 | 同步运行 |
--- ---
## 在线文档 ## 在线文档

15
doc/corp.csv Normal file
View File

@@ -0,0 +1,15 @@
中文名,字段名,备注
客户内码,custIsn,
客户类型,custType,企业
担保方式,guarType,"可选值:信用,保证,抵押,质押"
客户名称,custName,
证件类型,idType,
证件号码,idNum,
贸易和建筑业企业,,押类贸易和建筑业企业上调20BP
省农担担保贷款,isAgriGuar,省农担担保贷款下调40个BP
绿色贷款,isGreenLoan,绿色贷款或科技型企业最多下降5BP
科技型企业,isTechEnt,绿色贷款或科技型企业最多下降5BP
贷款期限,loanTerm,
申请金额,applyAmt,单位:元
抵质押类型,collType,
抵质押物是否三方所有,collThirdParty,
1 中文名 字段名 备注
2 客户内码 custIsn
3 客户类型 custType 企业
4 担保方式 guarType 可选值:信用,保证,抵押,质押
5 客户名称 custName
6 证件类型 idType
7 证件号码 idNum
8 贸易和建筑业企业 抵(质)押类:贸易和建筑业企业上调20BP
9 省农担担保贷款 isAgriGuar 省农担担保贷款下调40个BP
10 绿色贷款 isGreenLoan 绿色贷款或科技型企业最多下降5BP
11 科技型企业 isTechEnt 绿色贷款或科技型企业最多下降5BP
12 贷款期限 loanTerm
13 申请金额 applyAmt 单位:元
14 抵质押类型 collType
15 抵质押物是否三方所有 collThirdParty

View File

@@ -0,0 +1,127 @@
# 字段匹配分析报告
## 概述
本报告对比了 `person.csv``corp.csv` 中定义的字段与数据库表 `loan_pricing_workflow` 的实际字段,识别出需要添加的字段。
---
## 个人客户字段对比
| CSV字段名 | 中文名 | CSV要求 | 数据库字段 | 匹配状态 |
|----------------|------------|--------------------|------------------|---------|
| custIsn | 客户内码 | 必填 | cust_isn | ✅ 匹配 |
| custType | 客户类型 | 固定值"个人" | cust_type | ✅ 匹配 |
| guarType | 担保方式 | 必填,可选值:信用/保证/抵押/质押 | guar_type | ✅ 匹配 |
| custName | 客户名称 | 可选 | cust_name | ✅ 匹配 |
| idType | 证件类型 | 可选 | id_type | ✅ 匹配 |
| idNum | 证件号码 | 可选 | **❌ 缺失** | ⚠️ 需要添加 |
| applyAmt | 申请金额 | 必填,单位:元 | apply_amt | ✅ 匹配 |
| bizProof | 是否有经营佐证 | 可选 | biz_proof | ✅ 匹配 |
| loanLoop | 循环功能 | 可选 | **❌ 缺失** | ⚠️ 需要添加 |
| collType | 抵质押类型 | 可选 | coll_type | ✅ 匹配 |
| collThirdParty | 抵质押物是否三方所有 | 可选 | coll_third_party | ✅ 匹配 |
### 个人客户缺失字段
| 字段名 | 中文名 | 类型 | 说明 |
|-----------|------|--------------|-------------------------|
| id_num | 证件号码 | varchar(100) | 存储个人身份证号或其他证件号码 |
| loan_loop | 循环功能 | varchar(10) | 贷款合同是否开通循环功能true/false |
---
## 企业客户字段对比
| CSV字段名 | 中文名 | CSV要求 | 数据库字段 | 匹配状态 |
|----------------|------------|-----------------------|------------------|---------|
| custIsn | 客户内码 | 必填 | cust_isn | ✅ 匹配 |
| custType | 客户类型 | 固定值"企业" | cust_type | ✅ 匹配 |
| guarType | 担保方式 | 必填,可选值:信用/保证/抵押/质押 | guar_type | ✅ 匹配 |
| custName | 客户名称 | 可选 | cust_name | ✅ 匹配 |
| idType | 证件类型 | 可选 | id_type | ✅ 匹配 |
| idNum | 证件号码 | 可选 | **❌ 缺失** | ⚠️ 需要添加 |
| (无字段名) | 贸易和建筑业企业 | 抵押类贸易和建筑业企业上调20BP | **❌ 缺失** | ⚠️ 需要添加 |
| isAgriGuar | 省农担担保贷款 | 省农担担保贷款下调40个BP | is_agri_guar | ✅ 匹配 |
| isGreenLoan | 绿色贷款 | 绿色贷款或科技型企业最多下降5BP | **❌ 缺失** | ⚠️ 需要添加 |
| isTechEnt | 科技型企业 | 绿色贷款或科技型企业最多下降5BP | **❌ 缺失** | ⚠️ 需要添加 |
| loanTerm | 贷款期限 | 可选 | **❌ 缺失** | ⚠️ 需要添加 |
| applyAmt | 申请金额 | 必填,单位:元 | apply_amt | ✅ 匹配 |
| collType | 抵质押类型 | 可选 | coll_type | ✅ 匹配 |
| collThirdParty | 抵质押物是否三方所有 | 可选 | coll_third_party | ✅ 匹配 |
### 企业客户缺失字段
| 字段名 | 中文名 | 类型 | 说明 |
|-----------------------|------------|--------------|----------------------------------|
| id_num | 证件号码 | varchar(100) | 存储企业统一社会信用代码或其他证件号码 |
| is_trade_construction | 贸易和建筑业企业标识 | varchar(10) | 抵押类贸易和建筑业企业上调20BPtrue/false |
| is_green_loan | 绿色贷款 | varchar(10) | 绿色贷款标识true/false |
| is_tech_ent | 科技型企业 | varchar(10) | 科技型企业标识true/false |
| loan_term | 贷款期限 | varchar(50) | 贷款期限,单位:月/年 |
---
## 数据库变更 SQL
```sql
-- 添加缺失的字段到 loan_pricing_workflow 表
-- 个人和企业共同需要的字段
ALTER TABLE `loan_pricing_workflow` ADD COLUMN `id_num` varchar(100) DEFAULT NULL COMMENT '证件号码' AFTER `id_type`;
-- 个人客户专用字段
ALTER TABLE `loan_pricing_workflow` ADD COLUMN `loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false' AFTER `biz_proof`;
-- 企业客户专用字段
ALTER TABLE `loan_pricing_workflow` ADD COLUMN `is_trade_construction` varchar(10) DEFAULT NULL COMMENT '贸易和建筑业企业标识: true/false抵质押类上调20BP' AFTER `is_agri_guar`;
ALTER TABLE `loan_pricing_workflow` ADD COLUMN `is_green_loan` varchar(10) DEFAULT NULL COMMENT '绿色贷款: true/false' AFTER `is_agri_guar`;
ALTER TABLE `loan_pricing_workflow` ADD COLUMN `is_tech_ent` varchar(10) DEFAULT NULL COMMENT '科技型企业: true/false' AFTER `is_agri_guar`;
ALTER TABLE `loan_pricing_workflow` ADD COLUMN `loan_term` varchar(50) DEFAULT NULL COMMENT '贷款期限' AFTER `apply_amt`;
```
---
## Entity 类更新
`LoanPricingWorkflow.java` 需要添加以下属性:
```java
/** 证件号码 */
private String idNum;
/** 循环功能: true/false */
private String loanLoop;
/** 贸易和建筑业企业标识: true/false */
private String isTradeConstruction;
/** 绿色贷款: true/false */
private String isGreenLoan;
/** 科技型企业: true/false */
private String isTechEnt;
/** 贷款期限 */
private String loanTerm;
```
---
## 建议行动
1. **立即执行**:添加缺失的数据库字段
2. **更新 Entity**:在 `LoanPricingWorkflow` 实体类中添加对应的属性
3. **更新 Proposal**:在 `split-pricing-creation-interface` 的 tasks.md 中添加数据库变更任务
4. **测试验证**:确保新字段可以正确存储和检索数据
---
## 总结
- **个人客户缺失字段**2 个id_num, loan_loop
- **企业客户缺失字段**5 个id_num, is_trade_construction, is_green_loan, is_tech_ent, loan_term
- **共同缺失字段**1 个id_num
- **总计需要添加**6 个新字段
数据库和实体类需要同步更新,才能支持新的个人和企业发起接口。

View File

@@ -0,0 +1,161 @@
# 利率定价接口拆分实施报告
## 实施摘要
本次实施成功将原有的统一利率定价发起接口拆分为两个独立的接口:
- **个人客户发起接口**: `POST /loanPricing/workflow/create/personal`
- **企业客户发起接口**: `POST /loanPricing/workflow/create/corporate`
## 已完成的任务
### ✅ 数据库变更
- **文件**: `sql/add_missing_fields.sql`
- **新增字段**:
- `id_num` (varchar(100)) - 证件号码
- `loan_loop` (varchar(10)) - 循环功能(个人专用)
- `is_trade_construction` (varchar(10)) - 贸易和建筑业企业标识(企业专用)
- `is_green_loan` (varchar(10)) - 绿色贷款(企业专用)
- `is_tech_ent` (varchar(10)) - 科技型企业(企业专用)
- `loan_term` (varchar(50)) - 贷款期限(企业专用)
> **注意**: SQL 脚本已创建,需手动执行到数据库
### ✅ Entity 类更新
- **文件**: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
- 添加了 6 个新属性
### ✅ DTO 类创建
1. **PersonalLoanPricingCreateDTO.java** - 个人客户发起 DTO
- 包含个人特有字段bizProof、loanLoop
- 字段验证custIsn、guarType、applyAmt 必填
- 担保方式枚举验证
2. **CorporateLoanPricingCreateDTO.java** - 企业客户发起 DTO
- 包含企业特有字段isAgriGuar、isGreenLoan、isTechEnt、isTradeConstruction、loanTerm
- 字段验证custIsn、guarType、applyAmt 必填
- 担保方式枚举验证
### ✅ 转换器工具类
- **文件**: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java`
- 实现 DTO 到 Entity 的转换
- 自动设置 custType 为"个人"或"企业"
### ✅ Service 层更新
- **接口**: `ILoanPricingWorkflowService.java`
- 新增: `createPersonalLoanPricing(PersonalLoanPricingCreateDTO dto)`
- 新增: `createCorporateLoanPricing(CorporateLoanPricingCreateDTO dto)`
- **实现**: `LoanPricingWorkflowServiceImpl.java`
- 实现了两个新方法,复用现有的 createLoanPricing 逻辑
### ✅ Controller 层更新
- **文件**: `LoanPricingWorkflowController.java`
- 新增接口:
- `POST /loanPricing/workflow/create/personal`
- `POST /loanPricing/workflow/create/corporate`
- 保留了原有接口,保持向后兼容
### ✅ HTTP 测试脚本
1. **test_personal_create.http** - 个人客户接口测试
- 成功场景测试
- 3 个缺少必填字段的失败场景
- 担保方式枚举验证测试
- 抵质押信息测试
- 最小必填字段测试
2. **test_corporate_create.http** - 企业客户接口测试
- 成功场景测试
- 3 个缺少必填字段的失败场景
- 担保方式枚举验证测试
- 企业特有字段测试(省农担、绿色贷款、科技企业、贸易建筑)
- 最小必填字段测试
3. **test_backward_compatibility.http** - 向后兼容性测试
- 原有接口个人客户测试
- 原有接口企业客户测试
## 待完成任务
### ⏳ 需要手动执行的步骤
1. **执行数据库迁移脚本**
```bash
# 在 MySQL 中执行
source sql/add_missing_fields.sql
```
2. **执行 HTTP 测试脚本**
- 使用 IntelliJ IDEA 打开 `test_api/` 目录下的 .http 文件
- 逐一执行测试用例
- 验证返回结果
3. **启动应用并验证**
```bash
# 启动后端应用
mvn spring-boot:run
```
- 访问 Swagger UI: `http://localhost:8080/swagger-ui.html`
- 验证新接口文档显示正确
## 接口使用说明
### 个人客户发起接口
**端点**: `POST /loanPricing/workflow/create/personal`
**请求示例**:
```json
{
"custIsn": "TEST001",
"custName": "张三",
"idType": "身份证",
"idNum": "110101199001011234",
"guarType": "信用",
"applyAmt": "500000",
"bizProof": "true",
"loanLoop": "false"
}
```
### 企业客户发起接口
**端点**: `POST /loanPricing/workflow/create/corporate`
**请求示例**:
```json
{
"custIsn": "CORP001",
"custName": "测试科技有限公司",
"idType": "统一社会信用代码",
"idNum": "91110000100000000X",
"guarType": "抵押",
"applyAmt": "1000000",
"loanTerm": "36",
"isAgriGuar": "false",
"isGreenLoan": "true",
"isTechEnt": "true"
}
```
## 验证清单
- [x] 数据库迁移脚本已创建
- [x] Entity 类已更新
- [x] DTO 类已创建
- [x] 转换器已创建
- [x] Service 接口和实现已更新
- [x] Controller 接口已添加
- [x] HTTP 测试脚本已生成
- [ ] 数据库迁移脚本已执行
- [ ] 测试脚本已执行并通过
- [ ] Swagger 文档已验证

12
doc/person.csv Normal file
View File

@@ -0,0 +1,12 @@
中文名,字段名,备注
客户内码,custIsn,
客户类型,custType,个人
担保方式,guarType,"可选值:信用,保证,抵押,质押"
客户名称,custName,
证件类型,idType,
证件号码,idNum,
申请金额,applyAmt,单位:元
是否有经营佐证,bizProof,个人经营性贷款提供的经营佐证包括:借款人或其配偶为法定代表人、实际经营者、股东的企业(个体工商户)营业执照或企查查、企信宝查档资料。
循环功能,loanLoop,贷款合同是否开通循环功能
抵质押类型,collType,
抵质押物是否三方所有,collThirdParty,
1 中文名 字段名 备注
2 客户内码 custIsn
3 客户类型 custType 个人
4 担保方式 guarType 可选值:信用,保证,抵押,质押
5 客户名称 custName
6 证件类型 idType
7 证件号码 idNum
8 申请金额 applyAmt 单位:元
9 是否有经营佐证 bizProof 个人经营性贷款提供的经营佐证包括:借款人或其配偶为法定代表人、实际经营者、股东的企业(个体工商户)营业执照或企查查、企信宝查档资料。
10 循环功能 loanLoop 贷款合同是否开通循环功能
11 抵质押类型 collType
12 抵质押物是否三方所有 collThirdParty

View File

@@ -0,0 +1,854 @@
# 根据客户类型动态展示流程详情实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 创建两个独立的详情组件(个人客户和企业客户),根据客户类型动态渲染不同的字段展示。
**架构:** 将现有的单一详情页面拆分为两个独立组件PersonalWorkflowDetail.vue 和 CorporateWorkflowDetail.vue父组件
detail.vue 作为容器负责数据获取和根据 custType 动态分发渲染。
**技术栈:** Vue 2.6.12, Element UI 2.15.14, Vue Router 3.4.9
---
## Task 1: 创建个人客户详情组件 PersonalWorkflowDetail.vue
**文件:**
- 创建: `ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue`
**Step 1: 创建组件基础结构**
```vue
<template>
<div class="personal-workflow-detail" v-loading="loading">
<!-- 两栏布局左侧关键信息 + 右侧流程详情+模型输出 -->
<div v-if="!loading && detailData" class="detail-layout">
<!-- 左侧关键信息卡片 -->
<div class="left-panel">
<el-card class="summary-card">
<div slot="header" class="card-header">
<span class="card-title">关键信息</span>
</div>
<el-descriptions :column="1" direction="vertical" border>
<el-descriptions-item label="业务方流水号">{{ detailData.serialNum }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ detailData.custName }}</el-descriptions-item>
<el-descriptions-item label="客户类型">{{ detailData.custType }}</el-descriptions-item>
<el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
<el-descriptions-item label="基准利率">
<span class="rate-value">{{ getBaseLoanRate() }}</span> %
</el-descriptions-item>
<el-descriptions-item label="浮动BP">
<span class="total-bp-value">{{ getTotalBp() }}</span>
</el-descriptions-item>
<el-descriptions-item label="测算利率">
<span class="calculate-rate">{{ getCalculateRate() }}</span> %
</el-descriptions-item>
<el-descriptions-item label="执行利率">
<div class="execute-rate-input-wrapper">
<el-input
v-model="executeRateInput"
class="execute-rate-input"
placeholder="请输入执行利率"
>
<template slot="append">%</template>
</el-input>
<el-button
type="primary"
size="small"
@click="handleSetExecuteRate"
>
确定
</el-button>
</div>
</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
<!-- 右侧面板 -->
<div class="right-panel">
<!-- 流程详情卡片 -->
<el-card class="detail-card">
<div slot="header" class="card-header">
<span class="card-title">流程详情</span>
</div>
<!-- 基本信息组 -->
<div class="info-section">
<h4 class="section-title">基本信息</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="机构编码">{{ detailData.orgCode }}</el-descriptions-item>
<el-descriptions-item label="运行模式">{{ detailData.runType }}</el-descriptions-item>
<el-descriptions-item label="客户内码">{{ detailData.custIsn }}</el-descriptions-item>
<el-descriptions-item label="证件类型">{{ detailData.idType }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ detailData.idNum }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailData.createTime }}</el-descriptions-item>
<el-descriptions-item label="创建者">{{ detailData.createBy }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 业务信息组 -->
<div class="info-section">
<h4 class="section-title">业务信息</h4>
<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.bizProof) }}</el-descriptions-item>
<el-descriptions-item label="循环功能">{{ formatBoolean(detailData.loanLoop) }}</el-descriptions-item>
<el-descriptions-item label="抵质押类型">{{ detailData.collType || '-' }}</el-descriptions-item>
<el-descriptions-item label="抵质押物是否三方所有">{{ formatBoolean(detailData.collThirdParty) }}</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
<!-- 模型输出卡片 -->
<ModelOutputDisplay
:cust-type="detailData.custType"
:retail-output="retailOutput"
:corp-output="null"
/>
<!-- 议价池卡片 -->
<BargainingPoolDisplay
v-if="bargainingPool"
:branch-pool="bargainingPool.branchPool"
:sub-branch-pool="bargainingPool.subBranchPool"
:private-domain-pool="bargainingPool.privateDomainPool"
:excess-profit-share="bargainingPool.excessProfitShare"
/>
</div>
</div>
</div>
</template>
<script>
import { setExecuteRate } from "@/api/loanPricing/workflow"
import ModelOutputDisplay from "./ModelOutputDisplay.vue"
import BargainingPoolDisplay from "./BargainingPoolDisplay.vue"
export default {
name: "PersonalWorkflowDetail",
components: {
ModelOutputDisplay,
BargainingPoolDisplay
},
props: {
detailData: {
type: Object,
required: true
},
retailOutput: {
type: Object,
default: null
},
bargainingPool: {
type: Object,
default: null
}
},
data() {
return {
loading: false,
executeRateInput: ''
}
},
watch: {
'detailData.executeRate': {
handler(newVal) {
this.executeRateInput = newVal || ''
},
immediate: true
}
},
methods: {
/** 格式化布尔值为中文 */
formatBoolean(value) {
if (value === 'true' || value === true) return '是'
if (value === 'false' || value === false) return '否'
return value || '-'
},
/** 获取基准利率 */
getBaseLoanRate() {
return this.retailOutput?.baseLoanRate || '-'
},
/** 获取浮动BP */
getTotalBp() {
return this.retailOutput?.totalBp || '-'
},
/** 获取测算利率 */
getCalculateRate() {
return this.retailOutput?.calculateRate || '-'
},
/** 设定执行利率 */
handleSetExecuteRate() {
const value = this.executeRateInput
if (value === null || value === undefined || value === '') {
this.$modal.msgError("请输入执行利率")
return
}
const numValue = parseFloat(value)
if (isNaN(numValue)) {
this.$modal.msgError("请输入有效的数字")
return
}
if (numValue < 0 || numValue > 100) {
this.$modal.msgError("执行利率必须在 0 到 100 之间")
return
}
this.loading = true
setExecuteRate(this.detailData.serialNum, value.toString()).then(() => {
this.$modal.msgSuccess("执行利率设定成功")
this.$emit('refresh')
this.loading = false
}).catch(error => {
this.$modal.msgError("设定失败:" + (error.msg || error.message || "未知错误"))
this.loading = false
})
}
}
}
</script>
<style lang="scss" scoped>
.personal-workflow-detail {
.detail-layout {
display: flex;
gap: 20px;
align-items: flex-start;
.left-panel {
flex: 0 0 280px;
max-width: 280px;
.summary-card {
::v-deep .el-card__header {
padding: 16px 20px;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
}
.card-header {
display: flex;
align-items: center;
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
::v-deep .el-card__body {
padding: 16px;
}
.execute-rate-input-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
.execute-rate-input {
width: 100%;
}
}
.rate-value {
color: #67c23a;
font-weight: 500;
}
.total-bp-value {
color: #e6a23c;
font-weight: 600;
}
.calculate-rate {
color: #f56c6c;
font-weight: 600;
font-size: 16px;
}
}
}
.right-panel {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 20px;
.detail-card {
::v-deep .el-card__header {
padding: 16px 20px;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
}
.card-header {
display: flex;
align-items: center;
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
::v-deep .el-card__body {
padding: 20px;
}
.info-section {
&:not(:last-child) {
margin-bottom: 24px;
}
.section-title {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 500;
color: #606266;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
}
}
}
}
}
@media screen and (max-width: 992px) {
.personal-workflow-detail {
.detail-layout {
flex-direction: column;
.left-panel,
.right-panel {
flex: 1 1 100%;
max-width: 100%;
}
}
}
}
</style>
```
**Step 2: 验证组件语法正确**
检查: 在浏览器开发工具中确认无语法错误
---
## Task 2: 创建企业客户详情组件 CorporateWorkflowDetail.vue
**文件:**
- 创建: `ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue`
**Step 1: 创建组件基础结构**
```vue
<template>
<div class="corporate-workflow-detail" v-loading="loading">
<!-- 两栏布局左侧关键信息 + 右侧流程详情+模型输出 -->
<div v-if="!loading && detailData" class="detail-layout">
<!-- 左侧关键信息卡片 -->
<div class="left-panel">
<el-card class="summary-card">
<div slot="header" class="card-header">
<span class="card-title">关键信息</span>
</div>
<el-descriptions :column="1" direction="vertical" border>
<el-descriptions-item label="业务方流水号">{{ detailData.serialNum }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ detailData.custName }}</el-descriptions-item>
<el-descriptions-item label="客户类型">{{ detailData.custType }}</el-descriptions-item>
<el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
<el-descriptions-item label="基准利率">
<span class="rate-value">{{ getBaseLoanRate() }}</span> %
</el-descriptions-item>
<el-descriptions-item label="浮动BP">
<span class="total-bp-value">{{ getTotalBp() }}</span>
</el-descriptions-item>
<el-descriptions-item label="测算利率">
<span class="calculate-rate">{{ getCalculateRate() }}</span> %
</el-descriptions-item>
<el-descriptions-item label="执行利率">
<div class="execute-rate-input-wrapper">
<el-input
v-model="executeRateInput"
class="execute-rate-input"
placeholder="请输入执行利率"
>
<template slot="append">%</template>
</el-input>
<el-button
type="primary"
size="small"
@click="handleSetExecuteRate"
>
确定
</el-button>
</div>
</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
<!-- 右侧面板 -->
<div class="right-panel">
<!-- 流程详情卡片 -->
<el-card class="detail-card">
<div slot="header" class="card-header">
<span class="card-title">流程详情</span>
</div>
<!-- 基本信息组 -->
<div class="info-section">
<h4 class="section-title">基本信息</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="机构编码">{{ detailData.orgCode }}</el-descriptions-item>
<el-descriptions-item label="运行模式">{{ detailData.runType }}</el-descriptions-item>
<el-descriptions-item label="客户内码">{{ detailData.custIsn }}</el-descriptions-item>
<el-descriptions-item label="证件类型">{{ detailData.idType }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ detailData.idNum }}</el-descriptions-item>
<el-descriptions-item label="贷款期限">{{ detailData.loanTerm || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailData.createTime }}</el-descriptions-item>
<el-descriptions-item label="创建者">{{ detailData.createBy }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 业务信息组 -->
<div class="info-section">
<h4 class="section-title">业务信息</h4>
<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="绿色贷款">{{ formatBoolean(detailData.isGreenLoan) }}</el-descriptions-item>
<el-descriptions-item label="科技型企业">{{ formatBoolean(detailData.isTechEnt) }}</el-descriptions-item>
<el-descriptions-item label="抵质押类型">{{ detailData.collType || '-' }}</el-descriptions-item>
<el-descriptions-item label="抵质押物是否三方所有">{{ formatBoolean(detailData.collThirdParty) }}</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
<!-- 模型输出卡片 -->
<ModelOutputDisplay
:cust-type="detailData.custType"
:retail-output="null"
:corp-output="corpOutput"
/>
<!-- 议价池卡片 -->
<BargainingPoolDisplay
v-if="bargainingPool"
:branch-pool="bargainingPool.branchPool"
:sub-branch-pool="bargainingPool.subBranchPool"
:private-domain-pool="bargainingPool.privateDomainPool"
:excess-profit-share="bargainingPool.excessProfitShare"
/>
</div>
</div>
</div>
</template>
<script>
import { setExecuteRate } from "@/api/loanPricing/workflow"
import ModelOutputDisplay from "./ModelOutputDisplay.vue"
import BargainingPoolDisplay from "./BargainingPoolDisplay.vue"
export default {
name: "CorporateWorkflowDetail",
components: {
ModelOutputDisplay,
BargainingPoolDisplay
},
props: {
detailData: {
type: Object,
required: true
},
corpOutput: {
type: Object,
default: null
},
bargainingPool: {
type: Object,
default: null
}
},
data() {
return {
loading: false,
executeRateInput: ''
}
},
watch: {
'detailData.executeRate': {
handler(newVal) {
this.executeRateInput = newVal || ''
},
immediate: true
}
},
methods: {
/** 格式化布尔值为中文 */
formatBoolean(value) {
if (value === 'true' || value === true) return '是'
if (value === 'false' || value === false) return '否'
return value || '-'
},
/** 获取基准利率 */
getBaseLoanRate() {
return this.corpOutput?.baseLoanRate || '-'
},
/** 获取浮动BP */
getTotalBp() {
return this.corpOutput?.totalBp || '-'
},
/** 获取测算利率 */
getCalculateRate() {
return this.corpOutput?.calculateRate || '-'
},
/** 设定执行利率 */
handleSetExecuteRate() {
const value = this.executeRateInput
if (value === null || value === undefined || value === '') {
this.$modal.msgError("请输入执行利率")
return
}
const numValue = parseFloat(value)
if (isNaN(numValue)) {
this.$modal.msgError("请输入有效的数字")
return
}
if (numValue < 0 || numValue > 100) {
this.$modal.msgError("执行利率必须在 0 到 100 之间")
return
}
this.loading = true
setExecuteRate(this.detailData.serialNum, value.toString()).then(() => {
this.$modal.msgSuccess("执行利率设定成功")
this.$emit('refresh')
this.loading = false
}).catch(error => {
this.$modal.msgError("设定失败:" + (error.msg || error.message || "未知错误"))
this.loading = false
})
}
}
}
</script>
<style lang="scss" scoped>
.corporate-workflow-detail {
.detail-layout {
display: flex;
gap: 20px;
align-items: flex-start;
.left-panel {
flex: 0 0 280px;
max-width: 280px;
.summary-card {
::v-deep .el-card__header {
padding: 16px 20px;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
}
.card-header {
display: flex;
align-items: center;
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
::v-deep .el-card__body {
padding: 16px;
}
.execute-rate-input-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
.execute-rate-input {
width: 100%;
}
}
.rate-value {
color: #67c23a;
font-weight: 500;
}
.total-bp-value {
color: #e6a23c;
font-weight: 600;
}
.calculate-rate {
color: #f56c6c;
font-weight: 600;
font-size: 16px;
}
}
}
.right-panel {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 20px;
.detail-card {
::v-deep .el-card__header {
padding: 16px 20px;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
}
.card-header {
display: flex;
align-items: center;
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
::v-deep .el-card__body {
padding: 20px;
}
.info-section {
&:not(:last-child) {
margin-bottom: 24px;
}
.section-title {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 500;
color: #606266;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
}
}
}
}
}
@media screen and (max-width: 992px) {
.corporate-workflow-detail {
.detail-layout {
flex-direction: column;
.left-panel,
.right-panel {
flex: 1 1 100%;
max-width: 100%;
}
}
}
}
</style>
```
**Step 2: 验证组件语法正确**
检查: 在浏览器开发工具中确认无语法错误
---
## Task 3: 修改 detail.vue 为容器组件
**文件:**
- 修改: `ruoyi-ui/src/views/loanPricing/workflow/detail.vue`
**Step 1: 替换为容器组件结构**
将整个文件内容替换为:
```vue
<template>
<div class="app-container workflow-detail-container" v-loading="loading">
<!-- 页面头部标题和返回按钮 -->
<div class="page-header">
<h2 class="page-title">流程详情</h2>
<el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button>
</div>
<!-- 根据客户类型渲染对应的详情组件 -->
<personal-workflow-detail
v-if="!loading && workflowDetail && workflowDetail.custType === '个人'"
:detail-data="workflowDetail"
:retail-output="retailOutput"
:bargaining-pool="bargainingPool"
@refresh="getDetail"
/>
<corporate-workflow-detail
v-if="!loading && workflowDetail && workflowDetail.custType === '企业'"
:detail-data="workflowDetail"
:corp-output="corpOutput"
:bargaining-pool="bargainingPool"
@refresh="getDetail"
/>
</div>
</template>
<script>
import { getWorkflow } from "@/api/loanPricing/workflow"
import PersonalWorkflowDetail from "./components/PersonalWorkflowDetail.vue"
import CorporateWorkflowDetail from "./components/CorporateWorkflowDetail.vue"
export default {
name: "LoanPricingWorkflowDetail",
components: {
PersonalWorkflowDetail,
CorporateWorkflowDetail
},
data() {
return {
loading: true,
workflowDetail: null,
retailOutput: null,
corpOutput: null,
bargainingPool: null
}
},
created() {
this.getDetail()
},
methods: {
/** 获取流程详情 */
getDetail() {
const serialNum = this.$route.params.serialNum
if (!serialNum) {
this.$modal.msgError("缺少业务方流水号参数")
this.goBack()
return
}
getWorkflow(serialNum).then(response => {
this.workflowDetail = response.data.loanPricingWorkflow
this.retailOutput = response.data.modelRetailOutputFields
this.corpOutput = response.data.modelCorpOutputFields
this.bargainingPool = response.data.bargainingPool
this.loading = false
}).catch(error => {
this.$modal.msgError("获取流程详情失败:" + (error.message || "未知错误"))
this.loading = false
})
},
/** 返回上一页 */
goBack() {
this.$router.go(-1)
}
}
}
</script>
<style lang="scss" scoped>
.workflow-detail-container {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 0 4px;
.page-title {
margin: 0;
font-size: 20px;
font-weight: 500;
color: #303133;
}
}
}
</style>
```
**Step 2: 验证修改正确**
检查: 在浏览器中访问详情页,确认根据客户类型正确渲染对应组件
---
## Task 4: 测试验证
**Step 1: 测试个人客户详情页**
1. 在列表页点击个人客户记录的"查看"按钮
2. 确认页面正确显示 PersonalWorkflowDetail 组件
3. 确认所有个人客户字段正确显示
4. 确认模型输出正确展示retailOutput
5. 测试执行利率设定功能
**Step 2: 测试企业客户详情页**
1. 在列表页点击企业客户记录的"查看"按钮
2. 确认页面正确显示 CorporateWorkflowDetail 组件
3. 确认所有企业客户字段正确显示
4. 确认模型输出正确展示corpOutput
5. 测试执行利率设定功能
**Step 3: 响应式布局测试**
1. 调整浏览器窗口宽度到小于992px
2. 确认布局自动切换为单列垂直布局
3. 确认所有内容正常显示
---
## Task 5: 提交代码
**Step 1: 添加文件到 Git**
```bash
git add ruoyi-ui/src/views/loanPricing/workflow/components/PersonalWorkflowDetail.vue
git add ruoyi-ui/src/views/loanPricing/workflow/components/CorporateWorkflowDetail.vue
git add ruoyi-ui/src/views/loanPricing/workflow/detail.vue
```
**Step 2: 提交变更**
```bash
git commit -m "feat: 根据客户类型动态展示流程详情
- 创建 PersonalWorkflowDetail.vue 个人客户详情组件
- 创建 CorporateWorkflowDetail.vue 企业客户详情组件
- 修改 detail.vue 为容器组件,根据 custType 动态渲染
- 个人客户展示:基本信息(客户内码、证件信息)+ 业务信息(担保方式、经营佐证、循环功能等)
- 企业客户展示:基本信息(客户内码、证件信息、贷款期限)+ 业务信息(省农担担保、绿色贷款、科技型企业等)
- 两个组件完全独立,各自实现格式化和计算方法
- 支持响应式布局,小屏幕自动切换为单列布局"
```
---
## 相关文档
- 字段定义参考: `doc/person.csv``doc/corp.csv`
- API 文档: `doc/api/loan-pricing-workflow-api.md`
- 原详情页面: `ruoyi-ui/src/views/loanPricing/workflow/detail.vue` (修改前)

View File

@@ -0,0 +1,216 @@
# 利率定价创建流程前端重构设计文档
## 设计概述
本文档描述利率定价创建流程的前端重构设计,将原有的单一表单拆分为个人客户和企业客户两个独立的创建流程。
## 设计日期
2026-02-02
---
## 一、交互流程
### 用户操作步骤
1. **点击「新增」按钮** → 弹出「选择客户类型」对话框
2. **选择客户类型** → 打开对应的创建表单对话框
3. **填写表单并提交** → 调用对应的 API 接口
4. **成功/失败处理**
- 成功:关闭对话框,刷新列表,显示成功提示
- 失败:保留表单数据,显示后端错误信息
---
## 二、组件结构
### 文件组织
```
ruoyi-ui/src/
├── views/loanPricing/workflow/
│ ├── index.vue (修改)
│ └── components/
│ ├── CustomerTypeSelector.vue (新增)
│ ├── WorkflowCreateDialog.vue (删除)
│ ├── PersonalCreateDialog.vue (新增)
│ └── CorporateCreateDialog.vue (新增)
└── api/loanPricing/
└── workflow.js (修改)
```
### 组件职责
| 组件 | 职责 |
|-----------------------|-----------|
| CustomerTypeSelector | 客户类型选择对话框 |
| PersonalCreateDialog | 个人客户创建表单 |
| CorporateCreateDialog | 企业客户创建表单 |
---
## 三、表单字段设计
### 个人客户表单字段
| 分组 | 字段 | 类型 | 必填 | 说明 |
|-------|----------------|--------|----|------------|
| 基本信息 | custIsn | String | 是 | 客户内码 |
| 基本信息 | custName | String | 是 | 客户名称 |
| 基本信息 | idType | String | 是 | 证件类型 |
| 基本信息 | idNum | String | 是 | 证件号码 |
| 贷款信息 | guarType | String | 是 | 担保方式 |
| 贷款信息 | applyAmt | String | 是 | 申请金额(元) |
| 贷款信息 | bizProof | String | 是 | 是否有经营佐证 |
| 贷款信息 | loanLoop | String | 是 | 循环功能 |
| 抵质押信息 | collType | String | 是 | 抵质押类型 |
| 抵质押信息 | collThirdParty | String | 是 | 抵质押物是否三方所有 |
### 企业客户表单字段
| 分组 | 字段 | 类型 | 必填 | 说明 |
|-------|---------------------|--------|----|------------|
| 基本信息 | custIsn | String | 是 | 客户内码 |
| 基本信息 | custName | String | 是 | 客户名称 |
| 基本信息 | idType | String | 是 | 证件类型 |
| 基本信息 | idNum | String | 是 | 证件号码 |
| 贷款信息 | guarType | String | 是 | 担保方式 |
| 贷款信息 | applyAmt | String | 是 | 申请金额(元) |
| 贷款信息 | loanTerm | String | 是 | 贷款期限(月) |
| 企业标识 | isAgriGuar | String | 是 | 省农担担保贷款 |
| 企业标识 | isGreenLoan | String | 是 | 绿色贷款 |
| 企业标识 | isTechEnt | String | 是 | 科技型企业 |
| 企业标识 | isTradeConstruction | String | 是 | 贸易和建筑业企业 |
| 抵质押信息 | collType | String | 是 | 抵质押类型 |
| 抵质押信息 | collThirdParty | String | 是 | 抵质押物是否三方所有 |
---
## 四、API 接口设计
### 新增接口函数
```javascript
// 创建个人客户利率定价流程
export function createPersonalWorkflow(data) {
return request({
url: '/loanPricing/workflow/create/personal',
method: 'post',
data: data
})
}
// 创建企业客户利率定价流程
export function createCorporateWorkflow(data) {
return request({
url: '/loanPricing/workflow/create/corporate',
method: 'post',
data: data
})
}
```
### 数据转换规则
开关字段在提交时需要转换为字符串 `"true"/"false"`
```javascript
bizProof: this.form.bizProof ? 'true' : 'false'
```
---
## 五、验证规则
### 个人客户验证规则
| 字段 | 验证规则 |
|----------------|------------------------|
| custIsn | 必填,长度 1-50 字符 |
| custName | 必填,长度 1-100 字符 |
| idType | 必填 |
| idNum | 必填,身份证格式 15/18 位 |
| guarType | 必填 |
| applyAmt | 必填,正数,最大值 999999999.99 |
| bizProof | 必填(开关值,默认 "false" |
| loanLoop | 必填(开关值,默认 "false" |
| collType | 必填 |
| collThirdParty | 必填(开关值,默认 "false" |
### 企业客户验证规则
| 字段 | 验证规则 |
|---------------------|------------------------|
| custIsn | 必填,长度 1-50 字符 |
| custName | 必填,长度 1-100 字符 |
| idType | 必填 |
| idNum | 必填,统一社会信用代码 18 位 |
| guarType | 必填 |
| applyAmt | 必填,正数,最大值 999999999.99 |
| loanTerm | 必填,正整数,范围 1-360 |
| isAgriGuar | 必填(开关值,默认 "false" |
| isGreenLoan | 必填(开关值,默认 "false" |
| isTechEnt | 必填(开关值,默认 "false" |
| isTradeConstruction | 必填(开关值,默认 "false" |
| collType | 必填 |
| collThirdParty | 必填(开关值,默认 "false" |
---
## 六、UI 样式规范
### 对话框规范
| 对话框类型 | 宽度 | 说明 |
|--------|-------|------|
| 客户类型选择 | 500px | 居中显示 |
| 表单对话框 | 900px | 两列布局 |
### 表单布局
- 分组:使用 `el-divider` 分隔,带标题
- 字段:两列布局(`el-col :span="12"`
- 必填标识:红色星号
- 按钮:右下角,「确定」(主按钮)、「取消」
### 响应式处理
- 当屏幕宽度 < 768px 时,表单字段改为单列布局
---
## 七、错误处理
| 场景 | 处理方式 |
|--------|--------------------|
| 前端验证失败 | 阻止提交,高亮错误字段 |
| 网络错误 | 显示网络错误提示,保留表单 |
| 后端业务错误 | 显示后端返回的 `msg`,保留表单 |
| 成功 | 关闭对话框,刷新列表,显示成功提示 |
---
## 八、数据字典
### 担保方式 (guarType)
| 值 | 说明 |
|----|------|
| 信用 | 信用贷款 |
| 保证 | 保证贷款 |
| 抵押 | 抵押贷款 |
| 质押 | 质押贷款 |
### 抵质押类型 (collType)
| 值 | 说明 |
|----|------|
| 一线 | 一线抵押 |
| 一类 | 一类抵押 |
| 二类 | 二类抵押 |
### 证件类型 (idType)
- 个人:身份证
- 企业:统一社会信用代码

View File

@@ -0,0 +1,45 @@
# 提案: 添加执行利率设定前端功能
## 背景
后端已经实现了执行利率设定接口 `PUT /loanPricing/workflow/{serialNum}/executeRate`,并且详情接口 `GET /loanPricing/workflow/{serialNum}` 已经返回 `executeRate` 字段。但前端缺少相应的 UI 交互功能,业务人员无法通过界面设定执行利率。
## 问题
1. 议价池组件中只显示议价池数据,没有执行利率输入框
2. 前端缺少调用设定执行利率接口的 API 方法
3. 用户无法通过界面设定或更新执行利率
## 提案概述
在流程详情页面的议价池组件中添加执行利率设定功能,允许业务人员输入执行利率并提交。
### 功能范围
1. **API 方法**
-`ruoyi-ui/src/api/loanPricing/workflow.js` 中添加 `setExecuteRate` 方法
2. **议价池组件更新**
-`BargainingPoolDisplay.vue` 中添加执行利率输入框和提交按钮
- 添加执行利率显示/编辑状态的切换
- 支持显示已设定的执行利率值
- 添加表单验证(利率格式)
3. **详情页面更新**
-`detail.vue` 中传递 `executeRate``serialNum` 给议价池组件
- 添加提交成功后刷新详情的处理
## 影响范围
- 前端 API: `ruoyi-ui/src/api/loanPricing/workflow.js`
- 前端组件: `ruoyi-ui/src/views/loanPricing/workflow/components/BargainingPoolDisplay.vue`
- 前端页面: `ruoyi-ui/src/views/loanPricing/workflow/detail.vue`
- 规范: `loan-pricing-workflow-ui` (添加新需求)
## 设计考虑
1. **UI 位置**: 在议价池组件中添加新行,保持与现有议价池数据显示的一致性
2. **输入验证**: 利率格式验证(数字,可含小数点,范围合理)
3. **状态管理**: 编辑/查看状态切换,提交成功后显示最新值
4. **用户反馈**: 提交成功/失败的提示消息
5. **权限控制**: 后端接口无需特殊权限,前端暂不添加权限控制

View File

@@ -0,0 +1,45 @@
## ADDED Requirements
### Requirement: 执行利率设定
系统 SHALL 在流程详情页面的议价池组件中提供执行利率设定功能,允许用户输入并提交执行利率。
#### Scenario: 显示未设定的执行利率
- **WHEN** 用户在流程详情页面查看议价池组件,且该流程尚未设定执行利率
- **THEN** 系统在议价池组件中显示"执行利率"行,当前值显示为"-"
#### Scenario: 显示已设定的执行利率
- **WHEN** 用户在流程详情页面查看议价池组件,且该流程已设定执行利率
- **THEN** 系统在议价池组件中显示"执行利率"行,显示当前设定的执行利率值
#### Scenario: 进入编辑模式
- **WHEN** 用户在议价池组件中点击"执行利率"行的编辑按钮
- **THEN** 系统切换到编辑模式,显示输入框(预填充当前值或空)、提交按钮和取消按钮
#### Scenario: 提交执行利率成功
- **WHEN** 用户在编辑模式下输入有效的执行利率值并点击提交按钮
- **THEN** 系统调用 `PUT /loanPricing/workflow/{serialNum}/executeRate` 接口,成功后更新显示值为新设定的利率,显示成功提示消息,并退出编辑模式
#### Scenario: 提交执行利率失败
- **WHEN** 用户在编辑模式下提交执行利率,但后端接口返回错误
- **THEN** 系统保持编辑模式,显示错误提示消息
#### Scenario: 取消编辑
- **WHEN** 用户在编辑模式下点击取消按钮
- **THEN** 系统退出编辑模式,恢复显示模式,显示原来的执行利率值
#### Scenario: 输入验证
- **WHEN** 用户在编辑模式下输入非法的执行利率值(非数字、超出合理范围)
- **THEN** 系统在提交前进行验证,显示错误提示,阻止提交
#### Scenario: API 接口调用
- **WHEN** 用户提交执行利率
- **THEN** 前端调用 `setExecuteRate(serialNum, executeRate)` API 方法,该方法发送 `PUT /loanPricing/workflow/{serialNum}/executeRate` 请求

View File

@@ -0,0 +1,85 @@
# 实施任务
## 任务清单
1. **添加 API 方法**
- 文件: `ruoyi-ui/src/api/loanPricing/workflow.js`
- 操作: 添加 `setExecuteRate(serialNum, executeRate)` 方法
- 请求: `PUT /loanPricing/workflow/{serialNum}/executeRate`
- 验证: 方法添加成功
2. **更新议价池组件 - Props**
- 文件: `ruoyi-ui/src/views/loanPricing/workflow/components/BargainingPoolDisplay.vue`
- 操作: 添加 `executeRate``serialNum` props
- 验证: props 定义成功
3. **更新议价池组件 - UI**
- 文件: `ruoyi-ui/src/views/loanPricing/workflow/components/BargainingPoolDisplay.vue`
- 操作: 在 el-descriptions 中添加"执行利率"行,包含:
- 显示模式: 显示当前执行利率值(若无则显示"-"
- 编辑模式: 输入框 + 提交按钮 + 取消按钮
- 编辑按钮: 在显示模式下点击进入编辑模式
- 验证: UI 更新成功
4. **更新议价池组件 - 数据和逻辑**
- 文件: `ruoyi-ui/src/views/loanPricing/workflow/components/BargainingPoolDisplay.vue`
- 操作:
- 添加 `isEditing` 状态变量
- 添加 `tempExecuteRate` 临时输入值
- 添加 `handleEdit` 方法进入编辑模式
- 添加 `handleSubmit` 方法调用 API 并处理响应
- 添加 `handleCancel` 方法取消编辑
- 添加输入验证(数字格式、范围检查)
- 验证: 逻辑实现完整
5. **更新议价池组件 - API 调用**
- 文件: `ruoyi-ui/src/views/loanPricing/workflow/components/BargainingPoolDisplay.vue`
- 操作: 引入 `setExecuteRate` API 方法
- 验证: 引入成功
6. **更新详情页面 - 传递 Props**
- 文件: `ruoyi-ui/src/views/loanPricing/workflow/detail.vue`
- 操作: 在 BargainingPoolDisplay 组件上传递:
- `:execute-rate="workflowDetail.executeRate"`
- `:serial-num="workflowDetail.serialNum"`
- `@execute-rate-updated="handleExecuteRateUpdated"`
- 验证: props 传递正确
7. **前端功能验证**
- 操作:
- 启动前端服务
- 打开流程详情页面
- 测试首次设定执行利率
- 测试更新已设定的执行利率
- 测试输入验证
- 测试取消编辑
- 验证:
- 未设定时显示"-"
- 已设定时显示当前值
- 点击编辑按钮进入编辑模式
- 输入框显示当前值
- 提交成功后显示新值并显示成功提示
- 取消编辑恢复显示模式
- 输入非法值时显示错误提示
## 依赖关系
- 任务 1 必须首先执行API 方法)
- 任务 2-5 依次执行Props -> UI -> 数据逻辑 -> API 调用)
- 任务 6 依赖任务 2
- 任务 7 在所有代码任务完成后执行
## 验收标准
- [x] API 方法 `setExecuteRate` 添加成功
- [x] 议价池组件添加 `executeRate``serialNum` props
- [x] 议价池组件显示执行利率行
- [x] 未设定时显示"-"
- [x] 已设定时显示当前值
- [x] 编辑按钮切换到编辑模式
- [x] 输入框显示当前值
- [x] 提交按钮调用 API 成功
- [x] 提交成功后更新显示并显示成功提示
- [x] 取消按钮恢复显示模式
- [x] 输入验证正确工作
- [x] 详情页面正确传递 props

View File

@@ -0,0 +1,153 @@
# 设计文档: 执行利率设定接口
## 数据库设计
### 表结构变更
**表名**: `loan_pricing_workflow`
**新增字段**:
| 字段名 | 类型 | 可空 | 默认值 | 说明 |
|--------------|-------------|----|------|---------|
| execute_rate | varchar(20) | 是 | NULL | 执行利率(%) |
**DDL**:
```sql
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `execute_rate` varchar(20) DEFAULT NULL COMMENT '执行利率(%)' AFTER `loan_rate`;
```
## 接口设计
### 设定执行利率接口
**请求方式**: `PUT /loanPricing/workflow/{serialNum}/executeRate`
**权限要求**: 无特殊权限要求(与查询接口一致)
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|-----------|--------|----|--------|
| serialNum | String | 是 | 业务方流水号 |
**请求体**:
```json
{
"executeRate": "4.20"
}
```
**参数说明**:
| 参数名 | 类型 | 必填 | 说明 |
|-------------|--------|----|----------------------|
| executeRate | String | 是 | 执行利率,字符串格式(如 "4.20" |
**响应示例**:
成功 (200):
```json
{
"code": 200,
"msg": "设定成功"
}
```
失败 (404):
```json
{
"code": 404,
"msg": "记录不存在"
}
```
失败 (500):
```json
{
"code": 500,
"msg": "设定失败"
}
```
## 代码设计
### Entity 层
**LoanPricingWorkflow.java**
```java
/** 执行利率(%) */
private String executeRate;
```
### Service 层
**ILoanPricingWorkflowService.java**
```java
/**
* 设定执行利率
* @param serialNum 业务方流水号
* @param executeRate 执行利率
* @return 是否成功
*/
boolean setExecuteRate(String serialNum, String executeRate);
```
**LoanPricingWorkflowServiceImpl.java**
- 实现上述方法
- 根据 `serialNum` 更新记录的 `execute_rate` 字段
- MyBatis Plus 会自动填充 `update_by``update_time`
### Controller 层
**LoanPricingWorkflowController.java**
```java
/**
* 设定执行利率
*/
@Operation(summary = "设定执行利率")
@Log(title = "利率定价流程", businessType = BusinessType.UPDATE)
@PutMapping("/{serialNum}/executeRate")
public AjaxResult setExecuteRate(
@Parameter(description = "业务方流水号")
@PathVariable("serialNum") String serialNum,
@RequestBody Map<String, String> request)
{
String executeRate = request.get("executeRate");
boolean success = loanPricingWorkflowService.setExecuteRate(serialNum, executeRate);
return success ? success() : error("设定失败");
}
```
### VO 层变更
**LoanPricingWorkflowVO.java**
- 添加 `executeRate` 字段到 VO确保详情接口返回执行利率
## 数据校验
1. **记录存在性**: 根据 `serialNum` 查询记录,不存在则返回 404
2. **更新结果检查**: 检查更新操作影响行数0 行表示记录不存在
3. **格式校验**: 执行利率为字符串格式,前端传递格式化后的值(如 "4.20"
## 事务处理
- 使用 MyBatis Plus 的 `updateById` 方法,自动处理事务
- 更新失败时抛出异常,由全局异常处理器处理
## 日志记录
- 使用 `@Log` 注解记录操作日志
- 日志类型: `BusinessType.UPDATE`
- 自动记录操作人和操作时间

View File

@@ -0,0 +1,63 @@
# 提案: 添加执行利率设定接口
## 背景
当前利率定价流程已有测算利率(模型输出计算得到),但缺少最终执行利率的设定功能。业务人员需要根据模型测算结果和实际情况,手动设定最终执行的贷款利率。
## 问题
1. 数据库表中没有存储执行利率的字段
2. 后端缺少设定执行利率的接口
3. API 文档需要更新
## 提案概述
为利率定价流程添加执行利率设定功能,允许业务人员为流程记录设定/更新最终执行利率。
### 功能范围
1. **数据库变更**
-`loan_pricing_workflow` 表中添加 `execute_rate` 字段
2. **后端接口**
- 新增 `PUT /loanPricing/workflow/{serialNum}/executeRate` 接口
- 支持设定和更新执行利率
- 无需特殊权限控制(与查询接口保持一致)
- 可多次修改执行利率
3. **API 文档更新**
-`doc/api/loan-pricing-workflow-api.md` 中添加新接口文档
## 影响范围
- 数据库: `loan_pricing_workflow`
- 后端:
- Entity: `LoanPricingWorkflow.java`
- Service: `ILoanPricingWorkflowService.java` 及实现类
- Controller: `LoanPricingWorkflowController.java`
- 文档: `doc/api/loan-pricing-workflow-api.md`
## 设计考虑
1. **字段类型**: 使用 `varchar(20)` 类型,与 `loan_rate` 保持一致
2. **可空性**: 允许为 NULL未设定时返回 null
3. **可修改性**: 允许多次修改,记录 `update_by``update_time`
4. **权限控制**: 暂不加独立权限,所有登录用户可操作(后续可根据需要添加)
5. **接口语义**: 使用 PUT 语义表示更新资源
## 替代方案
### 方案 A: 添加专门的审批流程(未采纳)
- **优点**: 流程更规范,支持审批
- **缺点**: 实现复杂度高,当前需求不明确
### 方案 B: 在创建接口中直接支持(未采纳)
- **优点**: 减少接口数量
- **缺点**: 业务上执行利率是在查看测算结果后设定的,与创建分离更合理
### 方案 C: 独立的设定接口(采纳)
- **优点**: 职责清晰,实现简单,支持多次修改
- **缺点**: 无明显缺点

View File

@@ -0,0 +1,27 @@
# loan-pricing-workflow Delta
## ADDED Requirements
### Requirement: 执行利率设定
系统 SHALL 提供设定执行利率的接口,允许业务人员为利率定价流程设定或更新最终执行利率。
#### Scenario: 设定执行利率
- **WHEN** 业务人员对已存在的利率定价流程调用设定执行利率接口,提供有效的业务方流水号和执行利率值
- **THEN** 系统更新该流程记录的执行利率字段,返回成功响应,并自动记录更新者和更新时间
#### Scenario: 更新已有执行利率
- **WHEN** 业务人员对已设定执行利率的流程再次调用设定接口
- **THEN** 系统覆盖更新执行利率为新的值
#### Scenario: 设定不存在的流程
- **WHEN** 业务人员提供的业务方流水号不存在
- **THEN** 系统返回"记录不存在"的错误信息
#### Scenario: 执行利率在详情中返回
- **WHEN** 业务人员查询流程详情
- **THEN** 系统在响应数据中包含 `executeRate` 字段(如果已设定则返回值,否则返回 null)

View File

@@ -0,0 +1,77 @@
# 实施任务
## 任务清单
1. **添加数据库字段**
- 文件: 创建 SQL 迁移脚本 `sql/add_execute_rate_field.sql`
- 操作: 在 `loan_pricing_workflow` 表中添加 `execute_rate` 字段
-
DDL: `ALTER TABLE loan_pricing_workflow ADD COLUMN execute_rate varchar(20) DEFAULT NULL COMMENT '执行利率(%)' AFTER loan_rate;`
- 验证: 字段添加成功
2. **更新 Entity 类**
- 文件: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java`
- 操作: 添加 `executeRate` 字段及注释
- 验证: 字段添加成功,位于 `loanRate` 字段之后
3. **更新 VO 类**
- 文件: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/vo/LoanPricingWorkflowVO.java`
- 操作: 添加 `executeRate` 字段及注释
- 验证: 字段添加成功
4. **更新 Service 接口**
- 文件: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ILoanPricingWorkflowService.java`
- 操作: 添加 `setExecuteRate(String serialNum, String executeRate)` 方法声明
- 验证: 方法添加成功
5. **实现 Service 方法**
- 文件: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
- 操作: 实现 `setExecuteRate` 方法
- 逻辑: 根据 serialNum 查询记录,更新 execute_rate 字段
- 验证: 方法实现完成
6. **添加 Controller 接口**
- 文件: `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
- 操作: 添加 `PUT /{serialNum}/executeRate` 接口方法
- 验证: 接口添加成功,添加 Swagger 注解
7. **更新 API 文档**
- 文件: `doc/api/loan-pricing-workflow-api.md`
- 操作: 在接口列表中添加"设定执行利率"接口文档
- 内容: 接口地址、请求参数、响应示例、说明
- 验证: 文档更新完整
8. **后端编译验证**
- 操作: 运行 `mvn clean compile` 或 IDE 编译
- 验证: 编译成功无错误
9. **接口功能验证**
- 操作:
- 启动后端服务
- 调用 `PUT /loanPricing/workflow/{serialNum}/executeRate` 接口
- 调用 `GET /loanPricing/workflow/{serialNum}` 接口验证返回值
- 验证:
- 设定执行利率成功
- 再次设定可覆盖更新
- 不存在的 serialNum 返回 404
- 详情接口正确返回 executeRate
## 依赖关系
- 任务 1 必须首先执行(数据库字段)
- 任务 2、3 可并行执行Entity 和 VO
- 任务 4、5 依次执行Service 接口 -> 实现)
- 任务 6 依赖任务 5
- 任务 7 可在任务 6 完成后执行
- 任务 8、9 依次执行
## 验收标准
- [x] 数据库字段 `execute_rate` 添加成功
- [x] Entity 和 VO 类添加 `executeRate` 字段
- [x] Service 接口和实现方法添加成功
- [x] Controller 接口添加成功并编译通过
- [x] API 文档更新完整
- [x] 接口调用成功,执行利率正确保存
- [x] 详情接口正确返回 `executeRate`
- [x] 不存在的记录返回 404

View File

@@ -0,0 +1,61 @@
# 提案: 将模型输出展示组件改为三列布局
## 背景
当前 `ModelOutputDisplay.vue` 组件中所有 `el-descriptions` 使用 `:column="2"` 两列布局展示模型输出字段。
## 问题
两列布局导致垂直空间占用较多,用户需要滚动更多才能查看完整信息。三列布局可以更好地利用屏幕宽度(特别是在桌面端),减少垂直滚动需求。
## 提案概述
`ModelOutputDisplay.vue` 组件中所有 `el-descriptions``:column` 属性从 `2` 修改为 `3`
### 影响范围
- 前端: `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
### 具体修改点
需要修改以下位置的 `:column` 属性:
#### 个人客户模型输出
1. 基本信息 (line 11): `:column="2"``:column="3"`
2. 忠诚度分析 (line 22): `:column="2"``:column="3"`
3. 贡献度分析 (line 35): `:column="2"``:column="3"`
4. 关联度分析 (line 45): `:column="2"``:column="3"`
5. 贷款特征 (line 65): `:column="2"``:column="3"`
6. 风险度分析 (line 81): `:column="2"``:column="3"`
7. 测算结果 (line 93): `:column="2"``:column="3"`
#### 企业客户模型输出
8. 基本信息 (line 104): `:column="2"``:column="3"`
9. 忠诚度分析 (line 115): `:column="2"``:column="3"`
10. 贡献度分析 (line 126): `:column="2"``:column="3"`
11. 关联度分析 (line 136): `:column="2"``:column="3"`
12. 企业类别 (line 155): `:column="2"``:column="3"`
13. 贷款特征 (line 167): `:column="2"``:column="3"`
14. 风险度分析 (line 180): `:column="2"``:column="3"`
15. 测算结果 (line 192): `:column="2"``:column="3"`
## 设计考虑
1. **空间利用**: 三列布局可以减少约 33% 的垂直空间占用
2. **可读性**: Element UI 的 `el-descriptions` 组件会自动调整标签宽度三列布局在标准桌面显示器1920px 宽度)下可读性良好
3. **兼容性**: `el-descriptions` 组件原生支持多列布局,无需额外样式调整
4. **响应式**: 在小屏幕设备上Element UI 会自动调整布局,无需额外处理
## 替代方案
### 方案 A: 保持两列布局 (未采纳)
- **优点**: 标签内容空间更充足
- **缺点**: 垂直空间占用大,需要更多滚动
### 方案 B: 使用四列布局 (未采纳)
- **优点**: 垂直空间占用最少
- **缺点**: 标签内容可能被压缩,影响可读性
### 方案 C: 改为三列布局 (采纳)
- **优点**: 平衡空间利用率和可读性
- **缺点**: 无明显缺点

View File

@@ -0,0 +1,15 @@
# loan-pricing-workflow-ui Delta
## MODIFIED Requirements
### Requirement: 模型输出展示布局
模型输出组件 SHALL 使用三列布局展示字段信息,以提高空间利用率。
#### Scenario: 查看个人客户模型输出三列布局
- **WHEN** 用户在流程详情页查看个人客户记录的模型输出信息
- **THEN** 系统在所有模型输出 Tab基本信息、忠诚度分析、贡献度分析、关联度分析、贷款特征、风险度分析、测算结果中使用三列布局展示字段
#### Scenario: 查看企业客户模型输出三列布局
- **WHEN** 用户在流程详情页查看企业客户记录的模型输出信息
- **THEN** 系统在所有模型输出 Tab基本信息、忠诚度分析、贡献度分析、关联度分析、企业类别、贷款特征、风险度分析、测算结果中使用三列布局展示字段

View File

@@ -0,0 +1,36 @@
# 实施任务
## 任务清单
1. **修改 ModelOutputDisplay.vue 组件列数配置**
- 文件: `ruoyi-ui/src/views/loanPricing/workflow/components/ModelOutputDisplay.vue`
- 操作: 将所有 `el-descriptions``:column="2"` 修改为 `:column="3"`
- 涉及行数: 11, 22, 35, 45, 65, 81, 93, 104, 115, 126, 136, 155, 167, 180, 192
- 验证: 确认所有 15 处 `el-descriptions``:column` 属性已修改为 `3`
2. **前端构建验证**
- 操作: 运行 `cd ruoyi-ui && npm run build:prod`
- 验证: 构建成功无错误
3. **功能验证**
- 操作:
- 启动前端开发服务器 `npm run dev`
- 访问流程详情页,选择个人客户记录,验证模型输出显示为三列
- 选择企业客户记录,验证模型输出显示为三列
- 验证:
- 个人客户所有 7 个 Tab 的字段展示均为三列布局
- 企业客户所有 8 个 Tab 的字段展示均为三列布局
- 标签和值内容正常显示,无溢出或错位
## 依赖关系
- 任务 1 必须首先完成
- 任务 2 和任务 3 可并行执行
## 验收标准
- [x] 所有 `el-descriptions``:column` 属性值均为 `3`
- [x] 前端构建成功
- [x] 个人客户模型输出所有 Tab 正确显示为三列
- [x] 企业客户模型输出所有 Tab 正确显示为三列
- [x] 字段标签和内容显示正常,无布局问题

View File

@@ -0,0 +1,360 @@
# Design: 拆分个人和企业利率定价发起接口
## 数据库变更
### 缺失字段分析
经过对比 `person.csv``corp.csv` 中定义的字段与现有数据库表 `loan_pricing_workflow`,发现以下字段缺失:
#### 个人客户缺失字段
| 字段名 | 中文名 | 类型 | 说明 |
|-----------|------|--------------|-------------------------|
| id_num | 证件号码 | varchar(100) | 存储个人身份证号或其他证件号码 |
| loan_loop | 循环功能 | varchar(10) | 贷款合同是否开通循环功能true/false |
#### 企业客户缺失字段
| 字段名 | 中文名 | 类型 | 说明 |
|-----------------------|------------|--------------|----------------------------------|
| id_num | 证件号码 | varchar(100) | 存储企业统一社会信用代码或其他证件号码 |
| is_trade_construction | 贸易和建筑业企业标识 | varchar(10) | 抵押类贸易和建筑业企业上调20BPtrue/false |
| is_green_loan | 绿色贷款 | varchar(10) | 绿色贷款标识true/false |
| is_tech_ent | 科技型企业 | varchar(10) | 科技型企业标识true/false |
| loan_term | 贷款期限 | varchar(50) | 贷款期限,单位:月/年 |
### 数据库迁移 SQL
```sql
-- 添加缺失的字段到 loan_pricing_workflow 表
-- 个人和企业共同需要的字段
ALTER TABLE `loan_pricing_workflow` ADD COLUMN `id_num` varchar(100) DEFAULT NULL COMMENT '证件号码' AFTER `id_type`;
-- 个人客户专用字段
ALTER TABLE `loan_pricing_workflow` ADD COLUMN `loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false' AFTER `biz_proof`;
-- 企业客户专用字段
ALTER TABLE `loan_pricing_workflow` ADD COLUMN `is_trade_construction` varchar(10) DEFAULT NULL COMMENT '贸易和建筑业企业标识: true/false抵质押类上调20BP' AFTER `is_agri_guar`;
ALTER TABLE `loan_pricing_workflow` ADD COLUMN `is_green_loan` varchar(10) DEFAULT NULL COMMENT '绿色贷款: true/false' AFTER `is_agri_guar`;
ALTER TABLE `loan_pricing_workflow` ADD COLUMN `is_tech_ent` varchar(10) DEFAULT NULL COMMENT '科技型企业: true/false' AFTER `is_agri_guar`;
ALTER TABLE `loan_pricing_workflow` ADD COLUMN `loan_term` varchar(50) DEFAULT NULL COMMENT '贷款期限' AFTER `apply_amt`;
```
### Entity 类更新
`LoanPricingWorkflow.java` 需要添加以下属性:
```java
/** 证件号码 */
private String idNum;
/** 循环功能: true/false */
private String loanLoop;
/** 贸易和建筑业企业标识: true/false */
private String isTradeConstruction;
/** 绿色贷款: true/false */
private String isGreenLoan;
/** 科技型企业: true/false */
private String isTechEnt;
/** 贷款期限 */
private String loanTerm;
```
## 架构设计
### 1. DTO 结构设计
#### PersonalLoanPricingCreateDTO个人客户发起DTO
```java
@Data
public class PersonalLoanPricingCreateDTO {
@NotBlank(message = "客户内码不能为空")
private String custIsn;
@NotBlank(message = "客户类型不能为空")
private String custType; // 固定值 "个人"
@NotBlank(message = "担保方式不能为空")
private String guarType; // 可选值:信用,保证,抵押,质押
private String custName;
private String idType;
private String idNum;
@NotBlank(message = "申请金额不能为空")
private String applyAmt; // 单位:元
private String bizProof; // 是否有经营佐证
private String loanLoop; // 循环功能
private String collType; // 抵质押类型
private String collThirdParty; // 抵质押物是否三方所有
}
```
#### CorporateLoanPricingCreateDTO企业客户发起DTO
```java
@Data
public class CorporateLoanPricingCreateDTO {
@NotBlank(message = "客户内码不能为空")
private String custIsn;
@NotBlank(message = "客户类型不能为空")
private String custType; // 固定值 "企业"
@NotBlank(message = "担保方式不能为空")
private String guarType; // 可选值:信用,保证,抵押,质押
private String custName;
private String idType;
private String idNum;
@NotBlank(message = "申请金额不能为空")
private String applyAmt; // 单位:元
private String isAgriGuar; // 省农担担保贷款
private String isGreenLoan; // 绿色贷款
private String isTechEnt; // 科技型企业
private String loanTerm; // 贷款期限
private String collType; // 抵质押类型
private String collThirdParty; // 抵质押物是否三方所有
}
```
### 2. 接口设计
#### Controller 层
```java
@RestController
@RequestMapping("/loanPricing/workflow")
public class LoanPricingWorkflowController extends BaseController {
// 原有接口(保留)
@PostMapping("/create")
public AjaxResult create(@Validated @RequestBody LoanPricingWorkflow loanPricingWorkflow)
// 新增:个人客户发起接口
@PostMapping("/create/personal")
public AjaxResult createPersonal(@Validated @RequestBody PersonalLoanPricingCreateDTO dto)
// 新增:企业客户发起接口
@PostMapping("/create/corporate")
public AjaxResult createCorporate(@Validated @RequestBody CorporateLoanPricingCreateDTO dto)
}
```
#### Service 层接口
```java
public interface ILoanPricingWorkflowService {
// 原有方法(保留兼容)
LoanPricingWorkflow createLoanPricing(LoanPricingWorkflow loanPricingWorkflow);
// 新增:个人客户发起
LoanPricingWorkflow createPersonalLoanPricing(PersonalLoanPricingCreateDTO dto);
// 新增:企业客户发起
LoanPricingWorkflow createCorporateLoanPricing(CorporateLoanPricingCreateDTO dto);
}
```
### 3. 字段映射关系
| 字段类别 | 个人字段 | 企业字段 |
|------|------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
| 共同字段 | custIsn, custType, guarType, custName, idType, idNum, applyAmt, collType, collThirdParty | |
| 个人特有 | bizProof是否有经营佐证, loanLoop循环功能 | |
| 企业特有 | | isAgriGuar省农担担保贷款, isGreenLoan绿色贷款, isTechEnt科技型企业, isTradeConstruction贸易和建筑业企业, loanTerm贷款期限 |
### 4. 实现细节
#### DTO 转 Entity 转换器
创建一个转换工具类,将 DTO 转换为 `LoanPricingWorkflow` 实体:
```java
public class LoanPricingConverter {
public static LoanPricingWorkflow toEntity(PersonalLoanPricingCreateDTO dto) {
LoanPricingWorkflow entity = new LoanPricingWorkflow();
// 映射共同字段
entity.setCustIsn(dto.getCustIsn());
entity.setCustType("个人");
entity.setCustName(dto.getCustName());
entity.setIdType(dto.getIdType());
entity.setIdNum(dto.getIdNum());
entity.setGuarType(dto.getGuarType());
entity.setApplyAmt(dto.getApplyAmt());
entity.setCollType(dto.getCollType());
entity.setCollThirdParty(dto.getCollThirdParty());
// 映射个人特有字段
entity.setBizProof(dto.getBizProof());
entity.setLoanLoop(dto.getLoanLoop());
return entity;
}
public static LoanPricingWorkflow toEntity(CorporateLoanPricingCreateDTO dto) {
LoanPricingWorkflow entity = new LoanPricingWorkflow();
// 映射共同字段
entity.setCustIsn(dto.getCustIsn());
entity.setCustType("企业");
entity.setCustName(dto.getCustName());
entity.setIdType(dto.getIdType());
entity.setIdNum(dto.getIdNum());
entity.setGuarType(dto.getGuarType());
entity.setApplyAmt(dto.getApplyAmt());
entity.setCollType(dto.getCollType());
entity.setCollThirdParty(dto.getCollThirdParty());
// 映射企业特有字段
entity.setIsAgriGuar(dto.getIsAgriGuar());
entity.setIsGreenLoan(dto.getIsGreenLoan());
entity.setIsTechEnt(dto.getIsTechEnt());
entity.setIsTradeConstruction(dto.getIsTradeConstruction());
entity.setLoanTerm(dto.getLoanTerm());
return entity;
}
}
```
### 5. 验证策略
#### 个人客户验证
- 必填字段custIsn, custType, guarType, applyAmt
- 枚举验证guarType 必须是"信用/保证/抵押/质押"之一
#### 企业客户验证
- 必填字段custIsn, custType, guarType, applyAmt
- 枚举验证guarType 必须是"信用/保证/抵押/质押"之一
### 6. 向后兼容性策略
1. 保留原有 `POST /loanPricing/workflow/create` 接口
2. 新旧接口共存,前端可自由选择使用
3. 不添加 @Deprecated 标记,保持接口的可用性
## 技术决策
### 为什么使用两个独立的 DTO 而不是一个带条件验证的 DTO
1. **类型安全**:编译期就能发现字段错误
2. **API 文档更清晰**Swagger 只显示相关字段
3. **验证更简单**:不需要在 DTO 内部根据类型做条件验证
### 为什么保留原有接口?
1. **多接口共存**:前端可以选择使用新接口或继续使用原有接口
2. **降级方案**:如果新接口有问题,可以快速回退
3. **兼容性**:可能有其他系统调用该接口
## 文件清单
### 数据库相关
- **新增**: `sql/add_missing_fields.sql` - 数据库迁移脚本
### 新增文件
- `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/util/LoanPricingConverter.java`
### 测试文件
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTOTest.java`
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTOTest.java`
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.java`
- `test_api/test_personal_create.http` - 个人客户发起接口 HTTP 测试脚本
- `test_api/test_corporate_create.http` - 企业客户发起接口 HTTP 测试脚本
- `test_api/test_backward_compatibility.http` - 向后兼容性测试脚本
### 修改文件
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/entity/LoanPricingWorkflow.java` - 添加缺失字段的属性
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/controller/LoanPricingWorkflowController.java`
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ILoanPricingWorkflowService.java`
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java`
## 测试脚本示例
### HTTP 测试脚本格式
测试脚本使用 IntelliJ IDEA 的 .http 文件格式,示例如下:
```http
### Token
POST http://localhost:8080/login/test
Content-Type: application/json
{
"username": "admin",
"password": "admin123"
}
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 200, "Response code is 200");
client.global.set("token", response.body.data.token);
});
%}
### -
POST http://localhost:8080/loanPricing/workflow/create/personal
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "TEST001",
"custName": "",
"idType": "",
"idNum": "110101199001011234",
"guarType": "",
"applyAmt": "500000",
"bizProof": "true",
"loanLoop": "false"
}
> {%
client.test("Personal loan creation successful", function() {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 200, "Response code is 200");
client.assert(response.body.data.custType === "", "Customer type is ");
});
%}
### -
POST http://localhost:8080/loanPricing/workflow/create/corporate
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "CORP001",
"custName": "",
"idType": "",
"idNum": "91110000100000000X",
"guarType": "",
"applyAmt": "1000000",
"isAgriGuar": "false",
"isGreenLoan": "true",
"isTechEnt": "true",
"loanTerm": "36"
}
> {%
client.test("Corporate loan creation successful", function() {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 200, "Response code is 200");
client.assert(response.body.data.custType === "", "Customer type is ");
});
%}
```

View File

@@ -0,0 +1,83 @@
# Proposal: 拆分个人和企业利率定价发起接口
## 概述
将现有的统一利率定价发起接口 (`POST /loanPricing/workflow/create`) 拆分为两个独立的接口:
- 个人客户发起接口 (`POST /loanPricing/workflow/create/personal`)
- 企业客户发起接口 (`POST /loanPricing/workflow/create/corporate`)
## 背景
当前系统中,`LoanPricingWorkflow` 实体包含了个人和企业客户的所有字段。当发起流程时根据客户类型custType的不同需要填写的字段存在差异
- **个人客户**:需要填写个人特有字段(如是否有经营佐证、循环功能等)
- **企业客户**:需要填写企业特有字段(如贸易和建筑业企业标识、省农担担保贷款、绿色贷款、科技型企业、贷款期限等)
将接口拆分可以带来以下好处:
1. **接口契约更清晰**:每个接口只包含对应客户类型的字段
2. **验证更精确**:可以在 DTO 层面进行字段验证
3. **文档更友好**API 文档可以分别展示个人和企业的字段
4. **维护性更好**:字段变更不会影响另一类型的客户
## 影响范围
### 后端变更
- 创建新的 DTO 类:`PersonalLoanPricingCreateDTO``CorporateLoanPricingCreateDTO`
-`LoanPricingWorkflowController` 中添加两个新接口
- 修改 `ILoanPricingWorkflowService` 接口,添加两个新的创建方法
- 实现 `LoanPricingWorkflowServiceImpl` 中的新方法
- 保留原有接口以保持向后兼容(可选,建议标记为 Deprecated
### 前端变更
> **注:本次变更仅涉及后端,前端暂不修改**
### 数据库变更
需要向 `loan_pricing_workflow` 表添加以下字段:
| 字段名 | 类型 | 说明 | 适用客户 |
|-----------------------|--------------|------------------------|-------|
| id_num | varchar(100) | 证件号码 | 个人、企业 |
| loan_loop | varchar(10) | 循环功能 | 个人 |
| is_trade_construction | varchar(10) | 贸易和建筑业企业标识抵质押类上调20BP | 企业 |
| is_green_loan | varchar(10) | 绿色贷款最多下降5BP | 企业 |
| is_tech_ent | varchar(10) | 科技型企业最多下降5BP | 企业 |
| loan_term | varchar(50) | 贷款期限 | 企业 |
**迁移脚本位置**: `sql/add_missing_fields.sql`
同时需要更新 `LoanPricingWorkflow` Entity 类,添加对应的属性和字段映射注解。
## 设计方案
详见 `design.md`
## 风险和考虑
1. **向后兼容性**:原有接口 `POST /loanPricing/workflow/create` 继续保留
- 原有接口不做标记为 Deprecated 的处理
- 新旧接口共存,供前端自由选择使用
2. **数据迁移**:需要执行数据库迁移脚本添加新字段,但不涉及现有数据的迁移
3. **测试覆盖**:需要为新接口编写测试用例
## 依赖关系
- **数据库迁移必须最先执行**:所有后端开发依赖于新字段添加完成
- 依赖现有的 `LoanPricingWorkflow` 实体
- 依赖现有的 `ILoanPricingWorkflowService` 服务层
## 验收标准
1. **数据库变更**:所有新字段成功添加到数据库表,且可以正确存储和检索数据
2. **Entity 类更新**`LoanPricingWorkflow` 实体类包含所有新增字段的属性和映射
3. **个人客户接口**:个人客户发起接口只接受个人相关的字段
4. **企业客户接口**:企业客户发起接口只接受企业相关的字段
5. **接口功能**:两个接口都能成功创建利率定价流程记录,包括新增字段的保存
6. **字段验证**:字段验证正确生效
7. **API 文档**API 文档正确显示两个接口的差异

View File

@@ -0,0 +1,105 @@
# 企业利率定价流程发起 Capability Spec
## ADDED Requirements
### Requirement: 企业客户利率定价流程发起
系统 SHALL 提供企业客户专用的利率定价流程发起接口 (`POST /loanPricing/workflow/create/corporate`),该接口只接受企业客户相关的字段。
#### Scenario: 成功发起企业客户利率定价流程
- **WHEN** 业务人员通过企业客户发起接口提交包含必填字段(custIsn、guarType、applyAmt)的完整申请
- **THEN** 系统自动生成业务方流水号(serialNum)并保存记录custType 固定为"企业",返回成功响应
#### Scenario: 字段验证-必填字段
- **WHEN** 提交的企业客户申请缺少必填字段(custIsn、guarType、applyAmt)
- **THEN** 系统返回参数验证失败的错误信息
#### Scenario: 字段验证-担保方式枚举值
- **WHEN** 提交的企业客户申请中 guarType 不是"信用"、"保证"、"抵押"、"质押"之一
- **THEN** 系统返回参数验证失败的错误信息
#### Scenario: 记录企业特有字段
- **WHEN** 企业客户申请中包含 isAgriGuar省农担担保贷款、isGreenLoan绿色贷款、isTechEnt科技型企业或 loanTerm贷款期限字段
- **THEN** 系统正确保存这些字段的值
#### Scenario: 自动设置客户类型
- **WHEN** 通过企业客户发起接口创建流程
- **THEN** 系统自动将 custType 设置为"企业",无需客户端传入
#### Scenario: 自动生成业务方流水号
- **WHEN** 发起企业客户利率定价流程时
- **THEN** 系统使用时间戳自动生成唯一的业务方流水号
#### Scenario: 记录创建和更新信息
- **WHEN** 企业客户利率定价流程创建成功
- **THEN** 系统自动记录创建者、创建时间、更新者、更新时间
### Requirement: 企业客户发起接口字段定义
企业客户发起接口 SHALL 接受以下字段:
#### Scenario: 基本信息字段
- **WHEN** 客户端提交企业客户发起请求
- **THEN** 系统接受以下基本信息字段:
- `custIsn`(必填):客户内码
- `custName`:客户名称
- `idType`:证件类型
- `idNum`:证件号码
#### Scenario: 贷款信息字段
- **WHEN** 客户端提交企业客户发起请求
- **THEN** 系统接受以下贷款信息字段:
- `guarType`(必填):担保方式,可选值"信用"、"保证"、"抵押"、"质押"
- `applyAmt`(必填):申请金额,单位:元
- `loanTerm`:贷款期限
#### Scenario: 企业特有字段-优惠条件
- **WHEN** 客户端提交企业客户发起请求
- **THEN** 系统接受以下企业特有优惠条件字段:
- `isAgriGuar`省农担担保贷款省农担担保贷款下调40个BP
- `isGreenLoan`绿色贷款绿色贷款或科技型企业最多下降5BP
- `isTechEnt`科技型企业绿色贷款或科技型企业最多下降5BP
#### Scenario: 贸易和建筑业企业标识
- **WHEN** 企业客户申请中担保方式为抵(质)押类,且企业为贸易和建筑业企业
- **THEN** 系统记录该标识用于后续利率测算时上调20BP
#### Scenario: 抵质押信息字段
- **WHEN** 客户端提交企业客户发起请求
- **THEN** 系统接受以下抵质押信息字段:
- `collType`:抵质押类型
- `collThirdParty`:抵质押物是否三方所有
## MODIFIED Requirements
### Requirement: 利率定价流程发起接口弃用策略
原有的统一发起接口 (`POST /loanPricing/workflow/create`) SHALL 标记为 Deprecated系统 SHALL
继续支持该接口以保证向后兼容,同时引导使用新的个人或企业专用接口。
#### Scenario: 原有接口向后兼容
- **WHEN** 客户端继续使用原有发起接口
- **THEN** 系统继续处理请求并返回正确结果,但建议迁移到新接口
#### Scenario: 原有接口标记为已弃用
- **WHEN** 开发人员查看 API 文档
- **THEN** 原有接口标记为 Deprecated引导使用新的个人或企业专用接口
## Cross-References
- 相关 Capability: `personal-loan-pricing-creation`(个人客户利率定价流程发起)
- 修改自 Capability: `loan-pricing-workflow` 中的"利率定价流程发起"需求

View File

@@ -0,0 +1,98 @@
# 个人利率定价流程发起 Capability Spec
## ADDED Requirements
### Requirement: 个人客户利率定价流程发起
系统 SHALL 提供个人客户专用的利率定价流程发起接口 (`POST /loanPricing/workflow/create/personal`),该接口只接受个人客户相关的字段。
#### Scenario: 成功发起个人客户利率定价流程
- **WHEN** 业务人员通过个人客户发起接口提交包含必填字段(custIsn、guarType、applyAmt)的完整申请
- **THEN** 系统自动生成业务方流水号(serialNum)并保存记录custType 固定为"个人",返回成功响应
#### Scenario: 字段验证-必填字段
- **WHEN** 提交的个人客户申请缺少必填字段(custIsn、guarType、applyAmt)
- **THEN** 系统返回参数验证失败的错误信息
#### Scenario: 字段验证-担保方式枚举值
- **WHEN** 提交的个人客户申请中 guarType 不是"信用"、"保证"、"抵押"、"质押"之一
- **THEN** 系统返回参数验证失败的错误信息
#### Scenario: 记录个人特有字段
- **WHEN** 个人客户申请中包含 bizProof是否有经营佐证或 loanLoop循环功能字段
- **THEN** 系统正确保存这些字段的值
#### Scenario: 自动设置客户类型
- **WHEN** 通过个人客户发起接口创建流程
- **THEN** 系统自动将 custType 设置为"个人",无需客户端传入
#### Scenario: 自动生成业务方流水号
- **WHEN** 发起个人客户利率定价流程时
- **THEN** 系统使用时间戳自动生成唯一的业务方流水号
#### Scenario: 记录创建和更新信息
- **WHEN** 个人客户利率定价流程创建成功
- **THEN** 系统自动记录创建者、创建时间、更新者、更新时间
### Requirement: 个人客户发起接口字段定义
个人客户发起接口 SHALL 接受以下字段:
#### Scenario: 基本信息字段
- **WHEN** 客户端提交个人客户发起请求
- **THEN** 系统接受以下基本信息字段:
- `custIsn`(必填):客户内码
- `custName`:客户名称
- `idType`:证件类型
- `idNum`:证件号码
#### Scenario: 贷款信息字段
- **WHEN** 客户端提交个人客户发起请求
- **THEN** 系统接受以下贷款信息字段:
- `guarType`(必填):担保方式,可选值"信用"、"保证"、"抵押"、"质押"
- `applyAmt`(必填):申请金额,单位:元
#### Scenario: 个人特有字段
- **WHEN** 客户端提交个人客户发起请求
- **THEN** 系统接受以下个人特有字段:
- `bizProof`:是否有经营佐证(个人经营性贷款提供的经营佐证包括:借款人或其配偶为法定代表人、实际经营者、股东的企业(个体工商户)营业执照或企查查、企信宝查档资料)
- `loanLoop`:贷款合同是否开通循环功能
#### Scenario: 抵质押信息字段
- **WHEN** 客户端提交个人客户发起请求
- **THEN** 系统接受以下抵质押信息字段:
- `collType`:抵质押类型
- `collThirdParty`:抵质押物是否三方所有
## MODIFIED Requirements
### Requirement: 利率定价流程发起接口弃用策略
原有的统一发起接口 (`POST /loanPricing/workflow/create`) SHALL 标记为 Deprecated系统 SHALL
继续支持该接口以保证向后兼容,同时引导使用新的个人或企业专用接口。
#### Scenario: 原有接口向后兼容
- **WHEN** 客户端继续使用原有发起接口
- **THEN** 系统继续处理请求并返回正确结果,但建议迁移到新接口
#### Scenario: 原有接口标记为已弃用
- **WHEN** 开发人员查看 API 文档
- **THEN** 原有接口标记为 Deprecated引导使用新的个人或企业专用接口
## Cross-References
- 相关 Capability: `corporate-loan-pricing-creation`(企业客户利率定价流程发起)
- 修改自 Capability: `loan-pricing-workflow` 中的"利率定价流程发起"需求

View File

@@ -0,0 +1,149 @@
# Tasks: 拆分个人和企业利率定价发起接口
## 数据库变更任务
### 0. 数据库表结构更新
- [x] 执行数据库迁移脚本,添加缺失字段:
- `id_num` varchar(100) - 证件号码(个人和企业都需要)
- `loan_loop` varchar(10) - 循环功能(个人客户专用)
- `is_trade_construction` varchar(10) - 贸易和建筑业企业标识(企业客户专用)
- `is_green_loan` varchar(10) - 绿色贷款(企业客户专用)
- `is_tech_ent` varchar(10) - 科技型企业(企业客户专用)
- `loan_term` varchar(50) - 贷款期限(企业客户专用)
- [x] 更新 `LoanPricingWorkflow` Entity 类,添加对应的属性和字段映射注解
- [ ] 验证数据库变更是否成功执行(需手动执行 SQL 脚本)
### 0.1 创建数据库迁移脚本
- [x]`sql/` 目录下创建 `add_missing_fields.sql` 文件
- [x] 包含所有 ALTER TABLE 语句
- [x] 添加详细的注释说明每个字段的用途
## 后端实现任务
### 1. 创建 DTO 类
- [x] 创建 `PersonalLoanPricingCreateDTO.java`
- 添加共同字段custIsn、custName、idType、idNum、guarType、applyAmt、collType、collThirdParty
- 添加个人客户特有字段bizProof是否有经营佐证、loanLoop循环功能
- 添加共同字段验证注解custIsn、guarType、applyAmt 为必填
- 添加担保方式枚举验证
- [x] 创建 `CorporateLoanPricingCreateDTO.java`
- 添加共同字段custIsn、custName、idType、idNum、guarType、applyAmt、collType、collThirdParty
- 添加企业客户特有字段isAgriGuar省农担担保贷款、isGreenLoan绿色贷款、isTechEnt科技型企业、isTradeConstruction贸易和建筑业企业、loanTerm贷款期限
- 添加共同字段验证注解custIsn、guarType、applyAmt 为必填
- 添加担保方式枚举验证
### 2. 更新 Entity 类
- [x]`LoanPricingWorkflow.java` 中添加缺失的字段属性:
- `idNum` - 证件号码
- `loanLoop` - 循环功能
- `isTradeConstruction` - 贸易和建筑业企业标识
- `isGreenLoan` - 绿色贷款
- `isTechEnt` - 科技型企业
- `loanTerm` - 贷款期限
- [x] 为新字段添加 MyBatis Plus 字段映射注解(如果需要)
- [ ] 验证 Entity 类与数据库表字段映射正确(需执行 SQL 后验证)
### 3. 创建转换器工具类
- [x] 创建 `LoanPricingConverter.java`
- 实现 `toEntity(PersonalLoanPricingCreateDTO)` 方法
- 实现 `toEntity(CorporateLoanPricingCreateDTO)` 方法
- 确保自动设置 custType 为"个人"或"企业"
- 正确映射所有字段,包括新增的 idNum、loanLoop、isTradeConstruction、isGreenLoan、isTechEnt、loanTerm
### 4. 修改 Service 层
- [x]`ILoanPricingWorkflowService` 接口中添加:
- `createPersonalLoanPricing(PersonalLoanPricingCreateDTO dto)` 方法声明
- `createCorporateLoanPricing(CorporateLoanPricingCreateDTO dto)` 方法声明
- [x]`LoanPricingWorkflowServiceImpl` 中实现新方法:
- 实现 `createPersonalLoanPricing` 方法,调用转换器后复用现有逻辑
- 实现 `createCorporateLoanPricing` 方法,调用转换器后复用现有逻辑
### 5. 修改 Controller 层
- [x]`LoanPricingWorkflowController` 中添加:
- `POST /loanPricing/workflow/create/personal` 接口
- `POST /loanPricing/workflow/create/corporate` 接口
- [x] 添加 Swagger 注解,明确标注接口用途和字段说明
- [x] 保持原有接口不变,不做 @Deprecated 标记
### 6. 单元测试
- [ ] 创建 `PersonalLoanPricingCreateDTOTest.java` 测试类
- 测试必填字段验证custIsn、guarType、applyAmt
- 测试担保方式枚举验证(信用/保证/抵押/质押)
- 测试个人特有字段bizProof、loanLoop
- [ ] 创建 `CorporateLoanPricingCreateDTOTest.java` 测试类
- 测试必填字段验证custIsn、guarType、applyAmt
- 测试担保方式枚举验证(信用/保证/抵押/质押)
- 测试企业特有字段isAgriGuar、isGreenLoan、isTechEnt、isTradeConstruction、loanTerm
- [ ] 创建 `LoanPricingConverterTest.java` 测试类
- 测试个人 DTO 转 Entity 映射完整性
- 测试企业 DTO 转 Entity 映射完整性
- 验证 custType 自动设置正确
### 7. 集成测试与测试脚本
- [x] 生成 HTTP 测试脚本 `test_personal_create.http`
- 测试个人客户发起成功场景(完整必填字段)
- 测试缺少必填字段的失败场景
- 测试担保方式枚举验证失败场景
- 使用 `/login/test` 接口获取测试 token
- [x] 生成 HTTP 测试脚本 `test_corporate_create.http`
- 测试企业客户发起成功场景(完整必填字段)
- 测试缺少必填字段的失败场景
- 测试担保方式枚举验证失败场景
- 测试企业特有字段isAgriGuar、isGreenLoan、isTechEnt、loanTerm
- [x] 生成 HTTP 测试脚本 `test_backward_compatibility.http`
- 验证原有 `POST /loanPricing/workflow/create` 接口仍可正常工作
- [x] 生成 Shell 测试脚本 `test_personal_create.sh`
- 将 HTTP 测试脚本转换为 Shell 脚本格式
- 使用 curl 命令进行接口调用
- 包含颜色输出和测试结果统计
- [x] 生成 Shell 测试脚本 `test_corporate_create.sh`
- 将 HTTP 测试脚本转换为 Shell 脚本格式
- 使用 curl 命令进行接口调用
- 包含颜色输出和测试结果统计
- [x] 生成 Shell 测试脚本 `test_backward_compatibility.sh`
- 将 HTTP 测试脚本转换为 Shell 脚本格式
- 使用 curl 命令进行接口调用
- 包含颜色输出和测试结果统计
- [ ] 执行所有测试脚本并验证结果
- [ ] 生成测试报告到 `doc/` 目录
## 文档和部署
### 8. API 文档
- [ ] 更新 Swagger 文档,添加两个新接口的完整说明
- [ ] 生成并导出 API 文档到项目 doc 目录
### 9. 代码审查和合并
- [ ] 提交 Pull Request
- [ ] 通过代码审查
- [ ] 合并到主分支
## 依赖关系
- **数据库变更任务0必须最先执行**:所有后端开发依赖于数据库字段完成
- 任务 1-5 可以并行开发依赖任务0完成
- 任务 6-7 依赖任务 1-5 完成
- 任务 8-9 依赖所有前序任务完成
> **注:本次变更仅涉及后端,前端暂不修改**
## 验证检查点
1. **数据库字段完整性**确保所有新增字段idNum、loanLoop、isTradeConstruction、isGreenLoan、isTechEnt、loanTerm已添加到数据库表
2. **Entity 类映射正确性**:确保 Entity 类字段与数据库表字段一一对应
3. **DTO 验证**:确保所有必填字段有 `@NotBlank` 注解,枚举字段有正确的验证
4. **转换器正确性**:确保 DTO 到 Entity 的转换不丢失字段,包括新增字段
5. **API 路由**:确保新接口路径正确且可访问
6. **向后兼容**:确保原有接口继续工作
7. **API 文档**:确保 Swagger 文档正确显示所有接口的参数说明

View File

@@ -25,9 +25,9 @@ spring:
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:mysql://116.62.17.81:40627/loan-pricing-892?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://116.62.17.81:3306/loan-pricing?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root username: root
password: 123456 password: Kfcx@1234
# 从库数据源 # 从库数据源
slave: slave:
# 从数据源开关/默认关闭 # 从数据源开关/默认关闭
@@ -84,11 +84,11 @@ spring:
# 地址 # 地址
host: 116.62.17.81 host: 116.62.17.81
# 端口默认为6379 # 端口默认为6379
port: 44565 port: 6379
# 数据库索引 # 数据库索引
database: 0 database: 0
# 密码 # 密码
password: 123456 password: Kfcx@1234
# 连接超时时间 # 连接超时时间
timeout: 10s timeout: 10s
lettuce: lettuce:

View File

@@ -9,7 +9,8 @@ import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport; import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.enums.BusinessType;
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; import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO; import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
import com.ruoyi.loanpricing.service.ILoanPricingWorkflowService; import com.ruoyi.loanpricing.service.ILoanPricingWorkflowService;
@@ -20,6 +21,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Map;
/** /**
* 利率定价流程Controller * 利率定价流程Controller
* *
@@ -35,14 +38,25 @@ public class LoanPricingWorkflowController extends BaseController
private ILoanPricingWorkflowService loanPricingWorkflowService; private ILoanPricingWorkflowService loanPricingWorkflowService;
/** /**
* 发起利率定价流程 * 发起个人客户利率定价流程
*/ */
@Operation(summary = "发起利率定价流程") @Operation(summary = "发起个人客户利率定价流程", description = "用于个人客户的利率定价流程发起")
@Log(title = "利率定价流程", businessType = BusinessType.INSERT) @Log(title = "个人客户利率定价流程", businessType = BusinessType.INSERT)
@PostMapping("/create") @PostMapping("/create/personal")
public AjaxResult create(@Validated @RequestBody LoanPricingWorkflow loanPricingWorkflow) public AjaxResult createPersonal(@Validated @RequestBody PersonalLoanPricingCreateDTO dto) {
LoanPricingWorkflow result = loanPricingWorkflowService.createPersonalLoanPricing(dto);
return success(result);
}
/**
* 发起企业客户利率定价流程
*/
@Operation(summary = "发起企业客户利率定价流程", description = "用于企业客户的利率定价流程发起")
@Log(title = "企业客户利率定价流程", businessType = BusinessType.INSERT)
@PostMapping("/create/corporate")
public AjaxResult createCorporate(@Validated @RequestBody CorporateLoanPricingCreateDTO dto)
{ {
LoanPricingWorkflow result = loanPricingWorkflowService.createLoanPricing(loanPricingWorkflow); LoanPricingWorkflow result = loanPricingWorkflowService.createCorporateLoanPricing(dto);
return success(result); return success(result);
} }
@@ -80,4 +94,19 @@ public class LoanPricingWorkflowController extends BaseController
} }
return success(workflow); return success(workflow);
} }
/**
* 设定执行利率
*/
@Operation(summary = "设定执行利率")
@Log(title = "利率定价流程", businessType = BusinessType.UPDATE)
@PutMapping("/{serialNum}/executeRate")
public AjaxResult setExecuteRate(
@Parameter(description = "业务方流水号")
@PathVariable("serialNum") String serialNum,
@RequestBody Map<String, String> request) {
String executeRate = request.get("executeRate");
boolean success = loanPricingWorkflowService.setExecuteRate(serialNum, executeRate);
return success ? success() : error("设定失败");
}
} }

View File

@@ -0,0 +1,64 @@
package com.ruoyi.loanpricing.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.io.Serializable;
/**
* 企业客户利率定价发起DTO
*
* @author ruoyi
* @date 2025-01-19
*/
@Data
@Schema(description = "企业客户利率定价发起请求")
public class CorporateLoanPricingCreateDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "客户内码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CORP001")
@NotBlank(message = "客户内码不能为空")
private String custIsn;
@Schema(description = "客户名称", example = "测试科技有限公司")
private String custName;
@Schema(description = "证件类型", example = "统一社会信用代码")
private String idType;
@Schema(description = "证件号码", example = "91110000100000000X")
private String idNum;
@Schema(description = "担保方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "抵押", allowableValues = {"信用", "保证", "抵押", "质押"})
@NotBlank(message = "担保方式不能为空")
@Pattern(regexp = "^(信用|保证|抵押|质押)$", message = "担保方式必须是:信用、保证、抵押、质押之一")
private String guarType;
@Schema(description = "申请金额(元)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000000")
@NotBlank(message = "申请金额不能为空")
private String applyAmt;
@Schema(description = "贷款期限", example = "36")
private String loanTerm;
@Schema(description = "省农担担保贷款", example = "false")
private String isAgriGuar;
@Schema(description = "绿色贷款", example = "true")
private String isGreenLoan;
@Schema(description = "科技型企业", example = "true")
private String isTechEnt;
@Schema(description = "贸易和建筑业企业标识", example = "false")
private String isTradeConstruction;
@Schema(description = "抵质押类型", example = "一类")
private String collType;
@Schema(description = "抵质押物是否三方所有", example = "false")
private String collThirdParty;
}

View File

@@ -0,0 +1,55 @@
package com.ruoyi.loanpricing.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.io.Serializable;
/**
* 个人客户利率定价发起DTO
*
* @author ruoyi
* @date 2025-01-19
*/
@Data
@Schema(description = "个人客户利率定价发起请求")
public class PersonalLoanPricingCreateDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "客户内码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CUST001")
@NotBlank(message = "客户内码不能为空")
private String custIsn;
@Schema(description = "客户名称", example = "张三")
private String custName;
@Schema(description = "证件类型", example = "身份证")
private String idType;
@Schema(description = "证件号码", example = "110101199001011234")
private String idNum;
@Schema(description = "担保方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "信用", allowableValues = {"信用", "保证", "抵押", "质押"})
@NotBlank(message = "担保方式不能为空")
@Pattern(regexp = "^(信用|保证|抵押|质押)$", message = "担保方式必须是:信用、保证、抵押、质押之一")
private String guarType;
@Schema(description = "申请金额(元)", requiredMode = Schema.RequiredMode.REQUIRED, example = "500000")
@NotBlank(message = "申请金额不能为空")
private String applyAmt;
@Schema(description = "是否有经营佐证", example = "true")
private String bizProof;
@Schema(description = "循环功能", example = "false")
private String loanLoop;
@Schema(description = "抵质押类型", example = "一类")
private String collType;
@Schema(description = "抵质押物是否三方所有", example = "false")
private String collThirdParty;
}

View File

@@ -3,7 +3,6 @@ package com.ruoyi.loanpricing.domain.entity;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@@ -65,6 +64,11 @@ public class LoanPricingWorkflow implements Serializable
@NotBlank(message = "申请金额不能为空") @NotBlank(message = "申请金额不能为空")
private String applyAmt; private String applyAmt;
/**
* 贷款期限
*/
private String loanTerm;
/** 净身企业: true/false */ /** 净身企业: true/false */
private String isCleanEnt; private String isCleanEnt;
@@ -77,6 +81,21 @@ public class LoanPricingWorkflow implements Serializable
/** 省农担担保贷款: true/false */ /** 省农担担保贷款: true/false */
private String isAgriGuar; private String isAgriGuar;
/**
* 贸易和建筑业企业标识: true/false
*/
private String isTradeConstruction;
/**
* 绿色贷款: true/false
*/
private String isGreenLoan;
/**
* 科技型企业: true/false
*/
private String isTechEnt;
/** 是否纳税信用等级A级: true/false */ /** 是否纳税信用等级A级: true/false */
private String isTaxA; private String isTaxA;
@@ -89,6 +108,9 @@ public class LoanPricingWorkflow implements Serializable
/** 是否有经营佐证: true/false */ /** 是否有经营佐证: true/false */
private String bizProof; private String bizProof;
/** 循环功能: true/false */
private String loanLoop;
/** 抵质押类型: 一线/一类/二类 */ /** 抵质押类型: 一线/一类/二类 */
private String collType; private String collType;
@@ -96,15 +118,24 @@ public class LoanPricingWorkflow implements Serializable
private String collThirdParty; private String collThirdParty;
/** 贷款利率 */ /** 贷款利率 */
@NotBlank(message = "贷款利率不能为空")
private String loanRate; private String loanRate;
/**
* 执行利率(%)
*/
private String executeRate;
/** 客户名称 */ /** 客户名称 */
private String custName; private String custName;
/** 证件类型 */ /**
* 证件类型
*/
private String idType; private String idType;
/** 证件号码 */
private String idNum;
/** 是否普惠小微借款人: true/false */ /** 是否普惠小微借款人: true/false */
private String isInclusiveFinance; private String isInclusiveFinance;

View File

@@ -2,6 +2,8 @@ package com.ruoyi.loanpricing.service;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO; import com.ruoyi.loanpricing.domain.vo.LoanPricingWorkflowVO;
@@ -16,12 +18,20 @@ import java.util.List;
public interface ILoanPricingWorkflowService public interface ILoanPricingWorkflowService
{ {
/** /**
* 发起利率定价流程 * 发起个人客户利率定价流程
* *
* @param loanPricingWorkflow 利率定价流程信息 * @param dto 个人客户发起DTO
* @return 结果 * @return 结果
*/ */
public LoanPricingWorkflow createLoanPricing(LoanPricingWorkflow loanPricingWorkflow); public LoanPricingWorkflow createPersonalLoanPricing(PersonalLoanPricingCreateDTO dto);
/**
* 发起企业客户利率定价流程
*
* @param dto 企业客户发起DTO
* @return 结果
*/
public LoanPricingWorkflow createCorporateLoanPricing(CorporateLoanPricingCreateDTO dto);
/** /**
* 查询利率定价流程列表 * 查询利率定价流程列表
@@ -47,4 +57,13 @@ public interface ILoanPricingWorkflowService
* @return 利率定价流程 * @return 利率定价流程
*/ */
public LoanPricingWorkflowVO selectLoanPricingBySerialNum(String serialNum); public LoanPricingWorkflowVO selectLoanPricingBySerialNum(String serialNum);
/**
* 设定执行利率
*
* @param serialNum 业务方流水号
* @param executeRate 执行利率
* @return 是否成功
*/
public boolean setExecuteRate(String serialNum, String executeRate);
} }

View File

@@ -3,6 +3,8 @@ package com.ruoyi.loanpricing.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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.LoanPricingWorkflow;
import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields; import com.ruoyi.loanpricing.domain.entity.ModelCorpOutputFields;
import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields; import com.ruoyi.loanpricing.domain.entity.ModelRetailOutputFields;
@@ -12,6 +14,7 @@ import com.ruoyi.loanpricing.mapper.ModelCorpOutputFieldsMapper;
import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper; import com.ruoyi.loanpricing.mapper.ModelRetailOutputFieldsMapper;
import com.ruoyi.loanpricing.service.ILoanPricingWorkflowService; import com.ruoyi.loanpricing.service.ILoanPricingWorkflowService;
import com.ruoyi.loanpricing.service.LoanPricingModelService; import com.ruoyi.loanpricing.service.LoanPricingModelService;
import com.ruoyi.loanpricing.util.LoanPricingConverter;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -50,7 +53,6 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
* @param loanPricingWorkflow 利率定价流程信息 * @param loanPricingWorkflow 利率定价流程信息
* @return 结果 * @return 结果
*/ */
@Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public LoanPricingWorkflow createLoanPricing(LoanPricingWorkflow loanPricingWorkflow) public LoanPricingWorkflow createLoanPricing(LoanPricingWorkflow loanPricingWorkflow)
{ {
@@ -75,6 +77,32 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
return loanPricingWorkflow; return loanPricingWorkflow;
} }
/**
* 发起个人客户利率定价流程
*
* @param dto 个人客户发起DTO
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public LoanPricingWorkflow createPersonalLoanPricing(PersonalLoanPricingCreateDTO dto) {
LoanPricingWorkflow entity = LoanPricingConverter.toEntity(dto);
return createLoanPricing(entity);
}
/**
* 发起企业客户利率定价流程
*
* @param dto 企业客户发起DTO
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public LoanPricingWorkflow createCorporateLoanPricing(CorporateLoanPricingCreateDTO dto) {
LoanPricingWorkflow entity = LoanPricingConverter.toEntity(dto);
return createLoanPricing(entity);
}
/** /**
* 查询利率定价流程列表 * 查询利率定价流程列表
* *
@@ -150,7 +178,7 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
// 按创建者筛选 // 按创建者筛选
if (StringUtils.hasText(loanPricingWorkflow.getCreateBy())) if (StringUtils.hasText(loanPricingWorkflow.getCreateBy()))
{ {
wrapper.eq(LoanPricingWorkflow::getCreateBy, loanPricingWorkflow.getCreateBy()); wrapper.like(LoanPricingWorkflow::getCreateBy, loanPricingWorkflow.getCreateBy());
} }
// 按客户名称模糊查询 // 按客户名称模糊查询
@@ -162,9 +190,32 @@ public class LoanPricingWorkflowServiceImpl implements ILoanPricingWorkflowServi
// 按机构号筛选 // 按机构号筛选
if (StringUtils.hasText(loanPricingWorkflow.getOrgCode())) if (StringUtils.hasText(loanPricingWorkflow.getOrgCode()))
{ {
wrapper.eq(LoanPricingWorkflow::getOrgCode, loanPricingWorkflow.getOrgCode()); wrapper.like(LoanPricingWorkflow::getOrgCode, loanPricingWorkflow.getOrgCode());
} }
return wrapper; return wrapper;
} }
/**
* 设定执行利率
*
* @param serialNum 业务方流水号
* @param executeRate 执行利率
* @return 是否成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean setExecuteRate(String serialNum, String executeRate) {
LambdaQueryWrapper<LoanPricingWorkflow> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LoanPricingWorkflow::getSerialNum, serialNum);
LoanPricingWorkflow workflow = loanPricingWorkflowMapper.selectOne(wrapper);
if (workflow == null) {
return false;
}
workflow.setExecuteRate(executeRate);
int result = loanPricingWorkflowMapper.updateById(workflow);
return result > 0;
}
} }

View File

@@ -0,0 +1,65 @@
package com.ruoyi.loanpricing.util;
import com.ruoyi.loanpricing.domain.dto.CorporateLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.dto.PersonalLoanPricingCreateDTO;
import com.ruoyi.loanpricing.domain.entity.LoanPricingWorkflow;
/**
* 利率定价转换器
*
* @author ruoyi
* @date 2025-01-19
*/
public class LoanPricingConverter {
/**
* 个人客户DTO转Entity
*
* @param dto 个人客户发起DTO
* @return 利率定价流程实体
*/
public static LoanPricingWorkflow toEntity(PersonalLoanPricingCreateDTO dto) {
LoanPricingWorkflow entity = new LoanPricingWorkflow();
// 映射共同字段
entity.setCustIsn(dto.getCustIsn());
entity.setCustType("个人");
entity.setCustName(dto.getCustName());
entity.setIdType(dto.getIdType());
entity.setIdNum(dto.getIdNum());
entity.setGuarType(dto.getGuarType());
entity.setApplyAmt(dto.getApplyAmt());
entity.setCollType(dto.getCollType());
entity.setCollThirdParty(dto.getCollThirdParty());
// 映射个人特有字段
entity.setBizProof(dto.getBizProof());
entity.setLoanLoop(dto.getLoanLoop());
return entity;
}
/**
* 企业客户DTO转Entity
*
* @param dto 企业客户发起DTO
* @return 利率定价流程实体
*/
public static LoanPricingWorkflow toEntity(CorporateLoanPricingCreateDTO dto) {
LoanPricingWorkflow entity = new LoanPricingWorkflow();
// 映射共同字段
entity.setCustIsn(dto.getCustIsn());
entity.setCustType("企业");
entity.setCustName(dto.getCustName());
entity.setIdType(dto.getIdType());
entity.setIdNum(dto.getIdNum());
entity.setGuarType(dto.getGuarType());
entity.setApplyAmt(dto.getApplyAmt());
entity.setCollType(dto.getCollType());
entity.setCollThirdParty(dto.getCollThirdParty());
// 映射企业特有字段
entity.setIsAgriGuar(dto.getIsAgriGuar());
entity.setIsGreenLoan(dto.getIsGreenLoan());
entity.setIsTechEnt(dto.getIsTechEnt());
entity.setIsTradeConstruction(dto.getIsTradeConstruction());
entity.setLoanTerm(dto.getLoanTerm());
return entity;
}
}

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = 若依管理系统 VUE_APP_TITLE = 上虞利率定价系统
BABEL_ENV = production BABEL_ENV = production

View File

@@ -17,11 +17,29 @@ export function getWorkflow(serialNum) {
}) })
} }
// 新增利率定价流程 // 创建个人客户利率定价流程
export function createWorkflow(data) { export function createPersonalWorkflow(data) {
return request({ return request({
url: '/loanPricing/workflow/create', url: '/loanPricing/workflow/create/personal',
method: 'post', method: 'post',
data: data data: data
}) })
} }
// 创建企业客户利率定价流程
export function createCorporateWorkflow(data) {
return request({
url: '/loanPricing/workflow/create/corporate',
method: 'post',
data: data
})
}
// 设定执行利率
export function setExecuteRate(serialNum, executeRate) {
return request({
url: '/loanPricing/workflow/' + serialNum + '/executeRate',
method: 'put',
data: { executeRate: executeRate }
})
}

View File

@@ -12,13 +12,15 @@
<template v-if="device!=='mobile'"> <template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item" /> <search id="header-search" class="right-menu-item" />
<el-tooltip content="源码地址" effect="dark" placement="bottom"> <!-- 源码地址按钮已隐藏 -->
<!-- <el-tooltip content="源码地址" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" /> <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
</el-tooltip> </el-tooltip> -->
<el-tooltip content="文档地址" effect="dark" placement="bottom"> <!-- 文档地址按钮已隐藏 -->
<!-- <el-tooltip content="文档地址" effect="dark" placement="bottom">
<ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" /> <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
</el-tooltip> </el-tooltip> -->
<screenfull id="screenfull" class="right-menu-item hover-effect" /> <screenfull id="screenfull" class="right-menu-item hover-effect" />

View File

@@ -68,9 +68,9 @@ export const constantRoutes = [
children: [ children: [
{ {
path: 'index', path: 'index',
component: () => import('@/views/index'), component: () => import('@/views/loanPricing/workflow/index'),
name: 'Index', name: 'LoanPricingWorkflow',
meta: { title: '首页', icon: 'dashboard', affix: true } meta: { title: '流程列表', icon: 'dashboard', affix: true }
} }
] ]
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
<div slot="header" class="card-header"> <div slot="header" class="card-header">
<span class="card-title">议价池</span> <span class="card-title">议价池</span>
</div> </div>
<el-descriptions :column="4" border size="small"> <el-descriptions :column="2" border size="small">
<el-descriptions-item label="网点议价池"> <el-descriptions-item label="网点议价池">
{{ displayBranchPool }} {{ displayBranchPool }}
</el-descriptions-item> </el-descriptions-item>

View File

@@ -0,0 +1,319 @@
<template>
<el-dialog title="新增企业利率定价流程" :visible.sync="dialogVisible" width="900px" append-to-body
@close="handleClose">
<el-form ref="form" :model="form" :rules="rules" label-width="140px" class="workflow-create-form">
<!-- 基本信息 -->
<el-divider content-position="left">基本信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="客户内码" prop="custIsn">
<el-input v-model="form.custIsn" placeholder="请输入客户内码"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户名称" prop="custName">
<el-input v-model="form.custName" placeholder="请输入客户名称"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="证件类型" prop="idType">
<el-select v-model="form.idType" placeholder="请选择证件类型" style="width: 100%">
<el-option label="统一社会信用代码" value="统一社会信用代码"/>
<el-option label="其他" value="其他"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="证件号码" prop="idNum">
<el-input v-model="form.idNum" placeholder="请输入证件号码"/>
</el-form-item>
</el-col>
</el-row>
<!-- 贷款信息 -->
<el-divider content-position="left">贷款信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="担保方式" prop="guarType">
<el-select v-model="form.guarType" placeholder="请选择担保方式" style="width: 100%">
<el-option label="信用" value="信用"/>
<el-option label="保证" value="保证"/>
<el-option label="抵押" value="抵押"/>
<el-option label="质押" value="质押"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请金额(元)" prop="applyAmt">
<el-input v-model="form.applyAmt" placeholder="请输入申请金额"/>
</el-form-item>
</el-col>
</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>
</el-col>
</el-row>
<!-- 企业标识 -->
<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>
</el-col>
</el-row>
<!-- 抵质押信息 -->
<el-divider content-position="left">抵质押信息</el-divider>
<el-row>
<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-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="抵质押物三方所有" prop="collThirdParty">
<el-switch v-model="form.collThirdParty"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="submitting" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</template>
<script>
import {createCorporateWorkflow} from "@/api/loanPricing/workflow"
export default {
name: "CorporateCreateDialog",
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
// 统一社会信用代码验证
const validateIdNum = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入证件号码'))
} else if (this.form.idType === '统一社会信用代码') {
const reg = /^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/
if (!reg.test(value)) {
callback(new Error('请输入正确的统一社会信用代码'))
} else {
callback()
}
} else {
callback()
}
}
// 金额验证
const validateApplyAmt = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入申请金额'))
} else {
const num = parseFloat(value)
if (isNaN(num) || num <= 0) {
callback(new Error('请输入有效的金额'))
} else if (num > 999999999.99) {
callback(new Error('金额不能超过 999999999.99'))
} else {
callback()
}
}
}
// 贷款期限验证
const validateLoanTerm = (rule, value, callback) => {
if (!value && value !== 0) {
callback(new Error('请输入贷款期限'))
} else {
const num = parseInt(value)
if (isNaN(num) || num <= 0) {
callback(new Error('请输入有效的贷款期限'))
} else if (num > 360) {
callback(new Error('贷款期限不能超过 360 个月'))
} else {
callback()
}
}
}
return {
submitting: false,
form: {
orgCode: '',
runType: '1',
custIsn: undefined,
custName: undefined,
idType: undefined,
idNum: undefined,
guarType: undefined,
applyAmt: undefined,
loanTerm: undefined,
isAgriGuar: false,
isGreenLoan: false,
isTechEnt: false,
isTradeConstruction: false,
collType: undefined,
collThirdParty: false
},
rules: {
custIsn: [
{required: true, message: "客户内码不能为空", trigger: "blur"},
{min: 1, max: 50, message: "长度在 1 到 50 个字符", trigger: "blur"}
],
custName: [
{required: true, message: "客户名称不能为空", trigger: "blur"},
{min: 1, max: 100, message: "长度在 1 到 100 个字符", trigger: "blur"}
],
idType: [
{required: true, message: "请选择证件类型", trigger: "change"}
],
idNum: [
{required: true, validator: validateIdNum, trigger: "blur"}
],
guarType: [
{required: true, message: "请选择担保方式", trigger: "change"}
],
applyAmt: [
{required: true, validator: validateApplyAmt, trigger: "blur"}
],
loanTerm: [
{required: true, validator: validateLoanTerm, trigger: "blur"}
],
collType: [
{required: true, message: "请选择抵质押类型", trigger: "change"}
]
}
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
watch: {
visible(val) {
if (val) {
this.reset()
}
}
},
methods: {
/** 表单重置 */
reset() {
this.form = {
orgCode: '',
runType: '1',
custIsn: undefined,
custName: undefined,
idType: undefined,
idNum: undefined,
guarType: undefined,
applyAmt: undefined,
loanTerm: undefined,
isAgriGuar: false,
isGreenLoan: false,
isTechEnt: false,
isTradeConstruction: false,
collType: undefined,
collThirdParty: false
}
this.submitting = false
this.resetForm("form")
},
/** 对话框关闭处理 */
handleClose() {
this.cancel()
},
/** 取消按钮 */
cancel() {
this.dialogVisible = false
this.reset()
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
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'
}
createCorporateWorkflow(data).then(response => {
this.$modal.msgSuccess("新增成功")
this.dialogVisible = false
this.$emit('success')
}).catch(error => {
// 保留表单,显示错误信息
console.error('创建失败:', error)
}).finally(() => {
this.submitting = false
})
}
})
}
}
}
</script>
<style scoped>
.workflow-create-form .el-divider {
margin: 8px 0 16px 0;
font-weight: 500;
font-size: 14px;
}
@media screen and (max-width: 768px) {
.workflow-create-form .el-col {
width: 100% !important;
max-width: 100% !important;
flex: 0 0 100% !important;
}
}
</style>

View File

@@ -0,0 +1,324 @@
<template>
<div class="corporate-workflow-detail" v-loading="loading">
<!-- 两栏布局左侧关键信息 + 右侧流程详情+模型输出 -->
<div v-if="!loading && detailData" class="detail-layout">
<!-- 左侧关键信息卡片 -->
<div class="left-panel">
<el-card class="summary-card">
<div slot="header" class="card-header">
<span class="card-title">关键信息</span>
</div>
<el-descriptions :column="1" direction="vertical" border>
<el-descriptions-item label="业务方流水号">{{ detailData.serialNum }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ detailData.custName }}</el-descriptions-item>
<el-descriptions-item label="客户类型">{{ detailData.custType }}</el-descriptions-item>
<el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
<el-descriptions-item label="基准利率">
<span class="rate-value">{{ getBaseLoanRate() }}</span> %
</el-descriptions-item>
<el-descriptions-item label="浮动BP">
<span class="total-bp-value">{{ getTotalBp() }}</span>
</el-descriptions-item>
<el-descriptions-item label="测算利率">
<span class="calculate-rate">{{ getCalculateRate() }}</span> %
</el-descriptions-item>
<el-descriptions-item label="执行利率">
<div class="execute-rate-input-wrapper">
<el-input
v-model="executeRateInput"
class="execute-rate-input"
placeholder="请输入执行利率"
>
<template slot="append">%</template>
</el-input>
<el-button
type="primary"
size="small"
@click="handleSetExecuteRate"
>
确定
</el-button>
</div>
</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
<!-- 右侧面板 -->
<div class="right-panel">
<!-- 流程详情卡片 -->
<el-card class="detail-card">
<div slot="header" class="card-header">
<span class="card-title">流程详情</span>
</div>
<!-- 基本信息组 -->
<div class="info-section">
<h4 class="section-title">基本信息</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="机构编码">{{ detailData.orgCode }}</el-descriptions-item>
<el-descriptions-item label="运行模式">{{ detailData.runType }}</el-descriptions-item>
<el-descriptions-item label="客户内码">{{ detailData.custIsn }}</el-descriptions-item>
<el-descriptions-item label="证件类型">{{ detailData.idType }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ detailData.idNum }}</el-descriptions-item>
<el-descriptions-item label="贷款期限">{{ detailData.loanTerm || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailData.createTime }}</el-descriptions-item>
<el-descriptions-item label="创建者">{{ detailData.createBy }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 业务信息组 -->
<div class="info-section">
<h4 class="section-title">业务信息</h4>
<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="绿色贷款">{{ formatBoolean(detailData.isGreenLoan) }}</el-descriptions-item>
<el-descriptions-item label="科技型企业">{{ formatBoolean(detailData.isTechEnt) }}</el-descriptions-item>
<el-descriptions-item label="抵质押类型">{{ detailData.collType || '-' }}</el-descriptions-item>
<el-descriptions-item label="抵质押物是否三方所有">{{
formatBoolean(detailData.collThirdParty)
}}
</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
<!-- 模型输出卡片 -->
<ModelOutputDisplay
:cust-type="detailData.custType"
:retail-output="null"
:corp-output="corpOutput"
/>
<!-- 议价池卡片 -->
<BargainingPoolDisplay
v-if="bargainingPool"
:branch-pool="bargainingPool.branchPool"
:sub-branch-pool="bargainingPool.subBranchPool"
:private-domain-pool="bargainingPool.privateDomainPool"
:excess-profit-share="bargainingPool.excessProfitShare"
/>
</div>
</div>
</div>
</template>
<script>
import {setExecuteRate} from "@/api/loanPricing/workflow"
import ModelOutputDisplay from "./ModelOutputDisplay.vue"
import BargainingPoolDisplay from "./BargainingPoolDisplay.vue"
export default {
name: "CorporateWorkflowDetail",
components: {
ModelOutputDisplay,
BargainingPoolDisplay
},
props: {
detailData: {
type: Object,
required: true
},
corpOutput: {
type: Object,
default: null
},
bargainingPool: {
type: Object,
default: null
}
},
data() {
return {
loading: false,
executeRateInput: ''
}
},
watch: {
'detailData.executeRate': {
handler(newVal) {
this.executeRateInput = newVal || ''
},
immediate: true
}
},
methods: {
/** 格式化布尔值为中文 */
formatBoolean(value) {
if (value === 'true' || value === true) return '是'
if (value === 'false' || value === false) return '否'
return value || '-'
},
/** 获取基准利率 */
getBaseLoanRate() {
return this.corpOutput?.baseLoanRate || '-'
},
/** 获取浮动BP */
getTotalBp() {
return this.corpOutput?.totalBp || '-'
},
/** 获取测算利率 */
getCalculateRate() {
return this.corpOutput?.calculateRate || '-'
},
/** 设定执行利率 */
handleSetExecuteRate() {
const value = this.executeRateInput
if (value === null || value === undefined || value === '') {
this.$modal.msgError("请输入执行利率")
return
}
const numValue = parseFloat(value)
if (isNaN(numValue)) {
this.$modal.msgError("请输入有效的数字")
return
}
if (numValue < 0 || numValue > 100) {
this.$modal.msgError("执行利率必须在 0 到 100 之间")
return
}
this.loading = true
setExecuteRate(this.detailData.serialNum, value.toString()).then(() => {
this.$modal.msgSuccess("执行利率设定成功")
this.$emit('refresh')
this.loading = false
}).catch(error => {
this.$modal.msgError("设定失败:" + (error.msg || error.message || "未知错误"))
this.loading = false
})
}
}
}
</script>
<style lang="scss" scoped>
.corporate-workflow-detail {
.detail-layout {
display: flex;
gap: 20px;
align-items: flex-start;
.left-panel {
flex: 0 0 280px;
max-width: 280px;
.summary-card {
::v-deep .el-card__header {
padding: 16px 20px;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
}
.card-header {
display: flex;
align-items: center;
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
::v-deep .el-card__body {
padding: 16px;
}
.execute-rate-input-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
.execute-rate-input {
width: 100%;
}
}
.rate-value {
color: #67c23a;
font-weight: 500;
}
.total-bp-value {
color: #e6a23c;
font-weight: 600;
}
.calculate-rate {
color: #f56c6c;
font-weight: 600;
font-size: 16px;
}
}
}
.right-panel {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 20px;
.detail-card {
::v-deep .el-card__header {
padding: 16px 20px;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
}
.card-header {
display: flex;
align-items: center;
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
::v-deep .el-card__body {
padding: 20px;
}
.info-section {
&:not(:last-child) {
margin-bottom: 24px;
}
.section-title {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 500;
color: #606266;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
}
}
}
}
}
@media screen and (max-width: 992px) {
.corporate-workflow-detail {
.detail-layout {
flex-direction: column;
.left-panel,
.right-panel {
flex: 1 1 100%;
max-width: 100%;
}
}
}
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<el-dialog title="选择客户类型" :visible.sync="dialogVisible" width="500px" append-to-body
:close-on-click-modal="false">
<div class="customer-type-selector">
<div class="type-card" @click="selectType('personal')">
<div class="card-icon">
<i class="el-icon-user"></i>
</div>
<div class="card-content">
<div class="card-title">个人客户</div>
<div class="card-desc">个人客户利率定价</div>
</div>
</div>
<div class="type-card" @click="selectType('corporate')">
<div class="card-icon">
<i class="el-icon-office-building"></i>
</div>
<div class="card-content">
<div class="card-title">企业客户</div>
<div class="card-desc">企业客户利率定价</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
name: "CustomerTypeSelector",
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
methods: {
selectType(type) {
this.$emit('select', type)
this.dialogVisible = false
}
}
}
</script>
<style scoped>
.customer-type-selector {
display: flex;
gap: 20px;
padding: 20px 0;
}
.type-card {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.type-card:hover {
border-color: #409EFF;
background-color: #f0f7ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}
.card-icon {
font-size: 48px;
color: #409EFF;
margin-bottom: 15px;
}
.card-content {
text-align: center;
}
.card-title {
font-size: 18px;
font-weight: 500;
color: #303133;
margin-bottom: 8px;
}
.card-desc {
font-size: 14px;
color: #909399;
}
</style>

View File

@@ -0,0 +1,270 @@
<template>
<el-dialog title="新增个人利率定价流程" :visible.sync="dialogVisible" width="900px" append-to-body
@close="handleClose">
<el-form ref="form" :model="form" :rules="rules" label-width="140px" class="workflow-create-form">
<!-- 基本信息 -->
<el-divider content-position="left">基本信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="客户内码" prop="custIsn">
<el-input v-model="form.custIsn" placeholder="请输入客户内码"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户名称" prop="custName">
<el-input v-model="form.custName" placeholder="请输入客户名称"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="证件类型" prop="idType">
<el-select v-model="form.idType" placeholder="请选择证件类型" style="width: 100%">
<el-option label="身份证" value="身份证"/>
<el-option label="其他证件" value="其他证件"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="证件号码" prop="idNum">
<el-input v-model="form.idNum" placeholder="请输入证件号码"/>
</el-form-item>
</el-col>
</el-row>
<!-- 贷款信息 -->
<el-divider content-position="left">贷款信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="担保方式" prop="guarType">
<el-select v-model="form.guarType" placeholder="请选择担保方式" style="width: 100%">
<el-option label="信用" value="信用"/>
<el-option label="保证" value="保证"/>
<el-option label="抵押" value="抵押"/>
<el-option label="质押" value="质押"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请金额(元)" prop="applyAmt">
<el-input v-model="form.applyAmt" placeholder="请输入申请金额"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="是否有经营佐证" prop="bizProof">
<el-switch v-model="form.bizProof"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="循环功能" prop="loanLoop">
<el-switch v-model="form.loanLoop"/>
</el-form-item>
</el-col>
</el-row>
<!-- 抵质押信息 -->
<el-divider content-position="left">抵质押信息</el-divider>
<el-row>
<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-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="抵质押物三方所有" prop="collThirdParty">
<el-switch v-model="form.collThirdParty"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" :loading="submitting" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</template>
<script>
import {createPersonalWorkflow} from "@/api/loanPricing/workflow"
export default {
name: "PersonalCreateDialog",
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
// 身份证验证
const validateIdNum = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入证件号码'))
} else if (this.form.idType === '身份证') {
const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
if (!reg.test(value)) {
callback(new Error('请输入正确的身份证号码'))
} else {
callback()
}
} else {
callback()
}
}
// 金额验证
const validateApplyAmt = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入申请金额'))
} else {
const num = parseFloat(value)
if (isNaN(num) || num <= 0) {
callback(new Error('请输入有效的金额'))
} else if (num > 999999999.99) {
callback(new Error('金额不能超过 999999999.99'))
} else {
callback()
}
}
}
return {
submitting: false,
form: {
orgCode: '',
runType: '1',
custIsn: undefined,
custName: undefined,
idType: undefined,
idNum: undefined,
guarType: undefined,
applyAmt: undefined,
bizProof: false,
loanLoop: false,
collType: undefined,
collThirdParty: false
},
rules: {
custIsn: [
{required: true, message: "客户内码不能为空", trigger: "blur"},
{min: 1, max: 50, message: "长度在 1 到 50 个字符", trigger: "blur"}
],
custName: [
{required: true, message: "客户名称不能为空", trigger: "blur"},
{min: 1, max: 100, message: "长度在 1 到 100 个字符", trigger: "blur"}
],
idType: [
{required: true, message: "请选择证件类型", trigger: "change"}
],
idNum: [
{required: true, validator: validateIdNum, trigger: "blur"}
],
guarType: [
{required: true, message: "请选择担保方式", trigger: "change"}
],
applyAmt: [
{required: true, validator: validateApplyAmt, trigger: "blur"}
],
collType: [
{required: true, message: "请选择抵质押类型", trigger: "change"}
]
}
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
watch: {
visible(val) {
if (val) {
this.reset()
}
}
},
methods: {
/** 表单重置 */
reset() {
this.form = {
orgCode: '',
runType: '1',
custIsn: undefined,
custName: undefined,
idType: undefined,
idNum: undefined,
guarType: undefined,
applyAmt: undefined,
bizProof: false,
loanLoop: false,
collType: undefined,
collThirdParty: false
}
this.submitting = false
this.resetForm("form")
},
/** 对话框关闭处理 */
handleClose() {
this.cancel()
},
/** 取消按钮 */
cancel() {
this.dialogVisible = false
this.reset()
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.submitting = true
// 转换开关值为字符串
const data = {
...this.form,
bizProof: this.form.bizProof ? 'true' : 'false',
loanLoop: this.form.loanLoop ? 'true' : 'false',
collThirdParty: this.form.collThirdParty ? 'true' : 'false'
}
createPersonalWorkflow(data).then(response => {
this.$modal.msgSuccess("新增成功")
this.dialogVisible = false
this.$emit('success')
}).catch(error => {
// 保留表单,显示错误信息
console.error('创建失败:', error)
}).finally(() => {
this.submitting = false
})
}
})
}
}
}
</script>
<style scoped>
.workflow-create-form .el-divider {
margin: 8px 0 16px 0;
font-weight: 500;
font-size: 14px;
}
@media screen and (max-width: 768px) {
.workflow-create-form .el-col {
width: 100% !important;
max-width: 100% !important;
flex: 0 0 100% !important;
}
}
</style>

View File

@@ -0,0 +1,322 @@
<template>
<div class="personal-workflow-detail" v-loading="loading">
<!-- 两栏布局左侧关键信息 + 右侧流程详情+模型输出 -->
<div v-if="!loading && detailData" class="detail-layout">
<!-- 左侧关键信息卡片 -->
<div class="left-panel">
<el-card class="summary-card">
<div slot="header" class="card-header">
<span class="card-title">关键信息</span>
</div>
<el-descriptions :column="1" direction="vertical" border>
<el-descriptions-item label="业务方流水号">{{ detailData.serialNum }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ detailData.custName }}</el-descriptions-item>
<el-descriptions-item label="客户类型">{{ detailData.custType }}</el-descriptions-item>
<el-descriptions-item label="申请金额">{{ detailData.applyAmt }} </el-descriptions-item>
<el-descriptions-item label="基准利率">
<span class="rate-value">{{ getBaseLoanRate() }}</span> %
</el-descriptions-item>
<el-descriptions-item label="浮动BP">
<span class="total-bp-value">{{ getTotalBp() }}</span>
</el-descriptions-item>
<el-descriptions-item label="测算利率">
<span class="calculate-rate">{{ getCalculateRate() }}</span> %
</el-descriptions-item>
<el-descriptions-item label="执行利率">
<div class="execute-rate-input-wrapper">
<el-input
v-model="executeRateInput"
class="execute-rate-input"
placeholder="请输入执行利率"
>
<template slot="append">%</template>
</el-input>
<el-button
type="primary"
size="small"
@click="handleSetExecuteRate"
>
确定
</el-button>
</div>
</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
<!-- 右侧面板 -->
<div class="right-panel">
<!-- 流程详情卡片 -->
<el-card class="detail-card">
<div slot="header" class="card-header">
<span class="card-title">流程详情</span>
</div>
<!-- 基本信息组 -->
<div class="info-section">
<h4 class="section-title">基本信息</h4>
<el-descriptions :column="2" border>
<el-descriptions-item label="机构编码">{{ detailData.orgCode }}</el-descriptions-item>
<el-descriptions-item label="运行模式">{{ detailData.runType }}</el-descriptions-item>
<el-descriptions-item label="客户内码">{{ detailData.custIsn }}</el-descriptions-item>
<el-descriptions-item label="证件类型">{{ detailData.idType }}</el-descriptions-item>
<el-descriptions-item label="证件号码">{{ detailData.idNum }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailData.createTime }}</el-descriptions-item>
<el-descriptions-item label="创建者">{{ detailData.createBy }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 业务信息组 -->
<div class="info-section">
<h4 class="section-title">业务信息</h4>
<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.bizProof)
}}
</el-descriptions-item>
<el-descriptions-item label="循环功能">{{ formatBoolean(detailData.loanLoop) }}</el-descriptions-item>
<el-descriptions-item label="抵质押类型">{{ detailData.collType || '-' }}</el-descriptions-item>
<el-descriptions-item label="抵质押物是否三方所有">{{
formatBoolean(detailData.collThirdParty)
}}
</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
<!-- 模型输出卡片 -->
<ModelOutputDisplay
:cust-type="detailData.custType"
:retail-output="retailOutput"
:corp-output="null"
/>
<!-- 议价池卡片 -->
<BargainingPoolDisplay
v-if="bargainingPool"
:branch-pool="bargainingPool.branchPool"
:sub-branch-pool="bargainingPool.subBranchPool"
:private-domain-pool="bargainingPool.privateDomainPool"
:excess-profit-share="bargainingPool.excessProfitShare"
/>
</div>
</div>
</div>
</template>
<script>
import {setExecuteRate} from "@/api/loanPricing/workflow"
import ModelOutputDisplay from "./ModelOutputDisplay.vue"
import BargainingPoolDisplay from "./BargainingPoolDisplay.vue"
export default {
name: "PersonalWorkflowDetail",
components: {
ModelOutputDisplay,
BargainingPoolDisplay
},
props: {
detailData: {
type: Object,
required: true
},
retailOutput: {
type: Object,
default: null
},
bargainingPool: {
type: Object,
default: null
}
},
data() {
return {
loading: false,
executeRateInput: ''
}
},
watch: {
'detailData.executeRate': {
handler(newVal) {
this.executeRateInput = newVal || ''
},
immediate: true
}
},
methods: {
/** 格式化布尔值为中文 */
formatBoolean(value) {
if (value === 'true' || value === true) return '是'
if (value === 'false' || value === false) return '否'
return value || '-'
},
/** 获取基准利率 */
getBaseLoanRate() {
return this.retailOutput?.baseLoanRate || '-'
},
/** 获取浮动BP */
getTotalBp() {
return this.retailOutput?.totalBp || '-'
},
/** 获取测算利率 */
getCalculateRate() {
return this.retailOutput?.calculateRate || '-'
},
/** 设定执行利率 */
handleSetExecuteRate() {
const value = this.executeRateInput
if (value === null || value === undefined || value === '') {
this.$modal.msgError("请输入执行利率")
return
}
const numValue = parseFloat(value)
if (isNaN(numValue)) {
this.$modal.msgError("请输入有效的数字")
return
}
if (numValue < 0 || numValue > 100) {
this.$modal.msgError("执行利率必须在 0 到 100 之间")
return
}
this.loading = true
setExecuteRate(this.detailData.serialNum, value.toString()).then(() => {
this.$modal.msgSuccess("执行利率设定成功")
this.$emit('refresh')
this.loading = false
}).catch(error => {
this.$modal.msgError("设定失败:" + (error.msg || error.message || "未知错误"))
this.loading = false
})
}
}
}
</script>
<style lang="scss" scoped>
.personal-workflow-detail {
.detail-layout {
display: flex;
gap: 20px;
align-items: flex-start;
.left-panel {
flex: 0 0 280px;
max-width: 280px;
.summary-card {
::v-deep .el-card__header {
padding: 16px 20px;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
}
.card-header {
display: flex;
align-items: center;
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
::v-deep .el-card__body {
padding: 16px;
}
.execute-rate-input-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
.execute-rate-input {
width: 100%;
}
}
.rate-value {
color: #67c23a;
font-weight: 500;
}
.total-bp-value {
color: #e6a23c;
font-weight: 600;
}
.calculate-rate {
color: #f56c6c;
font-weight: 600;
font-size: 16px;
}
}
}
.right-panel {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 20px;
.detail-card {
::v-deep .el-card__header {
padding: 16px 20px;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
}
.card-header {
display: flex;
align-items: center;
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
::v-deep .el-card__body {
padding: 20px;
}
.info-section {
&:not(:last-child) {
margin-bottom: 24px;
}
.section-title {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 500;
color: #606266;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
}
}
}
}
}
@media screen and (max-width: 992px) {
.personal-workflow-detail {
.detail-layout {
flex-direction: column;
.left-panel,
.right-panel {
flex: 1 1 100%;
max-width: 100%;
}
}
}
}
</style>

View File

@@ -1,320 +0,0 @@
<template>
<el-dialog title="新增利率定价流程" :visible.sync="dialogVisible" width="900px" append-to-body @close="handleClose">
<el-form ref="form" :model="form" :rules="rules" label-width="140px" class="workflow-create-form">
<!-- 基本信息 -->
<el-divider content-position="left">基本信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="客户内码" prop="custIsn">
<el-input v-model="form.custIsn" placeholder="请输入客户内码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户名称" prop="custName">
<el-input v-model="form.custName" placeholder="请输入客户名称" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="客户类型" prop="custType">
<el-select v-model="form.custType" placeholder="请选择客户类型" style="width: 100%">
<el-option label="个人" value="个人" />
<el-option label="企业" value="企业" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="证件类型" prop="idType">
<el-select v-model="form.idType" placeholder="请选择证件类型" style="width: 100%">
<el-option label="身份证" value="身份证" />
<el-option label="统一社会信用代码" value="统一社会信用代码" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 贷款信息 -->
<el-divider content-position="left">贷款信息</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="申请金额(元)" prop="applyAmt">
<el-input v-model="form.applyAmt" placeholder="请输入申请金额" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="贷款利率(%)" prop="loanRate">
<el-input v-model="form.loanRate" placeholder="请输入贷款利率" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="担保方式" prop="guarType">
<el-select v-model="form.guarType" placeholder="请选择担保方式" style="width: 100%">
<el-option label="信用" value="信用" />
<el-option label="保证" value="保证" />
<el-option label="抵押" value="抵押" />
<el-option label="质押" value="质押" />
</el-select>
</el-form-item>
</el-col>
<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-row>
<el-row>
<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-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否有经营佐证">
<el-switch v-model="form.bizProofActive" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="抵质押物三方所有">
<el-switch v-model="form.collThirdPartyActive" />
</el-form-item>
</el-col>
</el-row>
<!-- 中间业务标识 -->
<el-divider content-position="left">中间业务标识</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="个人快捷支付">
<el-switch v-model="form.midPerQuickPayActive" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="个人电费代扣">
<el-switch v-model="form.midPerEleDdcActive" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="企业电费代扣">
<el-switch v-model="form.midEntEleDdcActive" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="企业水费代扣">
<el-switch v-model="form.midEntWaterDdcActive" />
</el-form-item>
</el-col>
</el-row>
<!-- 企业标识 -->
<el-divider content-position="left">企业标识</el-divider>
<el-row>
<el-col :span="12">
<el-form-item label="净身企业">
<el-switch v-model="form.isCleanEntActive" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开立基本结算账户">
<el-switch v-model="form.hasSettleAcctActive" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="制造业企业">
<el-switch v-model="form.isManufacturingActive" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="省农担担保贷款">
<el-switch v-model="form.isAgriGuarActive" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="纳税信用等级A级">
<el-switch v-model="form.isTaxAActive" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="县级及以上农业龙头企业">
<el-switch v-model="form.isAgriLeadingActive" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="普惠小微借款人">
<el-switch v-model="form.isInclusiveFinanceActive" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</template>
<script>
import { createWorkflow } from "@/api/loanPricing/workflow"
export default {
name: "WorkflowCreateDialog",
props: {
visible: {
type: Boolean,
default: false
}
},
data() {
return {
form: {},
rules: {
custIsn: [
{ required: true, message: "客户内码不能为空", trigger: "blur" }
],
custType: [
{ required: true, message: "请选择客户类型", trigger: "change" }
],
guarType: [
{ required: true, message: "请选择担保方式", trigger: "change" }
],
applyAmt: [
{ required: true, message: "申请金额不能为空", trigger: "blur" }
],
loanRate: [
{ required: true, message: "贷款利率不能为空", trigger: "blur" }
]
}
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
watch: {
visible(val) {
if (val) {
this.reset()
}
}
},
methods: {
/** 表单重置 */
reset() {
this.form = {
orgCode: '',
runType: '1',
custIsn: undefined,
custName: undefined,
custType: undefined,
idType: undefined,
applyAmt: undefined,
loanRate: undefined,
guarType: undefined,
loanPurpose: undefined,
collType: undefined,
// 开关状态
bizProofActive: false,
collThirdPartyActive: false,
midPerQuickPayActive: false,
midPerEleDdcActive: false,
midEntEleDdcActive: false,
midEntWaterDdcActive: false,
isCleanEntActive: false,
hasSettleAcctActive: false,
isManufacturingActive: false,
isAgriGuarActive: false,
isTaxAActive: false,
isAgriLeadingActive: false,
isInclusiveFinanceActive: false
}
this.resetForm("form")
},
/** 对话框关闭处理 */
handleClose() {
this.reset()
},
/** 取消按钮 */
cancel() {
this.dialogVisible = false
this.reset()
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
// 转换开关值为字符串
const data = {
...this.form,
bizProof: this.form.bizProofActive ? 'true' : 'false',
collThirdParty: this.form.collThirdPartyActive ? 'true' : 'false',
midPerQuickPay: this.form.midPerQuickPayActive ? 'true' : 'false',
midPerEleDdc: this.form.midPerEleDdcActive ? 'true' : 'false',
midEntEleDdc: this.form.midEntEleDdcActive ? 'true' : 'false',
midEntWaterDdc: this.form.midEntWaterDdcActive ? 'true' : 'false',
isCleanEnt: this.form.isCleanEntActive ? 'true' : 'false',
hasSettleAcct: this.form.hasSettleAcctActive ? 'true' : 'false',
isManufacturing: this.form.isManufacturingActive ? 'true' : 'false',
isAgriGuar: this.form.isAgriGuarActive ? 'true' : 'false',
isTaxA: this.form.isTaxAActive ? 'true' : 'false',
isAgriLeading: this.form.isAgriLeadingActive ? 'true' : 'false',
isInclusiveFinance: this.form.isInclusiveFinanceActive ? 'true' : 'false'
}
// 删除开关状态字段
delete data.bizProofActive
delete data.collThirdPartyActive
delete data.midPerQuickPayActive
delete data.midPerEleDdcActive
delete data.midEntEleDdcActive
delete data.midEntWaterDdcActive
delete data.isCleanEntActive
delete data.hasSettleAcctActive
delete data.isManufacturingActive
delete data.isAgriGuarActive
delete data.isTaxAActive
delete data.isAgriLeadingActive
delete data.isInclusiveFinanceActive
createWorkflow(data).then(response => {
this.$modal.msgSuccess("新增成功")
this.dialogVisible = false
this.$emit('success')
})
}
})
}
}
}
</script>
<style scoped>
.workflow-create-form .el-divider {
margin: 8px 0 16px 0;
font-weight: 500;
font-size: 14px;
}
</style>

View File

@@ -6,124 +6,35 @@
<el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button> <el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button>
</div> </div>
<!-- 两栏布局左侧关键信息 + 右侧流程详情+模型输出 --> <!-- 根据客户类型渲染对应的详情组件 -->
<div v-if="!loading && workflowDetail" class="detail-layout"> <personal-workflow-detail
<!-- 左侧关键信息卡片 --> v-if="!loading && workflowDetail && workflowDetail.custType === '个人'"
<div class="left-panel"> :detail-data="workflowDetail"
<el-card class="summary-card">
<div slot="header" class="card-header">
<span class="card-title">关键信息</span>
</div>
<el-descriptions :column="1" direction="vertical" border>
<el-descriptions-item label="业务方流水号">{{ workflowDetail.serialNum }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ workflowDetail.custName }}</el-descriptions-item>
<el-descriptions-item label="客户类型">{{ workflowDetail.custType }}</el-descriptions-item>
<el-descriptions-item label="申请金额">{{ workflowDetail.applyAmt }} </el-descriptions-item>
<el-descriptions-item label="基准利率">
<span class="rate-value">{{ getBaseLoanRate() }}</span> %
</el-descriptions-item>
<el-descriptions-item label="浮动BP">
<span class="total-bp-value">{{ getTotalBp() }}</span>
</el-descriptions-item>
<el-descriptions-item label="测算利率">
<span class="calculate-rate">{{ getCalculateRate() }}</span> %
</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
<!-- 右侧面板包含流程详情和模型输出 -->
<div class="right-panel">
<!-- 流程详情卡片 -->
<el-card class="detail-card">
<div slot="header" class="card-header">
<span class="card-title">流程详情</span>
</div>
<el-tabs v-model="activeTab">
<!-- 基本信息 -->
<el-tab-pane label="基本信息" name="basic">
<el-descriptions :column="2" border>
<el-descriptions-item label="机构编码">{{ workflowDetail.orgCode }}</el-descriptions-item>
<el-descriptions-item label="运行模式">{{ workflowDetail.runType }}</el-descriptions-item>
<el-descriptions-item label="客户内码">{{ workflowDetail.custIsn }}</el-descriptions-item>
<el-descriptions-item label="证件类型">{{ workflowDetail.idType }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 业务信息 -->
<el-tab-pane label="业务信息" name="business">
<el-descriptions :column="2" border>
<el-descriptions-item label="贷款用途">{{ formatLoanPurpose(workflowDetail.loanPurpose) }}</el-descriptions-item>
<el-descriptions-item label="是否有经营佐证">{{ formatBoolean(workflowDetail.bizProof) }}</el-descriptions-item>
<el-descriptions-item label="抵质押类型">{{ workflowDetail.collType }}</el-descriptions-item>
<el-descriptions-item label="抵质押物三方所有">{{ formatBoolean(workflowDetail.collThirdParty) }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 中间业务标识 -->
<el-tab-pane label="中间业务标识" name="mid">
<el-descriptions :column="2" border>
<el-descriptions-item label="个人快捷支付">{{ formatBoolean(workflowDetail.midPerQuickPay) }}</el-descriptions-item>
<el-descriptions-item label="个人电费代扣">{{ formatBoolean(workflowDetail.midPerEleDdc) }}</el-descriptions-item>
<el-descriptions-item label="企业电费代扣">{{ formatBoolean(workflowDetail.midEntEleDdc) }}</el-descriptions-item>
<el-descriptions-item label="企业水费代扣">{{ formatBoolean(workflowDetail.midEntWaterDdc) }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 企业标识 -->
<el-tab-pane label="企业标识" name="ent">
<el-descriptions :column="2" border>
<el-descriptions-item label="净身企业">{{ formatBoolean(workflowDetail.isCleanEnt) }}</el-descriptions-item>
<el-descriptions-item label="开立基本结算账户">{{ formatBoolean(workflowDetail.hasSettleAcct) }}</el-descriptions-item>
<el-descriptions-item label="制造业企业">{{ formatBoolean(workflowDetail.isManufacturing) }}</el-descriptions-item>
<el-descriptions-item label="省农担担保贷款">{{ formatBoolean(workflowDetail.isAgriGuar) }}</el-descriptions-item>
<el-descriptions-item label="纳税信用等级A级">{{ formatBoolean(workflowDetail.isTaxA) }}</el-descriptions-item>
<el-descriptions-item label="县级及以上农业龙头企业">{{ formatBoolean(workflowDetail.isAgriLeading) }}</el-descriptions-item>
<el-descriptions-item label="普惠小微借款人">{{ formatBoolean(workflowDetail.isInclusiveFinance) }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 其他信息 -->
<el-tab-pane label="其他信息" name="other">
<el-descriptions :column="2" border>
<el-descriptions-item label="创建时间">{{ workflowDetail.createTime }}</el-descriptions-item>
<el-descriptions-item label="创建者">{{ workflowDetail.createBy }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ workflowDetail.updateTime }}</el-descriptions-item>
<el-descriptions-item label="更新者">{{ workflowDetail.updateBy }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 模型输出卡片 -->
<ModelOutputDisplay
:cust-type="workflowDetail.custType"
:retail-output="retailOutput" :retail-output="retailOutput"
:corp-output="corpOutput" :bargaining-pool="bargainingPool"
@refresh="getDetail"
/> />
<!-- 议价池卡片 --> <corporate-workflow-detail
<BargainingPoolDisplay v-if="!loading && workflowDetail && workflowDetail.custType === '企业'"
:branch-pool="bargainingPool && bargainingPool.branchPool" :detail-data="workflowDetail"
:sub-branch-pool="bargainingPool && bargainingPool.subBranchPool" :corp-output="corpOutput"
:private-domain-pool="bargainingPool && bargainingPool.privateDomainPool" :bargaining-pool="bargainingPool"
:excess-profit-share="bargainingPool && bargainingPool.excessProfitShare" @refresh="getDetail"
/> />
</div> </div>
</div>
</div>
</template> </template>
<script> <script>
import { getWorkflow } from "@/api/loanPricing/workflow" import {getWorkflow} from "@/api/loanPricing/workflow"
import ModelOutputDisplay from "./components/ModelOutputDisplay.vue" import PersonalWorkflowDetail from "./components/PersonalWorkflowDetail.vue"
import BargainingPoolDisplay from "./components/BargainingPoolDisplay.vue" import CorporateWorkflowDetail from "./components/CorporateWorkflowDetail.vue"
export default { export default {
name: "LoanPricingWorkflowDetail", name: "LoanPricingWorkflowDetail",
components: { components: {
ModelOutputDisplay, PersonalWorkflowDetail,
BargainingPoolDisplay CorporateWorkflowDetail
}, },
data() { data() {
return { return {
@@ -131,8 +42,7 @@ export default {
workflowDetail: null, workflowDetail: null,
retailOutput: null, retailOutput: null,
corpOutput: null, corpOutput: null,
bargainingPool: null, bargainingPool: null
activeTab: 'basic'
} }
}, },
created() { created() {
@@ -149,7 +59,6 @@ export default {
} }
getWorkflow(serialNum).then(response => { getWorkflow(serialNum).then(response => {
// 适配新的 API 响应结构 LoanPricingWorkflowVO
this.workflowDetail = response.data.loanPricingWorkflow this.workflowDetail = response.data.loanPricingWorkflow
this.retailOutput = response.data.modelRetailOutputFields this.retailOutput = response.data.modelRetailOutputFields
this.corpOutput = response.data.modelCorpOutputFields this.corpOutput = response.data.modelCorpOutputFields
@@ -163,45 +72,6 @@ export default {
/** 返回上一页 */ /** 返回上一页 */
goBack() { goBack() {
this.$router.go(-1) this.$router.go(-1)
},
/** 格式化布尔值为中文 */
formatBoolean(value) {
if (value === 'true' || value === true) return '是'
if (value === 'false' || value === false) return '否'
return value || '-'
},
/** 格式化贷款用途 */
formatLoanPurpose(value) {
if (value === 'consumer') return '消费贷款'
if (value === 'business') return '经营贷款'
return value || '-'
},
/** 获取基准利率 */
getBaseLoanRate() {
if (this.workflowDetail.custType === '个人' && this.retailOutput) {
return this.retailOutput.baseLoanRate || '-'
} else if (this.workflowDetail.custType === '企业' && this.corpOutput) {
return this.corpOutput.baseLoanRate || '-'
}
return '-'
},
/** 获取浮动BP */
getTotalBp() {
if (this.workflowDetail.custType === '个人' && this.retailOutput) {
return this.retailOutput.totalBp || '-'
} else if (this.workflowDetail.custType === '企业' && this.corpOutput) {
return this.corpOutput.totalBp || '-'
}
return '-'
},
/** 获取测算利率 */
getCalculateRate() {
if (this.workflowDetail.custType === '个人' && this.retailOutput) {
return this.retailOutput.calculateRate || '-'
} else if (this.workflowDetail.custType === '企业' && this.corpOutput) {
return this.corpOutput.calculateRate || '-'
}
return '-'
} }
} }
} }
@@ -223,112 +93,5 @@ export default {
color: #303133; color: #303133;
} }
} }
// 两栏布局
.detail-layout {
display: flex;
gap: 20px;
align-items: flex-start;
// 左侧关键信息卡片 (固定宽度)
.left-panel {
flex: 0 0 280px;
max-width: 280px;
.summary-card {
::v-deep .el-card__header {
padding: 16px 20px;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
}
.card-header {
display: flex;
align-items: center;
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
::v-deep .el-card__body {
padding: 16px;
}
// 利率值样式
.rate-value {
color: #67c23a;
font-weight: 500;
}
// TOTAL_BP 样式
.total-bp-value {
color: #e6a23c;
font-weight: 600;
}
// 测算利率高亮样式
.calculate-rate {
color: #f56c6c;
font-weight: 600;
font-size: 16px;
}
}
}
// 右侧面板:包含流程详情和模型输出
.right-panel {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 20px;
.detail-card {
::v-deep .el-card__header {
padding: 16px 20px;
background-color: #fafafa;
border-bottom: 1px solid #ebeef5;
}
.card-header {
display: flex;
align-items: center;
.card-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
}
::v-deep .el-card__body {
padding: 20px;
}
::v-deep .el-tabs__header {
margin-bottom: 20px;
}
}
}
}
}
// 响应式布局
@media screen and (max-width: 992px) {
.workflow-detail-container {
.detail-layout {
// 切换为单列垂直布局
flex-direction: column;
.left-panel,
.right-panel {
flex: 1 1 100%;
max-width: 100%;
}
}
}
} }
</style> </style>

View File

@@ -39,7 +39,6 @@
icon="el-icon-plus" icon="el-icon-plus"
size="mini" size="mini"
@click="handleAdd" @click="handleAdd"
v-hasPermi="['loanPricing:workflow:create']"
>新增</el-button> >新增</el-button>
</el-col> </el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
@@ -65,7 +64,6 @@
type="text" type="text"
icon="el-icon-view" icon="el-icon-view"
@click="handleView(scope.row)" @click="handleView(scope.row)"
v-hasPermi="['loanPricing:workflow:query']"
>查看</el-button> >查看</el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -79,19 +77,29 @@
@pagination="getList" @pagination="getList"
/> />
<!-- 创建流程对话框 --> <!-- 客户类型选择对话框 -->
<workflow-create-dialog :visible.sync="showCreateDialog" @success="handleCreateSuccess" /> <customer-type-selector :visible.sync="showTypeSelector" @select="handleSelectType"/>
<!-- 个人客户创建对话框 -->
<personal-create-dialog :visible.sync="showPersonalDialog" @success="handleCreateSuccess"/>
<!-- 企业客户创建对话框 -->
<corporate-create-dialog :visible.sync="showCorporateDialog" @success="handleCreateSuccess"/>
</div> </div>
</template> </template>
<script> <script>
import { listWorkflow } from "@/api/loanPricing/workflow" import {listWorkflow} from "@/api/loanPricing/workflow"
import WorkflowCreateDialog from "./components/WorkflowCreateDialog" import CustomerTypeSelector from "./components/CustomerTypeSelector"
import PersonalCreateDialog from "./components/PersonalCreateDialog"
import CorporateCreateDialog from "./components/CorporateCreateDialog"
export default { export default {
name: "LoanPricingWorkflow", name: "LoanPricingWorkflow",
components: { components: {
WorkflowCreateDialog CustomerTypeSelector,
PersonalCreateDialog,
CorporateCreateDialog
}, },
data() { data() {
return { return {
@@ -103,8 +111,12 @@ export default {
total: 0, total: 0,
// 利率定价流程表格数据 // 利率定价流程表格数据
workflowList: [], workflowList: [],
// 是否显示创建弹出层 // 是否显示客户类型选择弹出层
showCreateDialog: false, showTypeSelector: false,
// 是否显示个人客户创建弹出层
showPersonalDialog: false,
// 是否显示企业客户创建弹出层
showCorporateDialog: false,
// 查询参数 // 查询参数
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
@@ -147,7 +159,15 @@ export default {
}, },
/** 新增按钮操作 */ /** 新增按钮操作 */
handleAdd() { handleAdd() {
this.showCreateDialog = true this.showTypeSelector = true
},
/** 选择客户类型回调 */
handleSelectType(type) {
if (type === 'personal') {
this.showPersonalDialog = true
} else if (type === 'corporate') {
this.showCorporateDialog = true
}
}, },
/** 创建成功回调 */ /** 创建成功回调 */
handleCreateSuccess() { handleCreateSuccess() {

View File

@@ -17,8 +17,8 @@
<pane size="84"> <pane size="84">
<el-col> <el-col>
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="用户名称" prop="userName"> <el-form-item label="柜员号" prop="userName">
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 240px" @keyup.enter.native="handleQuery" /> <el-input v-model="queryParams.userName" placeholder="请输入柜员号" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="手机号码" prop="phonenumber"> <el-form-item label="手机号码" prop="phonenumber">
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable style="width: 240px" @keyup.enter.native="handleQuery" /> <el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
@@ -59,7 +59,7 @@
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" /> <el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns.userId.visible" /> <el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns.userId.visible" />
<el-table-column label="用户名称" align="center" key="userName" prop="userName" v-if="columns.userName.visible" :show-overflow-tooltip="true" /> <el-table-column label="柜员号" align="center" key="userName" prop="userName" v-if="columns.userName.visible" :show-overflow-tooltip="true" />
<el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns.nickName.visible" :show-overflow-tooltip="true" /> <el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns.nickName.visible" :show-overflow-tooltip="true" />
<el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if="columns.deptName.visible" :show-overflow-tooltip="true" /> <el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if="columns.deptName.visible" :show-overflow-tooltip="true" />
<el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns.phonenumber.visible" width="120" /> <el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns.phonenumber.visible" width="120" />
@@ -105,7 +105,7 @@
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="归属部门" prop="deptId"> <el-form-item label="归属部门" prop="deptId">
<treeselect v-model="form.deptId" :options="enabledDeptOptions" :show-count="true" placeholder="请选择归属部门" /> <treeselect v-model="form.deptId" :options="enabledDeptOptions" :show-count="true" placeholder="请选择归属部门" @input="handleDeptChange" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -123,8 +123,8 @@
</el-row> </el-row>
<el-row> <el-row>
<el-col :span="12"> <el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName"> <el-form-item v-if="form.userId == undefined" label="柜员号" prop="userName">
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" /> <el-input v-model="form.userName" placeholder="请输入柜员号" maxlength="30" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
@@ -279,7 +279,7 @@ export default {
// 列信息 // 列信息
columns: { columns: {
userId: { label: '用户编号', visible: true }, userId: { label: '用户编号', visible: true },
userName: { label: '用户名称', visible: true }, userName: { label: '柜员号', visible: true },
nickName: { label: '用户昵称', visible: true }, nickName: { label: '用户昵称', visible: true },
deptName: { label: '部门', visible: true }, deptName: { label: '部门', visible: true },
phonenumber: { label: '手机号码', visible: true }, phonenumber: { label: '手机号码', visible: true },
@@ -289,12 +289,15 @@ export default {
// 表单校验 // 表单校验
rules: { rules: {
userName: [ userName: [
{ required: true, message: "用户名称不能为空", trigger: "blur" }, { required: true, message: "柜员号不能为空", trigger: "blur" },
{ min: 2, max: 20, message: '用户名称长度必须介于 2 和 20 之间', trigger: 'blur' } { min: 2, max: 20, message: '柜员号长度必须介于 2 和 20 之间', trigger: 'blur' }
], ],
nickName: [ nickName: [
{ required: true, message: "用户昵称不能为空", trigger: "blur" } { required: true, message: "用户昵称不能为空", trigger: "blur" }
], ],
deptId: [
{ required: true, message: "归属部门不能为空", trigger: "change" }
],
password: [ password: [
{ required: true, message: "用户密码不能为空", trigger: "blur" }, { required: true, message: "用户密码不能为空", trigger: "blur" },
{ min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' }, { min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' },
@@ -370,6 +373,10 @@ export default {
this.queryParams.deptId = data.id this.queryParams.deptId = data.id
this.handleQuery() this.handleQuery()
}, },
// 归属部门选择变化事件
handleDeptChange() {
this.$refs.form.validateField('deptId')
},
// 用户状态修改 // 用户状态修改
handleStatusChange(row) { handleStatusChange(row) {
let text = row.status === "0" ? "启用" : "停用" let text = row.status === "0" ? "启用" : "停用"

View File

@@ -0,0 +1,5 @@
-- 为 loan_pricing_workflow 表添加执行利率字段
-- 执行日期: 2025-01-22
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `execute_rate` varchar(20) DEFAULT NULL COMMENT '执行利率(%)' AFTER `loan_rate`;

View File

@@ -0,0 +1,33 @@
-- ============================================================
-- 添加缺失字段到 loan_pricing_workflow 表
-- 用途:支持个人和企业客户利率定价发起接口拆分
-- 创建日期2025-01-19
-- ============================================================
-- 个人和企业共同需要的字段
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `id_num` varchar(100) DEFAULT NULL COMMENT '证件号码' AFTER `id_type`;
-- 个人客户专用字段
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false贷款合同是否开通循环功能' AFTER `biz_proof`;
-- 企业客户专用字段
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `is_trade_construction` varchar(10) DEFAULT NULL COMMENT '贸易和建筑业企业标识: true/false抵质押类上调20BP' AFTER `is_agri_guar`;
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `is_green_loan` varchar(10) DEFAULT NULL COMMENT '绿色贷款: true/false绿色贷款最多下降5BP' AFTER `is_agri_guar`;
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `is_tech_ent` varchar(10) DEFAULT NULL COMMENT '科技型企业: true/false科技型企业最多下降5BP' AFTER `is_agri_guar`;
ALTER TABLE `loan_pricing_workflow`
ADD COLUMN `loan_term` varchar(50) DEFAULT NULL COMMENT '贷款期限' AFTER `apply_amt`;
-- ============================================================
-- 字段说明:
-- id_num: 存储个人身份证号或企业统一社会信用代码
-- loan_loop: 个人客户专用,标识贷款合同是否开通循环功能
-- is_trade_construction: 企业客户专用贸易和建筑业企业标识用于利率测算时上调20BP
-- is_green_loan: 企业客户专用绿色贷款标识最多下降5BP
-- is_tech_ent: 企业客户专用科技型企业标识最多下降5BP
-- loan_term: 企业客户专用,贷款期限,单位:月/年
-- ============================================================

45
sql/fix_all_comments.sql Normal file
View File

@@ -0,0 +1,45 @@
USE `loan-pricing`;
SET NAMES utf8mb4;
ALTER TABLE loan_pricing_workflow
COMMENT = '利率定价流程表';
ALTER TABLE loan_pricing_workflow
MODIFY COLUMN `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
MODIFY COLUMN `serial_num` varchar(50) NOT NULL COMMENT '业务方流水号',
MODIFY COLUMN `model_output_id` bigint(20) DEFAULT NULL COMMENT '模型输出ID',
MODIFY COLUMN `org_code` varchar(20) NOT NULL DEFAULT '' COMMENT '机构编码',
MODIFY COLUMN `run_type` varchar(10) NOT NULL DEFAULT '1' COMMENT '运行模式: 1-同步',
MODIFY COLUMN `cust_isn` varchar(50) NOT NULL COMMENT '客户内码',
MODIFY COLUMN `cust_type` varchar(20) NOT NULL COMMENT '客户类型: 个人/企业',
MODIFY COLUMN `guar_type` varchar(20) NOT NULL COMMENT '担保方式: 信用/保证/抵押/质押',
MODIFY COLUMN `mid_per_quick_pay` varchar(10) DEFAULT NULL COMMENT '中间业务_个人_快捷支付: true/false',
MODIFY COLUMN `mid_per_ele_ddc` varchar(10) DEFAULT NULL COMMENT '中间业务_个人_电费代扣: true/false',
MODIFY COLUMN `mid_ent_ele_ddc` varchar(10) DEFAULT NULL COMMENT '中间业务_企业_电费代扣: true/false',
MODIFY COLUMN `mid_ent_water_ddc` varchar(10) DEFAULT NULL COMMENT '中间业务_企业_水费代扣: true/false',
MODIFY COLUMN `apply_amt` varchar(50) NOT NULL COMMENT '申请金额(元)',
MODIFY COLUMN `loan_term` varchar(50) DEFAULT NULL COMMENT '贷款期限',
MODIFY COLUMN `is_clean_ent` varchar(10) DEFAULT NULL COMMENT '净身企业: true/false',
MODIFY COLUMN `has_settle_acct` varchar(10) DEFAULT NULL COMMENT '开立基本结算账户: true/false',
MODIFY COLUMN `is_manufacturing` varchar(10) DEFAULT NULL COMMENT '制造业企业: true/false',
MODIFY COLUMN `is_agri_guar` varchar(10) DEFAULT NULL COMMENT '省农担担保贷款: true/false',
MODIFY COLUMN `is_tech_ent` varchar(10) DEFAULT NULL COMMENT '科技型企业: true/false科技型企业最多下降5BP',
MODIFY COLUMN `is_green_loan` varchar(10) DEFAULT NULL COMMENT '绿色贷款: true/false绿色贷款最多下降5BP',
MODIFY COLUMN `is_trade_construction` varchar(10) DEFAULT NULL COMMENT '贸易和建筑业企业标识: true/false抵质押类上调20BP',
MODIFY COLUMN `is_tax_a` varchar(10) DEFAULT NULL COMMENT '是否纳税信用等级A级: true/false',
MODIFY COLUMN `is_agri_leading` varchar(10) DEFAULT NULL COMMENT '是否县级及以上农业龙头企业: true/false',
MODIFY COLUMN `loan_purpose` varchar(20) DEFAULT NULL COMMENT '贷款用途: consumer-消费/business-经营',
MODIFY COLUMN `biz_proof` varchar(10) DEFAULT NULL COMMENT '是否有经营佐证: true/false',
MODIFY COLUMN `loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false贷款合同是否开通循环功能',
MODIFY COLUMN `coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一线/一类/二类',
MODIFY COLUMN `coll_third_party` varchar(10) DEFAULT NULL COMMENT '抵质押物是否三方所有: true/false',
MODIFY COLUMN `loan_rate` varchar(20) NOT NULL COMMENT '贷款利率',
MODIFY COLUMN `execute_rate` varchar(20) DEFAULT NULL COMMENT '执行利率(%)',
MODIFY COLUMN `cust_name` varchar(100) DEFAULT NULL COMMENT '客户名称',
MODIFY COLUMN `id_type` varchar(50) DEFAULT NULL COMMENT '证件类型',
MODIFY COLUMN `id_num` varchar(100) DEFAULT NULL COMMENT '证件号码',
MODIFY COLUMN `is_inclusive_finance` varchar(10) DEFAULT NULL COMMENT '是否普惠小微借款人: true/false',
MODIFY COLUMN `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
MODIFY COLUMN `create_time` datetime DEFAULT NULL COMMENT '创建时间',
MODIFY COLUMN `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
MODIFY COLUMN `update_time` datetime DEFAULT NULL COMMENT '更新时间';

51
sql/fix_comments.sql Normal file
View File

@@ -0,0 +1,51 @@
-- ============================================================
-- 修复 loan_pricing_workflow 表字段注释乱码
-- 使用 utf8mb4 字符集
-- ============================================================
USE `loan-pricing`;
-- 修复表注释
ALTER TABLE loan_pricing_workflow
COMMENT = '利率定价流程表';
-- 修复所有字段注释
ALTER TABLE loan_pricing_workflow
MODIFY COLUMN `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
MODIFY COLUMN `serial_num` varchar(50) NOT NULL COMMENT '业务方流水号',
MODIFY COLUMN `model_output_id` bigint(20) DEFAULT NULL COMMENT '模型输出ID',
MODIFY COLUMN `org_code` varchar(20) NOT NULL DEFAULT '' COMMENT '机构编码',
MODIFY COLUMN `run_type` varchar(10) NOT NULL DEFAULT '1' COMMENT '运行模式: 1-同步',
MODIFY COLUMN `cust_isn` varchar(50) NOT NULL COMMENT '客户内码',
MODIFY COLUMN `cust_type` varchar(20) NOT NULL COMMENT '客户类型: 个人/企业',
MODIFY COLUMN `guar_type` varchar(20) NOT NULL COMMENT '担保方式: 信用/保证/抵押/质押',
MODIFY COLUMN `mid_per_quick_pay` varchar(10) DEFAULT NULL COMMENT '中间业务_个人_快捷支付: true/false',
MODIFY COLUMN `mid_per_ele_ddc` varchar(10) DEFAULT NULL COMMENT '中间业务_个人_电费代扣: true/false',
MODIFY COLUMN `mid_ent_ele_ddc` varchar(10) DEFAULT NULL COMMENT '中间业务_企业_电费代扣: true/false',
MODIFY COLUMN `mid_ent_water_ddc` varchar(10) DEFAULT NULL COMMENT '中间业务_企业_水费代扣: true/false',
MODIFY COLUMN `apply_amt` varchar(50) NOT NULL COMMENT '申请金额(元)',
MODIFY COLUMN `loan_term` varchar(50) DEFAULT NULL COMMENT '贷款期限',
MODIFY COLUMN `is_clean_ent` varchar(10) DEFAULT NULL COMMENT '净身企业: true/false',
MODIFY COLUMN `has_settle_acct` varchar(10) DEFAULT NULL COMMENT '开立基本结算账户: true/false',
MODIFY COLUMN `is_manufacturing` varchar(10) DEFAULT NULL COMMENT '制造业企业: true/false',
MODIFY COLUMN `is_agri_guar` varchar(10) DEFAULT NULL COMMENT '省农担担保贷款: true/false',
MODIFY COLUMN `is_tech_ent` varchar(10) DEFAULT NULL COMMENT '科技型企业: true/false科技型企业最多下降5BP',
MODIFY COLUMN `is_green_loan` varchar(10) DEFAULT NULL COMMENT '绿色贷款: true/false绿色贷款最多下降5BP',
MODIFY COLUMN `is_trade_construction` varchar(10) DEFAULT NULL COMMENT '贸易和建筑业企业标识: true/false抵质押类上调20BP',
MODIFY COLUMN `is_tax_a` varchar(10) DEFAULT NULL COMMENT '是否纳税信用等级A级: true/false',
MODIFY COLUMN `is_agri_leading` varchar(10) DEFAULT NULL COMMENT '是否县级及以上农业龙头企业: true/false',
MODIFY COLUMN `loan_purpose` varchar(20) DEFAULT NULL COMMENT '贷款用途: consumer-消费/business-经营',
MODIFY COLUMN `biz_proof` varchar(10) DEFAULT NULL COMMENT '是否有经营佐证: true/false',
MODIFY COLUMN `loan_loop` varchar(10) DEFAULT NULL COMMENT '循环功能: true/false贷款合同是否开通循环功能',
MODIFY COLUMN `coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一线/一类/二类',
MODIFY COLUMN `coll_third_party` varchar(10) DEFAULT NULL COMMENT '抵质押物是否三方所有: true/false',
MODIFY COLUMN `loan_rate` varchar(20) NOT NULL COMMENT '贷款利率',
MODIFY COLUMN `execute_rate` varchar(20) DEFAULT NULL COMMENT '执行利率(%)',
MODIFY COLUMN `cust_name` varchar(100) DEFAULT NULL COMMENT '客户名称',
MODIFY COLUMN `id_type` varchar(50) DEFAULT NULL COMMENT '证件类型',
MODIFY COLUMN `id_num` varchar(100) DEFAULT NULL COMMENT '证件号码',
MODIFY COLUMN `is_inclusive_finance` varchar(10) DEFAULT NULL COMMENT '是否普惠小微借款人: true/false',
MODIFY COLUMN `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
MODIFY COLUMN `create_time` datetime DEFAULT NULL COMMENT '创建时间',
MODIFY COLUMN `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
MODIFY COLUMN `update_time` datetime DEFAULT NULL COMMENT '更新时间';

View File

@@ -0,0 +1,6 @@
USE `loan-pricing`;
SET NAMES utf8mb4;
ALTER TABLE loan_pricing_workflow
MODIFY COLUMN loan_loop varchar(10) DEFAULT NULL COMMENT '循环功能: true/false贷款合同是否开通循环功能',
MODIFY COLUMN coll_third_party varchar(10) DEFAULT NULL COMMENT '抵质押物是否三方所有: true/false',
MODIFY COLUMN is_inclusive_finance varchar(10) DEFAULT NULL COMMENT '是否普惠小微借款人: true/false';

View File

@@ -0,0 +1,8 @@
-- ============================================================
-- 修改 loan_rate 字段允许为 NULL
-- 说明loan_rate 是由模型计算后填写的,创建时不需要用户输入
-- 创建日期: 2025-02-02
-- ============================================================
ALTER TABLE `loan_pricing_workflow`
MODIFY COLUMN `loan_rate` varchar(20) DEFAULT NULL COMMENT '贷款利率(由模型计算后填写)';

View File

@@ -25,6 +25,7 @@ CREATE TABLE `loan_pricing_workflow` (
`coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一线/一类/二类', `coll_type` varchar(20) DEFAULT NULL COMMENT '抵质押类型: 一线/一类/二类',
`coll_third_party` varchar(10) DEFAULT NULL COMMENT '抵质押物是否三方所有: true/false', `coll_third_party` varchar(10) DEFAULT NULL COMMENT '抵质押物是否三方所有: true/false',
`loan_rate` varchar(20) NOT NULL COMMENT '贷款利率', `loan_rate` varchar(20) NOT NULL COMMENT '贷款利率',
`execute_rate` varchar(20) DEFAULT NULL COMMENT '执行利率(%)',
`cust_name` varchar(100) DEFAULT NULL COMMENT '客户名称', `cust_name` varchar(100) DEFAULT NULL COMMENT '客户名称',
`id_type` varchar(50) DEFAULT NULL COMMENT '证件类型', `id_type` varchar(50) DEFAULT NULL COMMENT '证件类型',
`is_inclusive_finance` varchar(10) DEFAULT NULL COMMENT '是否普惠小微借款人: true/false', `is_inclusive_finance` varchar(10) DEFAULT NULL COMMENT '是否普惠小微借款人: true/false',

141
test-execute-rate-api.sh Normal file
View File

@@ -0,0 +1,141 @@
#!/bin/bash
# 利率定价流程执行利率接口测试脚本
# 测试环境: http://localhost:8080
BASE_URL="http://localhost:8080"
LOGIN_URL="${BASE_URL}/login"
WORKFLOW_URL="${BASE_URL}/loanPricing/workflow"
# 测试账号
USERNAME="admin"
PASSWORD="admin123"
# 测试流水号(从数据库获取)
SERIAL_NUM="20260121170730386"
# 颜色输出
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "=========================================="
echo "利率定价流程执行利率接口测试"
echo "=========================================="
echo ""
# 步骤1: 登录获取 Token (使用测试接口,无需验证码)
echo -e "${YELLOW}步骤1: 登录获取 Token (使用测试接口)${NC}"
LOGIN_RESPONSE=$(curl -s -X POST "${BASE_URL}/login/test" \
-H "Content-Type: application/json" \
-d "{\"username\":\"${USERNAME}\",\"password\":\"${PASSWORD}\"}")
TOKEN=$(echo $LOGIN_RESPONSE | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -z "$TOKEN" ]; then
echo -e "${RED}登录失败,无法获取 Token${NC}"
echo "响应: $LOGIN_RESPONSE"
exit 1
fi
echo -e "${GREEN}登录成功Token: ${TOKEN:0:20}...${NC}"
echo ""
# 步骤2: 查询当前流程详情
echo -e "${YELLOW}步骤2: 查询流程详情 (GET ${WORKFLOW_URL}/${SERIAL_NUM})${NC}"
DETAILS_RESPONSE=$(curl -s -X GET "${WORKFLOW_URL}/${SERIAL_NUM}" \
-H "Authorization: Bearer ${TOKEN}")
echo "$DETAILS_RESPONSE" | python -m json.tool 2>/dev/null || echo "$DETAILS_RESPONSE"
echo ""
# 步骤3: 设定执行利率
echo -e "${YELLOW}步骤3: 设定执行利率 (PUT ${WORKFLOW_URL}/${SERIAL_NUM}/executeRate)${NC}"
SET_RATE_RESPONSE=$(curl -s -X PUT "${WORKFLOW_URL}/${SERIAL_NUM}/executeRate" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"executeRate":"4.20"}')
echo "$SET_RATE_RESPONSE" | python -m json.tool 2>/dev/null || echo "$SET_RATE_RESPONSE"
# 检查响应
CODE=$(echo $SET_RATE_RESPONSE | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" = "200" ]; then
echo -e "${GREEN}✓ 设定执行利率成功${NC}"
else
echo -e "${RED}✗ 设定执行利率失败${NC}"
fi
echo ""
# 步骤4: 再次查询详情验证 executeRate 字段
echo -e "${YELLOW}步骤4: 再次查询详情验证 executeRate 字段${NC}"
DETAILS_RESPONSE2=$(curl -s -X GET "${WORKFLOW_URL}/${SERIAL_NUM}" \
-H "Authorization: Bearer ${TOKEN}")
echo "$DETAILS_RESPONSE2" | python -m json.tool 2>/dev/null || echo "$DETAILS_RESPONSE2"
# 检查 executeRate 字段
EXECUTE_RATE=$(echo $DETAILS_RESPONSE2 | grep -o '"executeRate":"[^"]*"' | cut -d'"' -f4)
if [ "$EXECUTE_RATE" = "4.20" ]; then
echo -e "${GREEN}✓ executeRate 字段正确返回: ${EXECUTE_RATE}${NC}"
else
echo -e "${RED}✗ executeRate 字段值不正确${NC}"
fi
echo ""
# 步骤5: 更新执行利率(验证可修改)
echo -e "${YELLOW}步骤5: 更新执行利率为 3.85${NC}"
UPDATE_RATE_RESPONSE=$(curl -s -X PUT "${WORKFLOW_URL}/${SERIAL_NUM}/executeRate" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"executeRate":"3.85"}')
echo "$UPDATE_RATE_RESPONSE" | python -m json.tool 2>/dev/null || echo "$UPDATE_RATE_RESPONSE"
CODE=$(echo $UPDATE_RATE_RESPONSE | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" = "200" ]; then
echo -e "${GREEN}✓ 更新执行利率成功${NC}"
else
echo -e "${RED}✗ 更新执行利率失败${NC}"
fi
echo ""
# 步骤6: 测试不存在的流水号
echo -e "${YELLOW}步骤6: 测试不存在的流水号 (404场景)${NC}"
NOT_FOUND_RESPONSE=$(curl -s -X PUT "${WORKFLOW_URL}/99999999999999999/executeRate" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"executeRate":"4.20"}')
echo "$NOT_FOUND_RESPONSE" | python -m json.tool 2>/dev/null || echo "$NOT_FOUND_RESPONSE"
CODE=$(echo $NOT_FOUND_RESPONSE | grep -o '"code":[0-9]*' | cut -d':' -f2)
MSG=$(echo $NOT_FOUND_RESPONSE | grep -o '"msg":"[^"]*"' | cut -d'"' -f4)
if [ "$CODE" != "200" ]; then
echo -e "${GREEN}✓ 不存在的流水号正确返回错误: ${MSG}${NC}"
else
echo -e "${RED}✗ 应该返回错误但返回了成功${NC}"
fi
echo ""
# 步骤7: 测试空值
echo -e "${YELLOW}步骤7: 测试设定空值${NC}"
NULL_RATE_RESPONSE=$(curl -s -X PUT "${WORKFLOW_URL}/${SERIAL_NUM}/executeRate" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{"executeRate":null}')
echo "$NULL_RATE_RESPONSE" | python -m json.tool 2>/dev/null || echo "$NULL_RATE_RESPONSE"
CODE=$(echo $NULL_RATE_RESPONSE | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" = "200" ]; then
echo -e "${GREEN}✓ 空值设定成功${NC}"
else
echo -e "${YELLOW}空值设定响应: ${MSG}${NC}"
fi
echo ""
echo "=========================================="
echo "测试完成"
echo "=========================================="

View File

@@ -0,0 +1,224 @@
### ============================================================
### 企业客户利率定价流程接口测试脚本
### 用途:测试企业客户发起接口的各项场景
### ============================================================
### ============================================================
### 1. 获取测试 Token如果未获取
### ============================================================
POST http://localhost:8080/login/test
Content-Type: application/json
{
"username": "admin",
"password": "admin123"
}
> {%
client.test("Request executed successfully", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 200, "Response code is 200");
client.global.set("token", response.body.data.token);
});
%}
### ============================================================
### 2. 企业客户发起 - 成功场景(完整必填字段)
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/corporate
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "CORP001",
"custName": "测试科技有限公司",
"idType": "统一社会信用代码",
"idNum": "91110000100000000X",
"guarType": "抵押",
"applyAmt": "1000000",
"loanTerm": "36",
"isAgriGuar": "false",
"isGreenLoan": "true",
"isTechEnt": "true"
}
> {%
client.test("Corporate loan creation successful", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 200, "Response code is 200");
client.assert(response.body.data.custType === "企业", "Customer type is 企业");
client.assert(response.body.data.serialNum !== null, "Serial number is generated");
client.assert(response.body.data.loanTerm === "36", "Loan term is correct");
});
%}
### ============================================================
### 3. 企业客户发起 - 缺少必填字段 custIsn
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/corporate
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custName": "测试企业2",
"idType": "统一社会信用代码",
"idNum": "91110000100000001X",
"guarType": "信用",
"applyAmt": "500000"
}
> {%
client.test("Missing custIsn validation works", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 500, "Response code indicates validation error");
});
%}
### ============================================================
### 4. 企业客户发起 - 缺少必填字段 guarType
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/corporate
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "CORP002",
"custName": "测试企业3",
"idType": "统一社会信用代码",
"idNum": "91110000100000002X",
"applyAmt": "800000"
}
> {%
client.test("Missing guarType validation works", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 500, "Response code indicates validation error");
});
%}
### ============================================================
### 5. 企业客户发起 - 缺少必填字段 applyAmt
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/corporate
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "CORP003",
"custName": "测试企业4",
"guarType": "保证",
"idType": "统一社会信用代码",
"idNum": "91110000100000003X"
}
> {%
client.test("Missing applyAmt validation works", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 500, "Response code indicates validation error");
});
%}
### ============================================================
### 6. 企业客户发起 - 担保方式枚举验证失败
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/corporate
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "CORP004",
"custName": "测试企业5",
"idType": "统一社会信用代码",
"idNum": "91110000100000004X",
"guarType": "无效值",
"applyAmt": "600000"
}
> {%
client.test("Invalid guarType validation works", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 500, "Response code indicates validation error");
});
%}
### ============================================================
### 7. 企业客户发起 - 包含省农担担保贷款标识
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/corporate
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "CORP005",
"custName": "农业科技有限公司",
"idType": "统一社会信用代码",
"idNum": "91110000100000005X",
"guarType": "保证",
"applyAmt": "2000000",
"loanTerm": "60",
"isAgriGuar": "true"
}
> {%
client.test("Corporate loan with agricultural guarantee", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 200, "Response code is 200");
client.assert(response.body.data.isAgriGuar === "true", "Agricultural guarantee is set");
});
%}
### ============================================================
### 8. 企业客户发起 - 贸易和建筑业企业标识
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/corporate
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "CORP006",
"custName": "建筑工程有限公司",
"idType": "统一社会信用代码",
"idNum": "91110000100000006X",
"guarType": "质押",
"applyAmt": "1500000",
"loanTerm": "24",
"isTradeConstruction": "true"
}
> {%
client.test("Corporate loan for trade/construction", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 200, "Response code is 200");
client.assert(response.body.data.isTradeConstruction === "true", "Trade/construction flag is set");
});
%}
### ============================================================
### 9. 企业客户发起 - 所有字段必填(信用贷款场景)
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/corporate
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "CORP007",
"custName": "科技创新有限公司",
"idType": "统一社会信用代码",
"idNum": "91110000100000007X",
"guarType": "信用",
"applyAmt": "3000000",
"loanTerm": "12",
"isAgriGuar": "false",
"isGreenLoan": "true",
"isTechEnt": "true",
"isTradeConstruction": "false",
"collType": "一类",
"collThirdParty": "false"
}
> {%
client.test("Corporate loan with all required fields", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 200, "Response code is 200");
});
%}

View File

@@ -0,0 +1,311 @@
#!/bin/bash
# ============================================================
# 企业客户利率定价流程接口测试脚本
# 用途:测试企业客户发起接口的各项场景
# 使用方法: ./test_corporate_create.sh
# ============================================================
# 配置
BASE_URL="http://localhost:8080"
LOGIN_URL="${BASE_URL}/login/test"
CORPORATE_CREATE_URL="${BASE_URL}/loanPricing/workflow/create/corporate"
# 颜色输出
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 测试结果统计
TESTS_PASSED=0
TESTS_FAILED=0
# ============================================================
# 函数:打印测试结果
# ============================================================
print_result() {
local test_name="$1"
local result="$2"
if [ "$result" == "0" ]; then
echo -e "${GREEN}${NC} $test_name - ${GREEN}PASSED${NC}"
((TESTS_PASSED++))
else
echo -e "${RED}${NC} $test_name - ${RED}FAILED${NC}"
((TESTS_FAILED++))
fi
}
# ============================================================
# 步骤 1: 获取测试 Token
# ============================================================
echo "=========================================="
echo "步骤 1: 获取测试 Token"
echo "=========================================="
TOKEN_RESPONSE=$(curl -s -X POST "$LOGIN_URL" \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "admin123"
}')
TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -z "$TOKEN" ]; then
echo -e "${RED}获取 Token 失败${NC}"
echo "响应: $TOKEN_RESPONSE"
exit 1
fi
echo -e "${GREEN}Token 获取成功${NC}"
echo "Token: ${TOKEN:0:20}..."
# ============================================================
# 步骤 2: 测试企业客户发起 - 成功场景
# ============================================================
echo ""
echo "=========================================="
echo "步骤 2: 测试企业客户发起 - 成功场景"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$CORPORATE_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "CORP001",
"custName": "测试科技有限公司",
"idType": "统一社会信用代码",
"idNum": "91110000100000000X",
"guarType": "抵押",
"applyAmt": "1000000",
"loanTerm": "36",
"isAgriGuar": "false",
"isGreenLoan": "true",
"isTechEnt": "true"
}')
# 检查响应
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
CUST_TYPE=$(echo "$RESPONSE" | grep -o '"custType":"[^"]*"' | cut -d'"' -f4)
SERIAL_NUM=$(echo "$RESPONSE" | grep -o '"serialNum":"[^"]*"' | cut -d'"' -f4)
if [ "$CODE" == "200" ] && [ "$CUST_TYPE" == "企业" ] && [ -n "$SERIAL_NUM" ]; then
print_result "企业客户发起成功场景" "0"
echo " 业务流水号: $SERIAL_NUM"
else
print_result "企业客户发起成功场景" "1"
echo " 响应: $RESPONSE"
fi
# ============================================================
# 步骤 3: 测试缺少必填字段 custIsn
# ============================================================
echo ""
echo "=========================================="
echo "步骤 3: 测试缺少必填字段 custIsn"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$CORPORATE_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custName": "测试企业2",
"idType": "统一社会信用代码",
"idNum": "91110000100000001X",
"guarType": "信用",
"applyAmt": "500000"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" == "500" ]; then
print_result "缺少 custIsn 字段验证" "0"
else
print_result "缺少 custIsn 字段验证" "1"
fi
# ============================================================
# 步骤 4: 测试缺少必填字段 guarType
# ============================================================
echo ""
echo "=========================================="
echo "步骤 4: 测试缺少必填字段 guarType"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$CORPORATE_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "CORP002",
"custName": "测试企业3",
"idType": "统一社会信用代码",
"idNum": "91110000100000002X",
"applyAmt": "800000"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" == "500" ]; then
print_result "缺少 guarType 字段验证" "0"
else
print_result "缺少 guarType 字段验证" "1"
fi
# ============================================================
# 步骤 5: 测试缺少必填字段 applyAmt
# ============================================================
echo ""
echo "=========================================="
echo "步骤 5: 测试缺少必填字段 applyAmt"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$CORPORATE_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "CORP003",
"custName": "测试企业4",
"guarType": "保证",
"idType": "统一社会信用代码",
"idNum": "91110000100000003X"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" == "500" ]; then
print_result "缺少 applyAmt 字段验证" "0"
else
print_result "缺少 applyAmt 字段验证" "1"
fi
# ============================================================
# 步骤 6: 测试担保方式枚举验证失败
# ============================================================
echo ""
echo "=========================================="
echo "步骤 6: 测试担保方式枚举验证失败"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$CORPORATE_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "CORP004",
"custName": "测试企业5",
"idType": "统一社会信用代码",
"idNum": "91110000100000004X",
"guarType": "无效值",
"applyAmt": "600000"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" == "500" ]; then
print_result "担保方式枚举验证" "0"
else
print_result "担保方式枚举验证" "1"
fi
# ============================================================
# 步骤 7: 测试省农担担保贷款标识
# ============================================================
echo ""
echo "=========================================="
echo "步骤 7: 测试省农担担保贷款标识"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$CORPORATE_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "CORP005",
"custName": "农业科技有限公司",
"idType": "统一社会信用代码",
"idNum": "91110000100000005X",
"guarType": "保证",
"applyAmt": "2000000",
"loanTerm": "60",
"isAgriGuar": "true"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
IS_AGRI_GUAR=$(echo "$RESPONSE" | grep -o '"isAgriGuar":"[^"]*"' | cut -d'"' -f4)
if [ "$CODE" == "200" ] && [ "$IS_AGRI_GUAR" == "true" ]; then
print_result "省农担担保贷款标识正确保存" "0"
echo " 省农担标识: $IS_AGRI_GUAR"
else
print_result "省农担担保贷款标识正确保存" "1"
fi
# ============================================================
# 步骤 8: 测试贸易和建筑业企业标识
# ============================================================
echo ""
echo "=========================================="
echo "步骤 8: 测试贸易和建筑业企业标识"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$CORPORATE_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "CORP006",
"custName": "建筑工程有限公司",
"idType": "统一社会信用代码",
"idNum": "91110000100000006X",
"guarType": "质押",
"applyAmt": "1500000",
"loanTerm": "24",
"isTradeConstruction": "true"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
IS_TRADE_CONSTRUCTION=$(echo "$RESPONSE" | grep -o '"isTradeConstruction":"[^"]*"' | cut -d'"' -f4)
if [ "$CODE" == "200" ] && [ "$IS_TRADE_CONSTRUCTION" == "true" ]; then
print_result "贸易和建筑业企业标识正确保存" "0"
echo " 贸易建筑标识: $IS_TRADE_CONSTRUCTION"
else
print_result "贸易和建筑业企业标识正确保存" "1"
fi
# ============================================================
# 步骤 9: 测试最小必填字段
# ============================================================
echo ""
echo "=========================================="
echo "步骤 9: 测试最小必填字段"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$CORPORATE_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "CORP007",
"guarType": "信用",
"applyAmt": "3000000"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" == "200" ]; then
print_result "最小必填字段测试" "0"
else
print_result "最小必填字段测试" "1"
fi
# ============================================================
# 测试结果汇总
# ============================================================
echo ""
echo "=========================================="
echo "测试结果汇总"
echo "=========================================="
echo -e "${GREEN}通过: $TESTS_PASSED${NC}"
echo -e "${RED}失败: $TESTS_FAILED${NC}"
echo "总计: $((TESTS_PASSED + TESTS_FAILED))"
if [ $TESTS_FAILED -eq 0 ]; then
echo -e "\n${GREEN}所有测试通过!${NC}"
exit 0
else
echo -e "\n${RED}存在测试失败,请检查日志${NC}"
exit 1
fi

View File

@@ -0,0 +1,196 @@
### ============================================================
### 个人客户利率定价流程接口测试脚本
### 用途:测试个人客户发起接口的各项场景
### ============================================================
### ============================================================
### 1. 获取测试 Token
### ============================================================
POST http://localhost:8080/login/test
Content-Type: application/json
{
"username": "admin",
"password": "admin123"
}
> {%
client.test("Request executed successfully", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 200, "Response code is 200");
client.global.set("token", response.body.data.token);
});
%}
### ============================================================
### 2. 个人客户发起 - 成功场景(完整必填字段)
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/personal
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "TEST001",
"custName": "张三",
"idType": "身份证",
"idNum": "110101199001011234",
"guarType": "信用",
"applyAmt": "500000",
"bizProof": "true",
"loanLoop": "false"
}
> {%
client.test("Personal loan creation successful", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 200, "Response code is 200");
client.assert(response.body.data.custType === "个人", "Customer type is 个人");
client.assert(response.body.data.serialNum !== null, "Serial number is generated");
});
%}
### ============================================================
### 3. 个人客户发起 - 缺少必填字段 custIsn
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/personal
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custName": "张三",
"idType": "身份证",
"idNum": "110101199001011234",
"guarType": "信用",
"applyAmt": "500000"
}
> {%
client.test("Missing custIsn validation works", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 500, "Response code indicates validation error");
});
%}
### ============================================================
### 4. 个人客户发起 - 缺少必填字段 guarType
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/personal
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "TEST002",
"custName": "李四",
"idType": "身份证",
"idNum": "110101199001011235",
"applyAmt": "300000"
}
> {%
client.test("Missing guarType validation works", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 500, "Response code indicates validation error");
});
%}
### ============================================================
### 5. 个人客户发起 - 缺少必填字段 applyAmt
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/personal
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "TEST003",
"custName": "王五",
"guarType": "保证",
"idType": "身份证",
"idNum": "110101199001011236"
}
> {%
client.test("Missing applyAmt validation works", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 500, "Response code indicates validation error");
});
%}
### ============================================================
### 6. 个人客户发起 - 担保方式枚举验证失败
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/personal
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "TEST004",
"custName": "赵六",
"idType": "身份证",
"idNum": "110101199001011237",
"guarType": "无效值",
"applyAmt": "200000"
}
> {%
client.test("Invalid guarType validation works", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 500, "Response code indicates validation error");
});
%}
### ============================================================
### 7. 个人客户发起 - 包含抵质押信息
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/personal
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "TEST005",
"custName": "孙七",
"idType": "身份证",
"idNum": "110101199001011238",
"guarType": "抵押",
"applyAmt": "800000",
"bizProof": "true",
"loanLoop": "true",
"collType": "一类",
"collThirdParty": "false"
}
> {%
client.test("Personal loan with collateral info", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 200, "Response code is 200");
client.assert(response.body.data.collType === "一类", "Collateral type is correct");
client.assert(response.body.data.loanLoop === "true", "Loan loop is set");
});
%}
### ============================================================
### 8. 个人客户发起 - 所有字段必填(质押贷款场景)
### ============================================================
POST http://localhost:8080/loanPricing/workflow/create/personal
Authorization: Bearer {{token}}
Content-Type: application/json
{
"custIsn": "TEST006",
"custName": "周八",
"idType": "身份证",
"idNum": "110101199001011239",
"guarType": "质押",
"applyAmt": "100000",
"bizProof": "false",
"loanLoop": "false",
"collType": "二类",
"collThirdParty": "true"
}
> {%
client.test("Personal loan with all required fields", function () {
client.assert(response.status === 200, "Response status is 200");
client.assert(response.body.code === 200, "Response code is 200");
client.assert(response.body.data.collThirdParty === "true", "Collateral third party is set");
});
%}

View File

@@ -0,0 +1,281 @@
#!/bin/bash
# ============================================================
# 个人客户利率定价流程接口测试脚本
# 用途:测试个人客户发起接口的各项场景
# 使用方法: ./test_personal_create.sh
# ============================================================
# 配置
BASE_URL="http://localhost:8080"
LOGIN_URL="${BASE_URL}/login/test"
PERSONAL_CREATE_URL="${BASE_URL}/loanPricing/workflow/create/personal"
# 颜色输出
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 测试结果统计
TESTS_PASSED=0
TESTS_FAILED=0
# ============================================================
# 函数:打印测试结果
# ============================================================
print_result() {
local test_name="$1"
local result="$2"
if [ "$result" == "0" ]; then
echo -e "${GREEN}${NC} $test_name - ${GREEN}PASSED${NC}"
((TESTS_PASSED++))
else
echo -e "${RED}${NC} $test_name - ${RED}FAILED${NC}"
((TESTS_FAILED++))
fi
}
# ============================================================
# 步骤 1: 获取测试 Token
# ============================================================
echo "=========================================="
echo "步骤 1: 获取测试 Token"
echo "=========================================="
TOKEN_RESPONSE=$(curl -s -X POST "$LOGIN_URL" \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "admin123"
}')
TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
if [ -z "$TOKEN" ]; then
echo -e "${RED}获取 Token 失败${NC}"
echo "响应: $TOKEN_RESPONSE"
exit 1
fi
echo -e "${GREEN}Token 获取成功${NC}"
echo "Token: ${TOKEN:0:20}..."
# ============================================================
# 步骤 2: 测试个人客户发起 - 成功场景
# ============================================================
echo ""
echo "=========================================="
echo "步骤 2: 测试个人客户发起 - 成功场景"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$PERSONAL_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "TEST001",
"custName": "张三",
"idType": "身份证",
"idNum": "110101199001011234",
"guarType": "信用",
"applyAmt": "500000",
"bizProof": "true",
"loanLoop": "false"
}')
# 检查响应
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
CUST_TYPE=$(echo "$RESPONSE" | grep -o '"custType":"[^"]*"' | cut -d'"' -f4)
SERIAL_NUM=$(echo "$RESPONSE" | grep -o '"serialNum":"[^"]*"' | cut -d'"' -f4)
if [ "$CODE" == "200" ] && [ "$CUST_TYPE" == "个人" ] && [ -n "$SERIAL_NUM" ]; then
print_result "个人客户发起成功场景" "0"
echo " 业务流水号: $SERIAL_NUM"
else
print_result "个人客户发起成功场景" "1"
echo " 响应: $RESPONSE"
fi
# ============================================================
# 步骤 3: 测试缺少必填字段 custIsn
# ============================================================
echo ""
echo "=========================================="
echo "步骤 3: 测试缺少必填字段 custIsn"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$PERSONAL_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custName": "李四",
"idType": "身份证",
"idNum": "110101199001011235",
"guarType": "信用",
"applyAmt": "300000"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" == "500" ]; then
print_result "缺少 custIsn 字段验证" "0"
else
print_result "缺少 custIsn 字段验证" "1"
fi
# ============================================================
# 步骤 4: 测试缺少必填字段 guarType
# ============================================================
echo ""
echo "=========================================="
echo "步骤 4: 测试缺少必填字段 guarType"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$PERSONAL_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "TEST002",
"custName": "王五",
"idType": "身份证",
"idNum": "110101199001011236",
"applyAmt": "300000"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" == "500" ]; then
print_result "缺少 guarType 字段验证" "0"
else
print_result "缺少 guarType 字段验证" "1"
fi
# ============================================================
# 步骤 5: 测试缺少必填字段 applyAmt
# ============================================================
echo ""
echo "=========================================="
echo "步骤 5: 测试缺少必填字段 applyAmt"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$PERSONAL_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "TEST003",
"custName": "赵六",
"idType": "身份证",
"idNum": "110101199001011237",
"guarType": "保证"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" == "500" ]; then
print_result "缺少 applyAmt 字段验证" "0"
else
print_result "缺少 applyAmt 字段验证" "1"
fi
# ============================================================
# 步骤 6: 测试担保方式枚举验证失败
# ============================================================
echo ""
echo "=========================================="
echo "步骤 6: 测试担保方式枚举验证失败"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$PERSONAL_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "TEST004",
"custName": "孙七",
"idType": "身份证",
"idNum": "110101199001011238",
"guarType": "无效值",
"applyAmt": "200000"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" == "500" ]; then
print_result "担保方式枚举验证" "0"
else
print_result "担保方式枚举验证" "1"
fi
# ============================================================
# 步骤 7: 测试包含抵质押信息
# ============================================================
echo ""
echo "=========================================="
echo "步骤 7: 测试包含抵质押信息"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$PERSONAL_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "TEST005",
"custName": "周八",
"idType": "身份证",
"idNum": "110101199001011239",
"guarType": "抵押",
"applyAmt": "800000",
"bizProof": "true",
"loanLoop": "true",
"collType": "一类",
"collThirdParty": "false"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
COLL_TYPE=$(echo "$RESPONSE" | grep -o '"collType":"[^"]*"' | cut -d'"' -f4)
LOAN_LOOP=$(echo "$RESPONSE" | grep -o '"loanLoop":"[^"]*"' | cut -d'"' -f4)
if [ "$CODE" == "200" ] && [ "$COLL_TYPE" == "一类" ] && [ "$LOAN_LOOP" == "true" ]; then
print_result "抵质押信息正确保存" "0"
echo " 抵质押类型: $COLL_TYPE"
echo " 循环功能: $LOAN_LOOP"
else
print_result "抵质押信息正确保存" "1"
fi
# ============================================================
# 步骤 8: 测试最小必填字段
# ============================================================
echo ""
echo "=========================================="
echo "步骤 8: 测试最小必填字段"
echo "=========================================="
RESPONSE=$(curl -s -X POST "$PERSONAL_CREATE_URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"custIsn": "TEST006",
"guarType": "质押",
"applyAmt": "100000"
}')
CODE=$(echo "$RESPONSE" | grep -o '"code":[0-9]*' | cut -d':' -f2)
if [ "$CODE" == "200" ]; then
print_result "最小必填字段测试" "0"
else
print_result "最小必填字段测试" "1"
fi
# ============================================================
# 测试结果汇总
# ============================================================
echo ""
echo "=========================================="
echo "测试结果汇总"
echo "=========================================="
echo -e "${GREEN}通过: $TESTS_PASSED${NC}"
echo -e "${RED}失败: $TESTS_FAILED${NC}"
echo "总计: $((TESTS_PASSED + TESTS_FAILED))"
if [ $TESTS_FAILED -eq 0 ]; then
echo -e "\n${GREEN}所有测试通过!${NC}"
exit 0
else
echo -e "\n${RED}存在测试失败,请检查日志${NC}"
exit 1
fi