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

2009 lines
59 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 信贷客户家庭关系维护功能 - 后端实施计划
> **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` 文件:
```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文件:
```bash
# 连接数据库并执行建表脚本
mysql -u <username> -p <database> < sql/ccdi_cust_fmy_relation.sql
```
**验证方式:**
```sql
SHOW CREATE TABLE ccdi_cust_fmy_relation;
```
**预期结果:** 表创建成功,包含所有字段和索引
**Step 3: Commit**
```bash
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. **类名和注释:**
```java
/**
* 信贷客户家庭关系对象 ccdi_cust_fmy_relation
*
* @author ruoyi
* @date 2026-02-11
*/
@Data
@TableName("ccdi_cust_fmy_relation")
public class CcdiCustFmyRelation implements Serializable {
```
2. **关键注释修改:**
- `/** 信贷客户身份证号 */` (原"员工身份证号")
- `/** 是否是客户亲属1-是 */`
- `/** 是否是员工亲属0-否 */`
3. **完整代码结构:**
```java
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: 编译验证**
```bash
mvn compile -pl ruoyi-ccdi
```
**预期结果:** BUILD SUCCESS
**Step 3: Commit**
```bash
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`,修改:
```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`,修改:
```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**
```java
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: 编译验证**
```bash
mvn compile -pl ruoyi-ccdi
```
**预期结果:** BUILD SUCCESS
**Step 5: Commit**
```bash
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. **修改类名和注释**
```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;
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`,修改:
```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: 编译验证**
```bash
mvn compile -pl ruoyi-ccdi
```
**预期结果:** BUILD SUCCESS
**Step 4: Commit**
```bash
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`,修改:
```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: 编译验证**
```bash
mvn compile -pl ruoyi-ccdi
```
**预期结果:** BUILD SUCCESS
**Step 3: Commit**
```bash
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`,修改:
```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: 编译验证**
```bash
mvn compile -pl ruoyi-ccdi
```
**预期结果:** BUILD SUCCESS
**Step 3: Commit**
```bash
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
<?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: 编译验证**
```bash
mvn compile -pl ruoyi-ccdi
```
**预期结果:** BUILD SUCCESS
**Step 3: Commit**
```bash
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`,修改:
```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`,修改:
```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: 编译验证**
```bash
mvn compile -pl ruoyi-ccdi
```
**预期结果:** BUILD SUCCESS
**Step 4: Commit**
```bash
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:`**
```java
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`,修改:
```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: 编译验证**
```bash
mvn compile -pl ruoyi-ccdi
```
**预期结果:** BUILD SUCCESS
**Step 4: Commit**
```bash
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`,修改:
```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: 编译验证**
```bash
mvn compile -pl ruoyi-ccdi
```
**预期结果:** BUILD SUCCESS
**Step 3: Commit**
```bash
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`:
```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**
```bash
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**
```bash
curl -X POST "http://localhost:8080/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
```
**预期结果:** 返回token
**Step 2: 测试查询列表接口**
```bash
curl -X GET "http://localhost:8080/ccdi/custFmyRelation/list?pageNum=1&pageSize=10" \
-H "Authorization: Bearer <token>"
```
**预期结果:** 返回空列表(无数据)
**Step 3: 测试新增接口**
```bash
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: 测试查询详情接口**
```bash
curl -X GET "http://localhost:8080/ccdi/custFmyRelation/1" \
-H "Authorization: Bearer <token>"
```
**预期结果:** 返回刚插入的记录
**Step 5: 测试修改接口**
```bash
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: 测试删除接口**
```bash
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` 过滤条件
- 与员工亲属关系完全分离