# 业务种类与历史贷款利率设计

## 背景

利率定价流程新增时，当前页面已经支持先选择客户类型，再通过客户号查询选择客户内码，最后打开个人或企业新增弹窗。新需求要求在新增流程时增加两个字段：

- 业务种类
- 历史贷款利率

字段和接口规则以 `doc/上虞_客户内码客户_历史利率_映射表.xlsx` 的“历史贷款合同查询选择”sheet 以及 `doc/利率定价接口.txt` 的历史合同查询接口为准。

## 已确认规则

- 个人客户和企业客户新增流程同时增加业务种类和历史贷款利率。
- 业务种类选项为：新客、存量新增、存量转贷。
- 业务种类需要保存到流程表，并在详情页展示。
- 业务种类只用于前端判断是否触发历史合同查询，不作为模型入参上传。
- 仅当业务种类选择“存量转贷”时，查询历史贷款合同。
- 存量转贷必须选择一条历史合同记录；未查询到历史合同、未选择历史合同或历史贷款利率为空时，禁止提交。
- 历史贷款利率需要保存到流程表。
- 发起利率定价的模型调用入参需要新增历史贷款利率字段。
- 历史合同查询列表展示 Excel 中“历史贷款合同查询选择”sheet 的全部返回字段，并由用户单选一条。

## 推荐方案

采用后端代理历史合同查询、前端弹窗单选的方案。

前端负责业务种类交互、触发查询、展示历史合同列表、单选回填历史贷款利率和提交前拦截。后端负责代理外部历史合同接口、统一错误处理、保存新增字段，以及在模型调用中传递历史贷款利率。

不采用前端直接调用外部网关地址的方案，避免把外部地址和 `appCode` 暴露到前端，也避免错误处理分散。

不采用进入新增弹窗前先查询历史合同的方案，因为只有存量转贷需要历史合同，前置查询会让新客和存量新增多走不必要步骤。

## 数据流

新增流程保持现有主路径：

1. 列表页点击新增。
2. 选择个人客户或企业客户。
3. 客户号查询并选择客户内码。
4. 打开个人或企业新增弹窗。
5. 在贷款信息中选择业务种类。
6. 若业务种类为新客或存量新增，直接填写其他字段并提交。
7. 若业务种类为存量转贷，前端用当前客户内码查询历史合同。
8. 前端弹出历史贷款合同选择列表。
9. 用户单选一条历史合同。
10. 前端回填该行历史贷款利率。
11. 提交创建接口。
12. 后端保存业务种类和历史贷款利率。
13. 后端调用模型时只把历史贷款利率带入模型请求。

## 历史合同查询接口设计

新增后端业务接口：

```text
GET /loanPricing/workflow/history-contract?custIsn=...
```

接口职责：

1. 校验 `custIsn` 非空。
2. 读取历史合同外部接口配置。
3. 使用 GET 调用外部接口，请求参数名为 `cust_isn`。
4. 解析外部接口返回列表。
5. 返回给前端的字段保持下划线命名。

新增配置：

```yaml
loan-rate-history:
  url: http://552f7aff0acd4c09ac3b83dbfee57fa0.apigateway.res.dc-pdt-zj96596.com/shangyu_loan_rate_history?appCode=1a89fa84abda480ba93ed73fd01ffd07
```

配置值只保存接口基址和固定 `appCode`，不包含 `cust_isn=`。代码统一通过 `UriComponentsBuilder.queryParam("cust_isn", custIsn)` 追加客户内码，避免重复参数或拼接错误。

开发和测试环境可指向本项目 mock 接口；生产配置使用真实外部接口地址。

## 历史合同返回字段

新增 `HistoryLoanContractVO`，JSON 字段按 Excel 保持下划线命名：

| 字段 | 名称 | 用途 |
| --- | --- | --- |
| `cust_isn` | 客户内码 | 前端展示 |
| `loan_contract_history` | 历史贷款合同号 | 前端展示 |
| `guar_type_history` | 历史贷款担保方式 | 前端展示 |
| `product_code_history` | 历史贷款产品代码 | 前端展示 |
| `loan_rate_history` | 历史贷款利率 | 前端展示，用户选择后作为后续参数 |
| `loan_amount_history` | 历史贷款金额 | 前端展示 |
| `loan_sign_date_history` | 历史贷款签订时间 | 前端展示 |

## 创建接口和数据模型

