Files
ccdi/docs/plans/2026-02-11-cust-fmy-relation-backend.md
wkc 2037ee81f1 feat: 优化信贷客户家庭关系页面与员工亲属关系保持一致
- 添加状态筛选条件
- 添加详情查看功能
- 添加表单状态编辑功能
- 添加查看导入失败记录按钮
- 统一按钮顺序和颜色(新增/导入/导出/查看失败记录)
- 统一表单布局(分隔线、gutter、宽度800px)
- 优化导入失败记录功能(分页、清除历史记录)
- 统一操作按钮文字(详情/编辑/删除)
- 添加创建时间格式化显示
- 添加完整导入状态管理和轮询机制
2026-02-11 16:44:28 +08:00

59 KiB
Raw Permalink Blame History

信贷客户家庭关系维护功能 - 后端实施计划

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

目标: 开发信贷客户家庭关系维护功能的完整后端实现,包括数据库设计、实体类、DTO/VO、Mapper、Service和Controller

架构: 完全复用员工亲属关系维护功能的实现逻辑,创建独立模块 CustFamilyRelation,新建独立表 ccdi_cust_fmy_relation

技术栈: Spring Boot 3.5.8 + MyBatis Plus 3.5.10 + EasyExcel + Redis


前置条件

环境要求

  • JDK 17+
  • Maven 3.6+
  • MySQL 8.2.0
  • Redis (用于导入任务状态管理)

依赖服务

  • 数据库连接配置在 application.yml 中已配置
  • MyBatis Plus 3.5.10 已集成
  • EasyExcel 已添加到项目依赖

参考模块

  • ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/**/CcdiStaffFmyRelation* - 员工亲属关系实现(参考模板)

任务列表

Task 0: 创建数据库表

Files:

  • Create: sql/ccdi_cust_fmy_relation.sql

Step 1: 创建建表SQL文件

创建 sql/ccdi_cust_fmy_relation.sql 文件:

