Files
loan-pricing/openspec/changes/split-pricing-creation-interface/design.md
2026-02-02 15:25:38 +08:00

13 KiB
Raw Blame History

Design: 拆分个人和企业利率定价发起接口

数据库变更

缺失字段分析

经过对比 person.csvcorp.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

-- 添加缺失的字段到 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. 向后兼容性策略

  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 文件格式,示例如下:

### 获取测试 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 企业");
    });
%}