个人创建 DTO `PersonalLoanPricingCreateDTO` 新增：

- `businessType`
- `loanRateHistory`

企业创建 DTO `CorporateLoanPricingCreateDTO` 新增：

- `businessType`
- `loanRateHistory`

校验规则：

- `businessType` 必填。
- `businessType` 只能是新客、存量新增、存量转贷。
- `businessType=存量转贷` 时，`loanRateHistory` 必填。
- `businessType` 为新客或存量新增时，不要求 `loanRateHistory`。

流程实体 `LoanPricingWorkflow` 新增：

- `businessType`
- `loanRateHistory`

转换器 `LoanPricingConverter` 需要同步更新：

- `toEntity(PersonalLoanPricingCreateDTO dto)` 映射 `businessType` 和 `loanRateHistory`。
- `toEntity(CorporateLoanPricingCreateDTO dto)` 映射 `businessType` 和 `loanRateHistory`。

这两个字段必须先写入 `LoanPricingWorkflow`，后续详情展示和模型入参才会闭环。

数据库 `loan_pricing_workflow` 新增：

```sql
ALTER TABLE `loan_pricing_workflow`
  ADD COLUMN `business_type` varchar(20) DEFAULT NULL COMMENT '业务种类' AFTER `loan_purpose`,
  ADD COLUMN `loan_rate_history` varchar(100) DEFAULT NULL COMMENT '历史贷款利率' AFTER `business_type`;
```

需要同步更新独立 schema SQL 和生产初始化 SQL 中的 `loan_pricing_workflow` 表结构。

跨字段校验放在创建服务层统一处理，不只依赖 DTO 注解：

- 创建个人流程前校验业务种类。
- 创建企业流程前校验业务种类。
- `businessType` 必须是新客、存量新增、存量转贷之一。
- `businessType=存量转贷` 时，`loanRateHistory` 必须非空。
- 校验通过后才允许入库和触发模型调用。

这样可以拦截绕过前端直接调用创建接口的请求。

## 模型入参

模型入参 `ModelInvokeDTO` 新增：

- `loanRateHistory`

`LoanPricingModelService` 仍按现有方式从 `LoanPricingWorkflow` 复制属性到 `ModelInvokeDTO`。由于 `LoanPricingWorkflow` 和 `ModelInvokeDTO` 字段同名，历史贷款利率会进入模型请求。

`businessType` 不添加到 `ModelInvokeDTO`，不进入模型请求。

`ModelService` 仍使用现有 `application/x-www-form-urlencoded` 请求方式，不调整模型调用协议。

## 前端交互

个人和企业新增弹窗都在“贷款信息”区域增加：

- 业务种类：下拉框，选项为新客、存量新增、存量转贷。
- 历史贷款利率：只读输入框，仅当业务种类为存量转贷时展示或启用。

业务种类选择逻辑：

- 选择新客：清空已选历史合同和历史贷款利率。
- 选择存量新增：清空已选历史合同和历史贷款利率。
- 选择存量转贷：使用当前 `custIsn` 查询历史贷款合同，并打开历史合同选择弹窗。

历史贷款合同选择弹窗：

- 宽度沿用客户号查询弹窗的 80%。
- 表格首列为单选框。
- 表格展示客户内码、历史贷款合同号、历史贷款担保方式、历史贷款产品代码、历史贷款利率、历史贷款金额、历史贷款签订时间。
- 用户选择一条记录后点击确定。
- 确定后关闭弹窗，并将该行 `loan_rate_history` 回填到新增表单的 `loanRateHistory`。

提交逻辑：

- 业务种类始终必填。
- 存量转贷时必须已选择历史合同。
- 存量转贷时 `loanRateHistory` 为空则禁止提交。
- 新客和存量新增提交时不需要历史贷款利率。

## 详情展示

个人详情和企业详情都需要展示：

- 业务种类
- 历史贷款利率

展示位置放在现有贷款信息或基础信息区域，字段值来自 `loanPricingWorkflow.businessType` 和 `loanPricingWorkflow.loanRateHistory`。

若历史贷款利率为空，详情页按现有详情页空值展示方式显示。

## 异常处理

