13 KiB
13 KiB
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) | 抵(质)押类:贸易和建筑业企业上调20BP,true/false |
| is_green_loan | 绿色贷款 | varchar(10) | 绿色贷款标识,true/false |
| is_tech_ent | 科技型企业 | varchar(10) | 科技型企业标识,true/false |
| loan_term | 贷款期限 | varchar(50) | 贷款期限,单位:月/年 |
数据库迁移 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 需要添加以下属性:
/** 证件号码 */
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)
@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)
@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 层
@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 层接口
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 实体:
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. 向后兼容性策略
- 保留原有
POST /loanPricing/workflow/create接口 - 新旧接口共存,前端可自由选择使用
- 不添加 @Deprecated 标记,保持接口的可用性
技术决策
为什么使用两个独立的 DTO 而不是一个带条件验证的 DTO?
- 类型安全:编译期就能发现字段错误
- API 文档更清晰:Swagger 只显示相关字段
- 验证更简单:不需要在 DTO 内部根据类型做条件验证
为什么保留原有接口?
- 多接口共存:前端可以选择使用新接口或继续使用原有接口
- 降级方案:如果新接口有问题,可以快速回退
- 兼容性:可能有其他系统调用该接口
文件清单
数据库相关
- 新增:
sql/add_missing_fields.sql- 数据库迁移脚本
新增文件
ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTO.javaruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTO.javaruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/util/LoanPricingConverter.java
测试文件
ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/dto/PersonalLoanPricingCreateDTOTest.javaruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/domain/dto/CorporateLoanPricingCreateDTOTest.javaruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/util/LoanPricingConverterTest.javatest_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.javaruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/ILoanPricingWorkflowService.javaruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/impl/LoanPricingWorkflowServiceImpl.java
测试脚本示例
HTTP 测试脚本格式
测试脚本使用 IntelliJ IDEA 的 .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 企业");
});
%}