# 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 ```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 企业"); }); %} ```