- 前端业务种类为空：提示“请选择业务种类”。
- 存量转贷未选择历史合同：提示“请选择历史贷款合同”，禁止提交。
- 存量转贷历史合同返回空列表：提示“未查询到历史贷款合同”，禁止提交。
- 历史合同查询失败：展示后端返回错误，保留当前新增弹窗，不提交。
- 后端历史合同接口 `custIsn` 为空：返回“客户内码不能为空”。
- 外部历史合同接口无返回或返回错误：后端抛出业务异常。
- 创建接口收到存量转贷但历史贷款利率为空：后端返回业务错误。

## Mock 接口

为本地开发和真实页面测试新增历史合同 mock：

```text
GET /rate/pricing/mock/history-contract?cust_isn=...
```

mock 接口返回 1 条或多条历史合同记录，字段使用历史合同返回字段中的下划线名称。

mock 需要提供固定测试场景：

- 常规 `cust_isn` 返回 1 条或多条带 `loan_rate_history` 的历史合同。
- 固定 `cust_isn` 返回空列表，用于验证“未查询到历史贷款合同”。
- 固定 `cust_isn` 返回记录但 `loan_rate_history` 为空，用于验证前端和后端禁止提交。

开发和测试 profile 的 `loan-rate-history.url` 可配置为本项目 mock 地址。

## 测试方案

### 后端验证

- 历史合同查询接口正常返回下划线字段。
- 历史合同查询接口缺少 `custIsn` 时返回错误。
- 历史合同外部接口返回空列表时，前端可收到空列表并拦截提交。
- 个人创建接口 `businessType=存量转贷` 且缺少 `loanRateHistory` 时返回错误。
- 企业创建接口 `businessType=存量转贷` 且缺少 `loanRateHistory` 时返回错误。
- 个人创建接口 `businessType` 缺失时返回错误。
- 企业创建接口 `businessType` 缺失时返回错误。
- 个人创建接口 `businessType` 非法值时返回错误。
- 企业创建接口 `businessType` 非法值时返回错误。
- 个人创建接口 `businessType=新客` 且无 `loanRateHistory` 时正常保存。
- 企业创建接口 `businessType=新客` 且无 `loanRateHistory` 时正常保存。
- 个人创建接口 `businessType=存量新增` 且无 `loanRateHistory` 时正常保存。
- 企业创建接口 `businessType=存量新增` 且无 `loanRateHistory` 时正常保存。
- 个人创建接口 `businessType=存量转贷` 且 `loanRateHistory` 为空字符串时返回错误。
- 企业创建接口 `businessType=存量转贷` 且 `loanRateHistory` 为空字符串时返回错误。
- 个人创建接口带业务种类和历史贷款利率时正常保存。
- 企业创建接口带业务种类和历史贷款利率时正常保存。
- 模型调用请求包含 `loanRateHistory`。
- 模型调用请求不包含 `businessType`。

### 前端验证

- 个人新增弹窗展示业务种类和历史贷款利率。
- 企业新增弹窗展示业务种类和历史贷款利率。
- 业务种类为新客时不触发历史合同查询。
- 业务种类为存量新增时不触发历史合同查询。
- 业务种类为存量转贷时触发历史合同查询。
- 历史合同弹窗展示全部 7 个返回字段。
- 历史合同弹窗只允许单选一条。
- 选择历史合同后回填历史贷款利率。
- 历史合同返回空列表时提示“未查询到历史贷款合同”，并禁止提交。
- 历史合同返回记录但 `loan_rate_history` 为空时，不能把空值作为有效选择提交。
- 业务种类从存量转贷切换到新客或存量新增时，清空历史贷款利率。
- 存量转贷未选择历史合同时禁止提交。

### 真实页面验证

按照项目规则，页面功能开发完成后必须启动前端页面，并用浏览器打开真实利率定价流程页面验证，禁止打开 prototype 页面。

真实页面至少覆盖：

1. 个人新客新增并提交。
2. 个人存量新增新增并提交。
3. 个人存量转贷查询历史合同、单选、回填历史贷款利率、提交、详情展示。
4. 企业新客新增并提交。
5. 企业存量新增新增并提交。
6. 企业存量转贷查询历史合同、单选、回填历史贷款利率、提交、详情展示。
7. 存量转贷不选择历史合同直接提交时被拦截。

测试结束后关闭本次测试启动的前端和后端进程。

## 不在本次范围

- 不修改客户号与客户内码映射流程。
- 不改变模型调用协议。
- 不把业务种类作为模型入参。
- 不新增历史合同落库明细表。
- 不保存完整历史合同记录，只保存用户选择后的历史贷款利率。