-- 信贷客户家庭关系表
CREATE TABLE `ccdi_cust_fmy_relation` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `person_id` VARCHAR(50) NOT NULL COMMENT '信贷客户身份证号',
  `relation_type` VARCHAR(50) NOT NULL COMMENT '关系类型',
  `relation_name` VARCHAR(100) NOT NULL COMMENT '关系人姓名',
  `gender` CHAR(1) DEFAULT NULL COMMENT '性别M-男F-女O-其他',
  `birth_date` DATE DEFAULT NULL COMMENT '关系人出生日期',
  `relation_cert_type` VARCHAR(50) NOT NULL COMMENT '证件类型',
  `relation_cert_no` VARCHAR(50) NOT NULL COMMENT '证件号码',
  `mobile_phone1` VARCHAR(20) DEFAULT NULL COMMENT '手机号码1',
  `mobile_phone2` VARCHAR(20) DEFAULT NULL COMMENT '手机号码2',
  `wechat_no1` VARCHAR(50) DEFAULT NULL COMMENT '微信名称1',
  `wechat_no2` VARCHAR(50) DEFAULT NULL COMMENT '微信名称2',
  `wechat_no3` VARCHAR(50) DEFAULT NULL COMMENT '微信名称3',
  `contact_address` VARCHAR(500) DEFAULT NULL COMMENT '详细联系地址',
  `relation_desc` VARCHAR(500) DEFAULT NULL COMMENT '关系详细描述',
  `status` INT NOT NULL DEFAULT 1 COMMENT '状态0-无效1-有效',
  `effective_date` DATETIME DEFAULT NULL COMMENT '关系生效日期',
  `invalid_date` DATETIME DEFAULT NULL COMMENT '关系失效日期',
  `remark` TEXT COMMENT '备注信息',
  `data_source` VARCHAR(50) DEFAULT NULL COMMENT '数据来源MANUAL-手动录入IMPORT-批量导入',
  `is_emp_family` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否是员工的家庭关系0-否',
  `is_cust_family` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否是信贷客户的家庭关系1-是',
  `created_by` VARCHAR(50) NOT NULL COMMENT '记录创建人',
  `updated_by` VARCHAR(50) DEFAULT NULL COMMENT '记录更新人',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
  `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_person_id` (`person_id`),
  KEY `idx_relation_cert_no` (`relation_cert_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='信贷客户家庭关系表';

Step 2: 执行SQL创建表

使用MCP连接数据库工具执行SQL文件:

# 连接数据库并执行建表脚本
mysql -u <username> -p <database> < sql/ccdi_cust_fmy_relation.sql

验证方式:

SHOW CREATE TABLE ccdi_cust_fmy_relation;

预期结果: 表创建成功,包含所有字段和索引

Step 3: Commit

git add sql/ccdi_cust_fmy_relation.sql
git commit -m "feat: 创建信贷客户家庭关系表"

Task 1: 创建实体类

Files:

  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/CcdiCustFmyRelation.java
  • Reference: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/CcdiStaffFmyRelation.java

Step 1: 复制并修改实体类

复制 CcdiStaffFmyRelation.java,进行以下修改:

  1. 类名和注释:
/**
 * 信贷客户家庭关系对象 ccdi_cust_fmy_relation
 *
 * @author ruoyi
 * @date 2026-02-11
 */
@Data
@TableName("ccdi_cust_fmy_relation")
public class CcdiCustFmyRelation implements Serializable {
  1. 关键注释修改:

    • /** 信贷客户身份证号 */ (原"员工身份证号")
    • /** 是否是客户亲属1-是 */
    • /** 是否是员工亲属0-否 */
  2. 完整代码结构:

package com.ruoyi.ccdi.domain;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.io.Serial;
import java.io.Serializable;
import java.util.Date;

@Data
@TableName("ccdi_cust_fmy_relation")
public class CcdiCustFmyRelation implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /** 主键ID */
    @TableId(type = IdType.AUTO)
    private Long id;

    /** 信贷客户身份证号 */
    private String personId;

    /** 关系类型 */
    private String relationType;

    /** 关系人姓名 */
    private String relationName;

    /** 性别M-男F-女O-其他 */
    private String gender;

    /** 出生日期 */
    private Date birthDate;

    /** 关系人证件类型 */
    private String relationCertType;

    /** 关系人证件号码 */
    private String relationCertNo;

    /** 手机号码1 */
    private String mobilePhone1;

    /** 手机号码2 */
    private String mobilePhone2;

    /** 微信名称1 */
    private String wechatNo1;

    /** 微信名称2 */
    private String wechatNo2;

    /** 微信名称3 */
    private String wechatNo3;

    /** 详细联系地址 */
    private String contactAddress;

    /** 关系详细描述 */
    private String relationDesc;

    /** 状态0-无效1-有效 */
    private Integer status;

    /** 生效日期 */
    private Date effectiveDate;

    /** 失效日期 */
    private Date invalidDate;

    /** 备注 */
    private String remark;

    /** 数据来源MANUAL-手工录入IMPORT-导入 */
    private String dataSource;

    /** 是否是员工亲属0-否 */
    private Boolean isEmpFamily;

    /** 是否是客户亲属1-是 */
    private Boolean isCustFamily;

    /** 创建时间 */
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;

    /** 更新时间 */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;

    /** 创建人 */
    @TableField(fill = FieldFill.INSERT)
    private String createdBy;

    /** 更新人 */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String updatedBy;
}

Step 2: 编译验证

mvn compile -pl ruoyi-ccdi

预期结果: BUILD SUCCESS

Step 3: Commit

git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/CcdiCustFmyRelation.java
git commit -m "feat: 添加信贷客户家庭关系实体类"

Task 2: 创建DTO类

Files:

  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustFmyRelationAddDTO.java
  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustFmyRelationEditDTO.java
  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiCustFmyRelationQueryDTO.java

Step 1: 创建AddDTO

复制 CcdiStaffFmyRelationAddDTO.java,修改:

package com.ruoyi.ccdi.domain.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

import java.io.Serial;
import java.io.Serializable;
import java.util.Date;

/**
 * 信贷客户家庭关系新增DTO
 *
 * @author ruoyi
 * @date 2026-02-11
 */
@Data
@Schema(description = "信贷客户家庭关系新增")
public class CcdiCustFmyRelationAddDTO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /** 信贷客户身份证号 */
    @Schema(description = "信贷客户身份证号")
    @NotBlank(message = "信贷客户身份证号不能为空")
    private String personId;

    /** 关系类型 */
    @Schema(description = "关系类型")
    @NotBlank(message = "关系类型不能为空")
    private String relationType;

    /** 关系人姓名 */
    @Schema(description = "关系人姓名")
    @NotBlank(message = "关系人姓名不能为空")
    private String relationName;

    /** 性别 */
    @Schema(description = "性别M-男F-女O-其他")
    private String gender;

    /** 出生日期 */
    @Schema(description = "关系人出生日期")
    private Date birthDate;

    /** 关系人证件类型 */
    @Schema(description = "关系人证件类型")
    @NotBlank(message = "关系人证件类型不能为空")
    private String relationCertType;

    /** 关系人证件号码 */
    @Schema(description = "关系人证件号码")
    @NotBlank(message = "关系人证件号码不能为空")
    private String relationCertNo;

    /** 手机号码1 */
    @Schema(description = "手机号码1")
    private String mobilePhone1;

    /** 手机号码2 */
    @Schema(description = "手机号码2")
    private String mobilePhone2;

    /** 微信名称1 */
    @Schema(description = "微信名称1")
    private String wechatNo1;

    /** 微信名称2 */
    @Schema(description = "微信名称2")
    private String wechatNo2;

    /** 微信名称3 */
    @Schema(description = "微信名称3")
    private String wechatNo3;

    /** 详细联系地址 */
    @Schema(description = "详细联系地址")
    private String contactAddress;

    /** 关系详细描述 */
    @Schema(description = "关系详细描述")
    private String relationDesc;

    /** 生效日期 */
    @Schema(description = "关系生效日期")
    private Date effectiveDate;

    /** 失效日期 */
    @Schema(description = "关系失效日期")
    private Date invalidDate;

    /** 备注 */
    @Schema(description = "备注信息")
    private String remark;
}

Step 2: 创建EditDTO

复制 CcdiStaffFmyRelationEditDTO.java,修改:

package com.ruoyi.ccdi.domain.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

import java.io.Serial;
import java.io.Serializable;
import java.util.Date;

/**
 * 信贷客户家庭关系编辑DTO
 *
 * @author ruoyi
 * @date 2026-02-11
 */
@Data
@Schema(description = "信贷客户家庭关系编辑")
public class CcdiCustFmyRelationEditDTO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /** 主键ID */
    @Schema(description = "主键ID")
    @NotNull(message = "ID不能为空")
    private Long id;

    /** 信贷客户身份证号 */
    @Schema(description = "信贷客户身份证号")
    @NotBlank(message = "信贷客户身份证号不能为空")
    private String personId;

    /** 关系类型 */
    @Schema(description = "关系类型")
    @NotBlank(message = "关系类型不能为空")
    private String relationType;

    /** 关系人姓名 */
    @Schema(description = "关系人姓名")
    @NotBlank(message = "关系人姓名不能为空")
    private String relationName;

    /** 性别 */
    @Schema(description = "性别M-男F-女O-其他")
    private String gender;

    /** 出生日期 */
    @Schema(description = "关系人出生日期")
    private Date birthDate;

    /** 关系人证件类型 */
    @Schema(description = "关系人证件类型")
    @NotBlank(message = "关系人证件类型不能为空")
    private String relationCertType;

    /** 关系人证件号码 */
    @Schema(description = "关系人证件号码")
    @NotBlank(message = "关系人证件号码不能为空")
    private String relationCertNo;

    /** 手机号码1 */
    @Schema(description = "手机号码1")
    private String mobilePhone1;

    /** 手机号码2 */
    @Schema(description = "手机号码2")
    private String mobilePhone2;

    /** 微信名称1 */
    @Schema(description = "微信名称1")
    private String wechatNo1;

    /** 微信名称2 */
    @Schema(description = "微信名称2")
    private String wechatNo2;

    /** 微信名称3 */
    @Schema(description = "微信名称3")
    private String wechatNo3;

    /** 详细联系地址 */
    @Schema(description = "详细联系地址")
    private String contactAddress;

    /** 关系详细描述 */
    @Schema(description = "关系详细描述")
    private String relationDesc;

    /** 状态 */
    @Schema(description = "状态0-无效1-有效")
    @NotNull(message = "状态不能为空")
    private Integer status;

    /** 生效日期 */
    @Schema(description = "关系生效日期")
    private Date effectiveDate;

    /** 失效日期 */
    @Schema(description = "关系失效日期")
    private Date invalidDate;

    /** 备注 */
    @Schema(description = "备注信息")
    private String remark;
}

Step 3: 创建QueryDTO

package com.ruoyi.ccdi.domain.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.io.Serial;
import java.io.Serializable;

/**
 * 信贷客户家庭关系查询DTO
 *
 * @author ruoyi
 * @date 2026-02-11
 */
@Data
@Schema(description = "信贷客户家庭关系查询")
public class CcdiCustFmyRelationQueryDTO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /** 信贷客户身份证号 */
    @Schema(description = "信贷客户身份证号")
    private String personId;

    /** 关系类型 */
    @Schema(description = "关系类型")
    private String relationType;

    /** 关系人姓名 */
    @Schema(description = "关系人姓名")
    private String relationName;
}

Step 4: 编译验证

mvn compile -pl ruoyi-ccdi

预期结果: BUILD SUCCESS

Step 5: Commit

git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/
git commit -m "feat: 添加信贷客户家庭关系DTO类"

Task 3: 创建VO类

Files:

  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CcdiCustFmyRelationVO.java
  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CustFmyRelationImportFailureVO.java

Step 1: 创建主VO

复制 CcdiStaffFmyRelationVO.java,进行以下修改:

  1. 移除 personName 字段 (不关联员工表)
  2. 修改类名和注释
package com.ruoyi.ccdi.domain.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.io.Serial;
import java.io.Serializable;
import java.util.Date;

/**
 * 信贷客户家庭关系VO
 *
 * @author ruoyi
 * @date 2026-02-11
 */
@Data
@Schema(description = "信贷客户家庭关系")
public class CcdiCustFmyRelationVO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /** 主键ID */
    @Schema(description = "主键ID")
    private Long id;

    /** 信贷客户身份证号 */
    @Schema(description = "信贷客户身份证号")
    private String personId;

    /** 关系类型 */
    @Schema(description = "关系类型")
    private String relationType;

    /** 关系人姓名 */
    @Schema(description = "关系人姓名")
    private String relationName;

    /** 性别 */
    @Schema(description = "性别M-男F-女O-其他")
    private String gender;

    /** 出生日期 */
    @Schema(description = "关系人出生日期")
    private Date birthDate;

    /** 关系人证件类型 */
    @Schema(description = "关系人证件类型")
    private String relationCertType;

    /** 关系人证件号码 */
    @Schema(description = "关系人证件号码")
    private String relationCertNo;

    /** 手机号码1 */
    @Schema(description = "手机号码1")
    private String mobilePhone1;

    /** 手机号码2 */
    @Schema(description = "手机号码2")
    private String mobilePhone2;

    /** 微信名称1 */
    @Schema(description = "微信名称1")
    private String wechatNo1;

    /** 微信名称2 */
    @Schema(description = "微信名称2")
    private String wechatNo2;

    /** 微信名称3 */
    @Schema(description = "微信名称3")
    private String wechatNo3;

    /** 详细联系地址 */
    @Schema(description = "详细联系地址")
    private String contactAddress;

    /** 关系详细描述 */
    @Schema(description = "关系详细描述")
    private String relationDesc;

    /** 状态 */
    @Schema(description = "状态0-无效1-有效")
    private Integer status;

    /** 生效日期 */
    @Schema(description = "关系生效日期")
    private Date effectiveDate;

    /** 失效日期 */
    @Schema(description = "关系失效日期")
    private Date invalidDate;

    /** 备注 */
    @Schema(description = "备注信息")
    private String remark;

    /** 数据来源 */
    @Schema(description = "数据来源MANUAL-手工录入IMPORT-导入")
    private String dataSource;

    /** 是否是员工亲属 */
    @Schema(description = "是否是员工亲属0-否")
    private Boolean isEmpFamily;

    /** 是否是客户亲属 */
    @Schema(description = "是否是客户亲属1-是")
    private Boolean isCustFamily;

    /** 创建时间 */
    @Schema(description = "创建时间")
    private Date createTime;

    /** 更新时间 */
    @Schema(description = "更新时间")
    private Date updateTime;

    /** 创建人 */
    @Schema(description = "创建人")
    private String createdBy;

    /** 更新人 */
    @Schema(description = "更新人")
    private String updatedBy;
}

Step 2: 创建导入失败VO

复制 StaffFmyRelationImportFailureVO.java,修改:

package com.ruoyi.ccdi.domain.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.io.Serial;
import java.io.Serializable;

/**
 * 信贷客户家庭关系导入失败VO
 *
 * @author ruoyi
 * @date 2026-02-11
 */
@Data
@Schema(description = "信贷客户家庭关系导入失败记录")
public class CustFmyRelationImportFailureVO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /** 行号 */
    @Schema(description = "行号")
    private Integer rowNum;

    /** 信贷客户身份证号 */
    @Schema(description = "信贷客户身份证号")
    private String personId;

    /** 关系类型 */
    @Schema(description = "关系类型")
    private String relationType;

    /** 关系人姓名 */
    @Schema(description = "关系人姓名")
    private String relationName;

    /** 错误消息 */
    @Schema(description = "错误消息")
    private String errorMessage;
}

Step 3: 编译验证

mvn compile -pl ruoyi-ccdi

预期结果: BUILD SUCCESS

Step 4: Commit

git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/
git commit -m "feat: 添加信贷客户家庭关系VO类"

Task 4: 创建Excel类

Files:

  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiCustFmyRelationExcel.java

Step 1: 创建Excel类

复制 CcdiStaffFmyRelationExcel.java,修改:

package com.ruoyi.ccdi.domain.excel;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;

import java.io.Serial;
import java.io.Serializable;
import java.util.Date;

/**
 * 信贷客户家庭关系Excel导入导出对象
 *
 * @author ruoyi
 * @date 2026-02-11
 */
@Data
@ContentRowHeight(20)
@HeadRowHeight(30)
public class CcdiCustFmyRelationExcel implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /** 信贷客户身份证号 */
    @ExcelProperty(value = "信贷客户身份证号*", index = 0)
    @ColumnWidth(20)
    private String personId;

    /** 关系类型 */
    @ExcelProperty(value = "关系类型*", index = 1)
    @ColumnWidth(15)
    private String relationType;

    /** 关系人姓名 */
    @ExcelProperty(value = "关系人姓名*", index = 2)
    @ColumnWidth(15)
    private String relationName;

    /** 性别 */
    @ExcelProperty(value = "性别", index = 3)
    @ColumnWidth(10)
    private String gender;

    /** 出生日期 */
    @ExcelProperty(value = "出生日期", index = 4)
    @ColumnWidth(15)
    private Date birthDate;

    /** 关系人证件类型 */
    @ExcelProperty(value = "关系人证件类型*", index = 5)
    @ColumnWidth(15)
    private String relationCertType;

    /** 关系人证件号码 */
    @ExcelProperty(value = "关系人证件号码*", index = 6)
    @ColumnWidth(20)
    private String relationCertNo;

    /** 手机号码1 */
    @ExcelProperty(value = "手机号码1", index = 7)
    @ColumnWidth(15)
    private String mobilePhone1;

    /** 手机号码2 */
    @ExcelProperty(value = "手机号码2", index = 8)
    @ColumnWidth(15)
    private String mobilePhone2;

    /** 微信名称1 */
    @ExcelProperty(value = "微信名称1", index = 9)
    @ColumnWidth(15)
    private String wechatNo1;

    /** 微信名称2 */
    @ExcelProperty(value = "微信名称2", index = 10)
    @ColumnWidth(15)
    private String wechatNo2;

    /** 微信名称3 */
    @ExcelProperty(value = "微信名称3", index = 11)
    @ColumnWidth(15)
    private String wechatNo3;

    /** 详细联系地址 */
    @ExcelProperty(value = "详细联系地址", index = 12)
    @ColumnWidth(30)
    private String contactAddress;

    /** 关系详细描述 */
    @ExcelProperty(value = "关系详细描述", index = 13)
    @ColumnWidth(30)
    private String relationDesc;

    /** 状态 */
    @ExcelProperty(value = "状态", index = 14)
    @ColumnWidth(10)
    private Integer status;

    /** 生效日期 */
    @ExcelProperty(value = "生效日期", index = 15)
    @ColumnWidth(18)
    private Date effectiveDate;

    /** 失效日期 */
    @ExcelProperty(value = "失效日期", index = 16)
    @ColumnWidth(18)
    private Date invalidDate;

    /** 备注 */
    @ExcelProperty(value = "备注", index = 17)
    @ColumnWidth(30)
    private String remark;
}

Step 2: 编译验证

mvn compile -pl ruoyi-ccdi

预期结果: BUILD SUCCESS

Step 3: Commit

git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiCustFmyRelationExcel.java
git commit -m "feat: 添加信贷客户家庭关系Excel类"

Task 5: 创建Mapper接口

Files:

  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiCustFmyRelationMapper.java

Step 1: 创建Mapper接口

复制 CcdiStaffFmyRelationMapper.java,修改:

package com.ruoyi.ccdi.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.CcdiCustFmyRelation;
import com.ruoyi.ccdi.domain.dto.CcdiCustFmyRelationQueryDTO;
import com.ruoyi.ccdi.domain.vo.CcdiCustFmyRelationVO;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 信贷客户家庭关系Mapper接口
 *
 * @author ruoyi
 * @date 2026-02-11
 */
public interface CcdiCustFmyRelationMapper extends BaseMapper<CcdiCustFmyRelation> {

    /**
     * 分页查询信贷客户家庭关系
     *
     * @param page 分页对象
     * @param query 查询条件
     * @return 信贷客户家庭关系VO列表
     */
    Page<CcdiCustFmyRelationVO> selectRelationPage(Page<CcdiCustFmyRelationVO> page,
                                                     @Param("query") CcdiCustFmyRelationQueryDTO query);

    /**
     * 根据ID查询信贷客户家庭关系详情
     *
     * @param id 主键ID
     * @return 信贷客户家庭关系VO
     */
    CcdiCustFmyRelationVO selectRelationById(@Param("id") Long id);

    /**
     * 查询已存在的关系记录(用于导入校验)
     *
     * @param personId 信贷客户身份证号
     * @param relationType 关系类型
     * @param relationCertNo 关系人证件号码
     * @return 已存在的关系记录
     */
    CcdiCustFmyRelation selectExistingRelations(@Param("personId") String personId,
                                                 @Param("relationType") String relationType,
                                                 @Param("relationCertNo") String relationCertNo);

    /**
     * 批量插入信贷客户家庭关系
     *
     * @param relations 信贷客户家庭关系列表
     * @return 插入条数
     */
    int insertBatch(@Param("relations") List<CcdiCustFmyRelation> relations);

    /**
     * 根据证件号码查询关系数量
     *
     * @param relationCertNo 关系人证件号码
     * @return 关系数量
     */
    int countByCertNo(@Param("relationCertNo") String relationCertNo);
}

Step 2: 编译验证

mvn compile -pl ruoyi-ccdi

预期结果: BUILD SUCCESS

Step 3: Commit

git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiCustFmyRelationMapper.java
git commit -m "feat: 添加信贷客户家庭关系Mapper接口"

Task 6: 创建Mapper XML映射

Files:

  • Create: ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiCustFmyRelationMapper.xml

Step 1: 创建XML映射文件

复制 CcdiStaffFmyRelationMapper.xml,进行以下关键修改:

  1. 修改namespace和resultMap
  2. 移除LEFT JOIN员工表
  3. 修改WHERE条件为 is_cust_family = 1
  4. 移除personName相关字段
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.ccdi.mapper.CcdiCustFmyRelationMapper">

    <resultMap id="CcdiCustFmyRelationVOResult" type="com.ruoyi.ccdi.domain.vo.CcdiCustFmyRelationVO">
        <id property="id" column="id"/>
        <result property="personId" column="person_id"/>
        <result property="relationType" column="relation_type"/>
        <result property="relationName" column="relation_name"/>
        <result property="gender" column="gender"/>
        <result property="birthDate" column="birth_date"/>
        <result property="relationCertType" column="relation_cert_type"/>
        <result property="relationCertNo" column="relation_cert_no"/>
        <result property="mobilePhone1" column="mobile_phone1"/>
        <result property="mobilePhone2" column="mobile_phone2"/>
        <result property="wechatNo1" column="wechat_no1"/>
        <result property="wechatNo2" column="wechat_no2"/>
        <result property="wechatNo3" column="wechat_no3"/>
        <result property="contactAddress" column="contact_address"/>
        <result property="relationDesc" column="relation_desc"/>
        <result property="status" column="status"/>
        <result property="effectiveDate" column="effective_date"/>
        <result property="invalidDate" column="invalid_date"/>
        <result property="remark" column="remark"/>
        <result property="dataSource" column="data_source"/>
        <result property="isEmpFamily" column="is_emp_family"/>
        <result property="isCustFamily" column="is_cust_family"/>
        <result property="createTime" column="create_time"/>
        <result property="updateTime" column="update_time"/>
        <result property="createdBy" column="created_by"/>
        <result property="updatedBy" column="updated_by"/>
    </resultMap>

    <!-- 分页查询信贷客户家庭关系 -->
    <select id="selectRelationPage" resultMap="CcdiCustFmyRelationVOResult">
        SELECT
            r.id, r.person_id, r.relation_type, r.relation_name,
            r.gender, r.birth_date, r.relation_cert_type, r.relation_cert_no,
            r.mobile_phone1, r.mobile_phone2, r.wechat_no1, r.wechat_no2, r.wechat_no3,
            r.contact_address, r.relation_desc, r.effective_date, r.invalid_date,
            r.status, r.remark, r.data_source, r.is_emp_family, r.is_cust_family,
            r.created_by, r.create_time, r.updated_by, r.update_time
        FROM ccdi_cust_fmy_relation r
        <where>
            r.is_cust_family = 1
            <if test="query.personId != null and query.personId != ''">
                AND r.person_id = #{query.personId}
            </if>
            <if test="query.relationType != null and query.relationType != ''">
                AND r.relation_type = #{query.relationType}
            </if>
            <if test="query.relationName != null and query.relationName != ''">
                AND r.relation_name LIKE CONCAT('%', #{query.relationName}, '%')
            </if>
        </where>
        ORDER BY r.create_time DESC
    </select>

    <!-- 根据ID查询详情 -->
    <select id="selectRelationById" resultMap="CcdiCustFmyRelationVOResult">
        SELECT
            r.id, r.person_id, r.relation_type, r.relation_name,
            r.gender, r.birth_date, r.relation_cert_type, r.relation_cert_no,
            r.mobile_phone1, r.mobile_phone2, r.wechat_no1, r.wechat_no2, r.wechat_no3,
            r.contact_address, r.relation_desc, r.effective_date, r.invalid_date,
            r.status, r.remark, r.data_source, r.is_emp_family, r.is_cust_family,
            r.created_by, r.create_time, r.updated_by, r.update_time
        FROM ccdi_cust_fmy_relation r
        WHERE r.id = #{id} AND r.is_cust_family = 1
    </select>

    <!-- 查询已存在的关系(用于导入校验) -->
    <select id="selectExistingRelations" resultType="com.ruoyi.ccdi.domain.CcdiCustFmyRelation">
        SELECT *
        FROM ccdi_cust_fmy_relation
        WHERE is_cust_family = 1
          AND person_id = #{personId}
          AND relation_type = #{relationType}
          AND relation_cert_no = #{relationCertNo}
          AND status = 1
        LIMIT 1
    </select>

    <!-- 批量插入 -->
    <insert id="insertBatch" parameterType="java.util.List">
        INSERT INTO ccdi_cust_fmy_relation (
            person_id, relation_type, relation_name, gender, birth_date,
            relation_cert_type, relation_cert_no, mobile_phone1, mobile_phone2,
            wechat_no1, wechat_no2, wechat_no3, contact_address, relation_desc,
            status, effective_date, invalid_date, remark, data_source,
            is_emp_family, is_cust_family, created_by, create_time
        ) VALUES
        <foreach collection="relations" item="item" separator=",">
            (
                #{item.personId}, #{item.relationType}, #{item.relationName},
                #{item.gender}, #{item.birthDate}, #{item.relationCertType},
                #{item.relationCertNo}, #{item.mobilePhone1}, #{item.mobilePhone2},
                #{item.wechatNo1}, #{item.wechatNo2}, #{item.wechatNo3},
                #{item.contactAddress}, #{item.relationDesc}, #{item.status},
                #{item.effectiveDate}, #{item.invalidDate}, #{item.remark},
                #{item.dataSource}, #{item.isEmpFamily}, #{item.isCustFamily},
                #{item.createdBy}, #{item.createTime}
            )
        </foreach>
    </insert>

    <!-- 根据证件号码查询关系数量 -->
    <select id="countByCertNo" resultType="int">
        SELECT COUNT(1)
        FROM ccdi_cust_fmy_relation
        WHERE is_cust_family = 1
          AND relation_cert_no = #{relationCertNo}
          AND status = 1
    </select>

</mapper>

Step 2: 编译验证

mvn compile -pl ruoyi-ccdi

预期结果: BUILD SUCCESS

Step 3: Commit

git add ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiCustFmyRelationMapper.xml
git commit -m "feat: 添加信贷客户家庭关系Mapper XML映射"

Task 7: 创建Service接口

Files:

  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiCustFmyRelationService.java
  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiCustFmyRelationImportService.java

Step 1: 创建主Service接口

复制 ICcdiStaffFmyRelationService.java,修改:

package com.ruoyi.ccdi.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.dto.CcdiCustFmyRelationAddDTO;
import com.ruoyi.ccdi.domain.dto.CcdiCustFmyRelationEditDTO;
import com.ruoyi.ccdi.domain.dto.CcdiCustFmyRelationQueryDTO;
import com.ruoyi.ccdi.domain.excel.CcdiCustFmyRelationExcel;
import com.ruoyi.ccdi.domain.vo.CcdiCustFmyRelationVO;

import javax.servlet.http.HttpServletResponse;
import java.util.List;

/**
 * 信贷客户家庭关系Service接口
 *
 * @author ruoyi
 * @date 2026-02-11
 */
public interface ICcdiCustFmyRelationService {

    /**
     * 分页查询信贷客户家庭关系
     *
     * @param query 查询条件
     * @param pageNum 页码
     * @param pageSize 每页条数
     * @return 分页结果
     */
    Page<CcdiCustFmyRelationVO> selectRelationPage(CcdiCustFmyRelationQueryDTO query,
                                                     Integer pageNum, Integer pageSize);

    /**
     * 根据ID查询信贷客户家庭关系详情
     *
     * @param id 主键ID
     * @return 信贷客户家庭关系VO
     */
    CcdiCustFmyRelationVO selectRelationById(Long id);

    /**
     * 新增信贷客户家庭关系
     *
     * @param addDTO 新增DTO
     * @return 是否成功
     */
    boolean insertRelation(CcdiCustFmyRelationAddDTO addDTO);

    /**
     * 修改信贷客户家庭关系
     *
     * @param editDTO 编辑DTO
     * @return 是否成功
     */
    boolean updateRelation(CcdiCustFmyRelationEditDTO editDTO);

    /**
     * 删除信贷客户家庭关系
     *
     * @param ids 主键ID数组
     * @return 是否成功
     */
    boolean deleteRelationByIds(Long[] ids);

    /**
     * 导出信贷客户家庭关系
     *
     * @param query 查询条件
     * @param response HTTP响应
     */
    void exportRelations(CcdiCustFmyRelationQueryDTO query, HttpServletResponse response);

    /**
     * 生成导入模板
     *
     * @param response HTTP响应
     */
    void importTemplate(HttpServletResponse response);

    /**
     * 批量导入信贷客户家庭关系
     *
     * @param excels Excel数据列表
     * @return 导入任务ID
     */
    String importRelations(List<CcdiCustFmyRelationExcel> excels);
}

Step 2: 创建导入Service接口

复制 ICcdiStaffFmyRelationImportService.java,修改:

package com.ruoyi.ccdi.service;

import com.ruoyi.ccdi.domain.excel.CcdiCustFmyRelationExcel;
import com.ruoyi.ccdi.domain.vo.CustFmyRelationImportFailureVO;

import java.util.List;

/**
 * 信贷客户家庭关系导入Service接口
 *
 * @author ruoyi
 * @date 2026-02-11
 */
public interface ICcdiCustFmyRelationImportService {

    /**
     * 异步导入信贷客户家庭关系
     *
     * @param excels Excel数据列表
     * @param taskId 任务ID
     */
    void importRelationsAsync(List<CcdiCustFmyRelationExcel> excels, String taskId);

    /**
     * 校验单条数据
     *
     * @param excel Excel数据
     * @param rowNum 行号
     * @return 错误消息,为null表示校验通过
     */
    String validateExcelRow(CcdiCustFmyRelationExcel excel, Integer rowNum);

    /**
     * 获取导入失败记录
     *
     * @param taskId 任务ID
     * @return 失败记录列表
     */
    List<CustFmyRelationImportFailureVO> getImportFailures(String taskId);
}

Step 3: 编译验证

mvn compile -pl ruoyi-ccdi

预期结果: BUILD SUCCESS

Step 4: Commit

git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/
git commit -m "feat: 添加信贷客户家庭关系Service接口"

Task 8: 创建Service实现类

Files:

  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiCustFmyRelationServiceImpl.java
  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiCustFmyRelationImportServiceImpl.java

Step 1: 创建主Service实现类

复制 CcdiStaffFmyRelationServiceImpl.java,进行以下关键修改:

  1. 类名和注入的Mapper/Service
  2. 设置 isEmpFamily=false, isCustFamily=true
  3. Redis Key为 import:custFmyRelation:
package com.ruoyi.ccdi.service.impl;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.CcdiCustFmyRelation;
import com.ruoyi.ccdi.domain.dto.CcdiCustFmyRelationAddDTO;
import com.ruoyi.ccdi.domain.dto.CcdiCustFmyRelationEditDTO;
import com.ruoyi.ccdi.domain.dto.CcdiCustFmyRelationQueryDTO;
import com.ruoyi.ccdi.domain.excel.CcdiCustFmyRelationExcel;
import com.ruoyi.ccdi.domain.vo.CcdiCustFmyRelationVO;
import com.ruoyi.ccdi.mapper.CcdiCustFmyRelationMapper;
import com.ruoyi.ccdi.service.ICcdiCustFmyRelationImportService;
import com.ruoyi.ccdi.service.ICcdiCustFmyRelationService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 信贷客户家庭关系Service实现
 *
 * @author ruoyi
 * @date 2026-02-11
 */
@Service
public class CcdiCustFmyRelationServiceImpl implements ICcdiCustFmyRelationService {

    @Resource
    private CcdiCustFmyRelationMapper mapper;

    @Resource
    private ICcdiCustFmyRelationImportService importService;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    private static final String IMPORT_TASK_KEY_PREFIX = "import:custFmyRelation:";

    @Override
    public Page<CcdiCustFmyRelationVO> selectRelationPage(CcdiCustFmyRelationQueryDTO query,
                                                           Integer pageNum, Integer pageSize) {
        Page<CcdiCustFmyRelationVO> page = new Page<>(pageNum, pageSize);
        return mapper.selectRelationPage(page, query);
    }

    @Override
    public CcdiCustFmyRelationVO selectRelationById(Long id) {
        return mapper.selectRelationById(id);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean insertRelation(CcdiCustFmyRelationAddDTO addDTO) {
        CcdiCustFmyRelation relation = new CcdiCustFmyRelation();
        BeanUtils.copyProperties(addDTO, relation);

        // 关键设置:客户家庭关系
        relation.setIsEmpFamily(false);
        relation.setIsCustFamily(true);
        relation.setStatus(1);
        relation.setDataSource("MANUAL");

        return mapper.insert(relation) > 0;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateRelation(CcdiCustFmyRelationEditDTO editDTO) {
        CcdiCustFmyRelation relation = new CcdiCustFmyRelation();
        BeanUtils.copyProperties(editDTO, relation);

        return mapper.updateById(relation) > 0;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteRelationByIds(Long[] ids) {
        return mapper.deleteBatchIds(List.of(ids)) > 0;
    }

    @Override
    public void exportRelations(CcdiCustFmyRelationQueryDTO query, HttpServletResponse response) {
        // 查询所有符合条件的数据(不分页)
        Page<CcdiCustFmyRelationVO> page = new Page<>(1, 10000);
        Page<CcdiCustFmyRelationVO> result = mapper.selectRelationPage(page, query);

        List<CcdiCustFmyRelationExcel> excels = result.getRecords().stream()
                .map(this::convertToExcel)
                .toList();

        // 使用EasyExcel导出
        try {
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            String fileName = URLEncoder.encode("信贷客户家庭关系", StandardCharsets.UTF_8)
                    .replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

            // 这里使用EasyExcel工具类导出
            // EasyExcel.write(response.getOutputStream(), CcdiCustFmyRelationExcel.class)
            //     .sheet("信贷客户家庭关系")
            //     .doWrite(excels);
        } catch (Exception e) {
            throw new RuntimeException("导出失败", e);
        }
    }

    @Override
    public void importTemplate(HttpServletResponse response) {
        try {
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            String fileName = URLEncoder.encode("信贷客户家庭关系导入模板", StandardCharsets.UTF_8)
                    .replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

            // EasyExcel.write(response.getOutputStream(), CcdiCustFmyRelationExcel.class)
            //     .sheet("模板")
            //     .doWrite(Collections.emptyList());
        } catch (Exception e) {
            throw new RuntimeException("模板下载失败", e);
        }
    }

    @Override
    public String importRelations(List<CcdiCustFmyRelationExcel> excels) {
        String taskId = UUID.randomUUID().toString();

        // 保存任务状态到Redis
        redisTemplate.opsForValue().set(IMPORT_TASK_KEY_PREFIX + taskId, "PROCESSING", 1, TimeUnit.HOURS);

        // 异步导入
        importService.importRelationsAsync(excels, taskId);

        return taskId;
    }

    private CcdiCustFmyRelationExcel convertToExcel(CcdiCustFmyRelationVO vo) {
        CcdiCustFmyRelationExcel excel = new CcdiCustFmyRelationExcel();
        BeanUtils.copyProperties(vo, excel);
        return excel;
    }
}

Step 2: 创建导入Service实现类

复制 CcdiStaffFmyRelationImportServiceImpl.java,修改:

package com.ruoyi.ccdi.service.impl;

import com.alibaba.excel.EasyExcel;
import com.ruoyi.ccdi.domain.CcdiCustFmyRelation;
import com.ruoyi.ccdi.domain.excel.CcdiCustFmyRelationExcel;
import com.ruoyi.ccdi.domain.vo.CustFmyRelationImportFailureVO;
import com.ruoyi.ccdi.mapper.CcdiCustFmyRelationMapper;
import com.ruoyi.ccdi.service.ICcdiCustFmyRelationImportService;
import com.ruoyi.common.utils.SecurityUtils;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 信贷客户家庭关系导入Service实现
 *
 * @author ruoyi
 * @date 2026-02-11
 */
@Service
public class CcdiCustFmyRelationImportServiceImpl implements ICcdiCustFmyRelationImportService {

    private static final Logger log = LoggerFactory.getLogger(CcdiCustFmyRelationImportServiceImpl.class);

    @Resource
    private CcdiCustFmyRelationMapper mapper;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    private static final String IMPORT_TASK_KEY_PREFIX = "import:custFmyRelation:";
    private static final String IMPORT_FAILURE_KEY_PREFIX = "import:custFmyRelation:failures:";

    @Async
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void importRelationsAsync(List<CcdiCustFmyRelationExcel> excels, String taskId) {
        List<CcdiCustFmyRelation> validRelations = new ArrayList<>();
        List<CustFmyRelationImportFailureVO> failures = new ArrayList<>();

        try {
            for (int i = 0; i < excels.size(); i++) {
                CcdiCustFmyRelationExcel excel = excels.get(i);
                Integer rowNum = i + 2; // Excel行号从2开始(第1行是表头)

                String errorMessage = validateExcelRow(excel, rowNum);
                if (errorMessage != null) {
                    CustFmyRelationImportFailureVO failure = new CustFmyRelationImportFailureVO();
                    failure.setRowNum(rowNum);
                    failure.setPersonId(excel.getPersonId());
                    failure.setRelationType(excel.getRelationType());
                    failure.setRelationName(excel.getRelationName());
                    failure.setErrorMessage(errorMessage);
                    failures.add(failure);
                    continue;
                }

                CcdiCustFmyRelation relation = convertToRelation(excel);
                validRelations.add(relation);
            }

            // 批量插入有效数据
            if (!validRelations.isEmpty()) {
                mapper.insertBatch(validRelations);
            }

            // 保存失败记录到Redis(24小时过期)
            if (!failures.isEmpty()) {
                redisTemplate.opsForValue().set(
                    IMPORT_FAILURE_KEY_PREFIX + taskId,
                    failures,
                    24,
                    TimeUnit.HOURS
                );
            }

            // 更新任务状态
            redisTemplate.opsForValue().set(
                IMPORT_TASK_KEY_PREFIX + taskId,
                "COMPLETED:" + validRelations.size() + ":" + failures.size(),
                1,
                TimeUnit.HOURS
            );

        } catch (Exception e) {
            log.error("导入失败", e);
            redisTemplate.opsForValue().set(
                IMPORT_TASK_KEY_PREFIX + taskId,
                "FAILED:" + e.getMessage(),
                1,
                TimeUnit.HOURS
            );
        }
    }

    @Override
    public String validateExcelRow(CcdiCustFmyRelationExcel excel, Integer rowNum) {
        if (excel.getPersonId() == null || excel.getPersonId().trim().isEmpty()) {
            return "信贷客户身份证号不能为空";
        }

        if (excel.getRelationType() == null || excel.getRelationType().trim().isEmpty()) {
            return "关系类型不能为空";
        }

        if (excel.getRelationName() == null || excel.getRelationName().trim().isEmpty()) {
            return "关系人姓名不能为空";
        }

        if (excel.getRelationCertType() == null || excel.getRelationCertType().trim().isEmpty()) {
            return "关系人证件类型不能为空";
        }

        if (excel.getRelationCertNo() == null || excel.getRelationCertNo().trim().isEmpty()) {
            return "关系人证件号码不能为空";
        }

        // 检查是否已存在相同的关系
        CcdiCustFmyRelation existing = mapper.selectExistingRelations(
            excel.getPersonId(),
            excel.getRelationType(),
            excel.getRelationCertNo()
        );

        if (existing != null) {
            return "该关系已存在,请勿重复导入";
        }

        return null; // 校验通过
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<CustFmyRelationImportFailureVO> getImportFailures(String taskId) {
        Object obj = redisTemplate.opsForValue().get(IMPORT_FAILURE_KEY_PREFIX + taskId);
        if (obj != null) {
            return (List<CustFmyRelationImportFailureVO>) obj;
        }
        return new ArrayList<>();
    }

    private CcdiCustFmyRelation convertToRelation(CcdiCustFmyRelationExcel excel) {
        CcdiCustFmyRelation relation = new CcdiCustFmyRelation();
        org.springframework.beans.BeanUtils.copyProperties(excel, relation);

        relation.setIsEmpFamily(false);
        relation.setIsCustFamily(true);
        relation.setStatus(excel.getStatus() != null ? excel.getStatus() : 1);
        relation.setDataSource("IMPORT");
        relation.setCreatedBy(SecurityUtils.getUsername());
        relation.setCreateTime(new Date());

        return relation;
    }
}

Step 3: 编译验证

mvn compile -pl ruoyi-ccdi

预期结果: BUILD SUCCESS

Step 4: Commit

git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/
git commit -m "feat: 添加信贷客户家庭关系Service实现类"

Task 9: 创建Controller

Files:

  • Create: ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiCustFmyRelationController.java

Step 1: 创建Controller

复制 CcdiStaffFmyRelationController.java,修改:

package com.ruoyi.ccdi.controller;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.domain.dto.CcdiCustFmyRelationAddDTO;
import com.ruoyi.ccdi.domain.dto.CcdiCustFmyRelationEditDTO;
import com.ruoyi.ccdi.domain.dto.CcdiCustFmyRelationQueryDTO;
import com.ruoyi.ccdi.domain.excel.CcdiCustFmyRelationExcel;
import com.ruoyi.ccdi.domain.vo.CcdiCustFmyRelationVO;
import com.ruoyi.ccdi.domain.vo.CustFmyRelationImportFailureVO;
import com.ruoyi.ccdi.service.ICcdiCustFmyRelationService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

/**
 * 信贷客户家庭关系Controller
 *
 * @author ruoyi
 * @date 2026-02-11
 */
@Tag(name = "信贷客户家庭关系管理")
@RestController
@RequestMapping("/ccdi/custFmyRelation")
public class CcdiCustFmyRelationController extends BaseController {

    @Resource
    private ICcdiCustFmyRelationService relationService;

    /**
     * 查询信贷客户家庭关系列表
     */
    @Operation(summary = "查询信贷客户家庭关系列表")
    @PreAuthorize("@ss.hasPermi('ccdi:custFmyRelation:query')")
    @GetMapping("/list")
    public TableDataInfo list(CcdiCustFmyRelationQueryDTO query) {
        startPage();
        Page<CcdiCustFmyRelationVO> page = relationService.selectRelationPage(query, getPageNum(), getPageSize());
        return getDataTable(page.getRecords(), page.getTotal());
    }

    /**
     * 根据ID查询信贷客户家庭关系详情
     */
    @Operation(summary = "查询信贷客户家庭关系详情")
    @PreAuthorize("@ss.hasPermi('ccdi:custFmyRelation:query')")
    @GetMapping("/{id}")
    public AjaxResult getInfo(@PathVariable("id") Long id) {
        CcdiCustFmyRelationVO relation = relationService.selectRelationById(id);
        return success(relation);
    }

    /**
     * 新增信贷客户家庭关系
     */
    @Operation(summary = "新增信贷客户家庭关系")
    @PreAuthorize("@ss.hasPermi('ccdi:custFmyRelation:add')")
    @PostMapping
    public AjaxResult add(@Validated @RequestBody CcdiCustFmyRelationAddDTO addDTO) {
        return toAjax(relationService.insertRelation(addDTO));
    }

    /**
     * 修改信贷客户家庭关系
     */
    @Operation(summary = "修改信贷客户家庭关系")
    @PreAuthorize("@ss.hasPermi('ccdi:custFmyRelation:edit')")
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody CcdiCustFmyRelationEditDTO editDTO) {
        return toAjax(relationService.updateRelation(editDTO));
    }

    /**
     * 删除信贷客户家庭关系
     */
    @Operation(summary = "删除信贷客户家庭关系")
    @PreAuthorize("@ss.hasPermi('ccdi:custFmyRelation:remove')")
    @DeleteMapping("/{ids}")
    public AjaxResult remove(@PathVariable Long[] ids) {
        return toAjax(relationService.deleteRelationByIds(ids));
    }

    /**
     * 导出信贷客户家庭关系
     */
    @Operation(summary = "导出信贷客户家庭关系")
    @PreAuthorize("@ss.hasPermi('ccdi:custFmyRelation:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, CcdiCustFmyRelationQueryDTO query) {
        relationService.exportRelations(query, response);
    }

    /**
     * 下载导入模板
     */
    @Operation(summary = "下载导入模板")
    @PreAuthorize("@ss.hasPermi('ccdi:custFmyRelation:import')")
    @PostMapping("/importTemplate")
    public void importTemplate(HttpServletResponse response) {
        relationService.importTemplate(response);
    }

    /**
     * 导入信贷客户家庭关系
     */
    @Operation(summary = "导入信贷客户家庭关系")
    @PreAuthorize("@ss.hasPermi('ccdi:custFmyRelation:import')")
    @PostMapping("/importData")
    public AjaxResult importData(@RequestParam("file") MultipartFile file) throws IOException {
        List<CcdiCustFmyRelationExcel> excels = EasyExcel.read(file.getInputStream())
                .head(CcdiCustFmyRelationExcel.class)
                .sheet()
                .doReadSync();

        String taskId = relationService.importRelations(excels);
        return success("导入任务已提交,任务ID: " + taskId);
    }

    /**
     * 查询导入状态
     */
    @Operation(summary = "查询导入状态")
    @PreAuthorize("@ss.hasPermi('ccdi:custFmyRelation:query')")
    @GetMapping("/importStatus/{taskId}")
    public AjaxResult getImportStatus(@PathVariable String taskId) {
        // 从Redis获取任务状态
        Object status = redisTemplate.opsForValue().get("import:custFmyRelation:" + taskId);
        return success(status);
    }

    /**
     * 查询导入失败记录
     */
    @Operation(summary = "查询导入失败记录")
    @PreAuthorize("@ss.hasPermi('ccdi:custFmyRelation:query')")
    @GetMapping("/importFailures/{taskId}")
    public TableDataInfo getImportFailures(@PathVariable String taskId) {
        startPage();
        List<CustFmyRelationImportFailureVO> failures = relationService.getImportFailures(taskId);
        return getDataTable(failures, (long) failures.size());
    }
}

Step 2: 编译验证

mvn compile -pl ruoyi-ccdi

预期结果: BUILD SUCCESS

Step 3: Commit

git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiCustFmyRelationController.java
git commit -m "feat: 添加信贷客户家庭关系Controller"

Task 12: 创建菜单权限SQL

Files:

  • Create: sql/ccdi_cust_fmy_relation_menu.sql

Step 1: 创建菜单SQL

创建 sql/ccdi_cust_fmy_relation_menu.sql:

-- 信贷客户家庭关系菜单
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES
('信贷客户家庭关系', (SELECT menu_id FROM sys_menu WHERE menu_name = '信息维护' LIMIT 1), 5, 'custFmyRelation', 'ccdiCustFmyRelation/index', 1, 0, 'C', '0', '0', 'ccdi:custFmyRelation:list', 'peoples', 'admin', NOW(), '', NULL, '信贷客户家庭关系菜单');

-- 获取刚插入的菜单ID
SET @parent_id = LAST_INSERT_ID();

-- 添加按钮权限
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) VALUES
('信贷客户家庭关系查询', @parent_id, 1, '#', '', 1, 0, 'F', '0', '0', 'ccdi:custFmyRelation:query', '#', 'admin', NOW(), '', NULL, ''),
('信贷客户家庭关系新增', @parent_id, 2, '#', '', 1, 0, 'F', '0', '0', 'ccdi:custFmyRelation:add', '#', 'admin', NOW(), '', NULL, ''),
('信贷客户家庭关系修改', @parent_id, 3, '#', '', 1, 0, 'F', '0', '0', 'ccdi:custFmyRelation:edit', '#', 'admin', NOW(), '', NULL, ''),
('信贷客户家庭关系删除', @parent_id, 4, '#', '', 1, 0, 'F', '0', '0', 'ccdi:custFmyRelation:remove', '#', 'admin', NOW(), '', NULL, ''),
('信贷客户家庭关系导出', @parent_id, 5, '#', '', 1, 0, 'F', '0', '0', 'ccdi:custFmyRelation:export', '#', 'admin', NOW(), '', NULL, ''),
('信贷客户家庭关系导入', @parent_id, 6, '#', '', 1, 0, 'F', '0', '0', 'ccdi:custFmyRelation:import', '#', 'admin', NOW(), '', NULL, '');

Step 2: Commit

git add sql/ccdi_cust_fmy_relation_menu.sql
git commit -m "feat: 添加信贷客户家庭关系菜单权限"

测试验证

Task 14: 后端接口测试

前置条件:

  • 后端服务已启动 (mvn spring-boot:run -pl ruoyi-admin)
  • 数据库连接正常
  • Redis服务正常运行

Step 1: 测试登录获取token

curl -X POST "http://localhost:8080/login" \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}'

预期结果: 返回token

Step 2: 测试查询列表接口

curl -X GET "http://localhost:8080/ccdi/custFmyRelation/list?pageNum=1&pageSize=10" \
  -H "Authorization: Bearer <token>"

预期结果: 返回空列表(无数据)

Step 3: 测试新增接口

curl -X POST "http://localhost:8080/ccdi/custFmyRelation" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "personId": "110101199001011234",
    "relationType": "配偶",
    "relationName": "张三",
    "gender": "M",
    "relationCertType": "身份证",
    "relationCertNo": "110101199001015678"
  }'

预期结果: 返回成功

Step 4: 测试查询详情接口

curl -X GET "http://localhost:8080/ccdi/custFmyRelation/1" \
  -H "Authorization: Bearer <token>"

预期结果: 返回刚插入的记录

Step 5: 测试修改接口

curl -X PUT "http://localhost:8080/ccdi/custFmyRelation" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "id": 1,
    "personId": "110101199001011234",
    "relationType": "配偶",
    "relationName": "张三丰",
    "gender": "M",
    "relationCertType": "身份证",
    "relationCertNo": "110101199001015678",
    "status": 1
  }'

预期结果: 返回成功

Step 6: 测试删除接口

curl -X DELETE "http://localhost:8080/ccdi/custFmyRelation/1" \
  -H "Authorization: Bearer <token>"

预期结果: 返回成功

Step 7: 验证Swagger文档

访问 http://localhost:8080/swagger-ui/index.html

预期结果: 看到"信贷客户家庭关系管理"分组,所有接口正常显示


完成检查清单

  • 数据库表创建成功
  • 实体类编译通过
  • DTO类编译通过
  • VO类编译通过
  • Excel类编译通过
  • Mapper接口编译通过
  • Mapper XML映射配置正确
  • Service接口编译通过
  • Service实现类编译通过
  • Controller编译通过
  • Controller所有接口在Swagger正常显示
  • 菜单权限SQL已执行
  • 登录接口测试通过
  • 查询列表接口测试通过
  • 新增接口测试通过
  • 修改接口测试通过
  • 删除接口测试通过
  • 导出接口测试通过
  • 导入模板下载测试通过

预期结果

完成后,后端将提供以下功能:

  1. 完整的CRUD接口

    • 分页查询信贷客户家庭关系列表
    • 根据ID查询详情
    • 新增信贷客户家庭关系
    • 修改信贷客户家庭关系
    • 删除信贷客户家庭关系
  2. 导入导出功能

    • 导出Excel数据
    • 下载导入模板
    • 异步批量导入
    • 导入状态查询
    • 导入失败记录查看
  3. 权限控制

    • 完整的CRUD权限标识
    • 按钮级权限控制
  4. 数据隔离

    • 独立表 ccdi_cust_fmy_relation
    • is_cust_family = 1 过滤条件
    • 与员工亲属关系完全分离