Files
ccdi/docs/plans/2026-02-11-cust-fmy-relation-import-alignment.md
wkc 45e4096366 feat: 执行信贷客户家庭关系菜单权限SQL
- 插入主菜单(信息维护下第5位)
- 插入6个按钮权限(查询/新增/修改/删除/导出/导入)
- 菜单ID: 2068
- 权限前缀: ccdi:custFmyRelation
2026-02-11 16:59:42 +08:00

8.6 KiB

信贷客户家庭关系导入功能对齐方案

概述

本文档描述了如何将信贷客户家庭关系功能的导入实现完全对齐到员工亲属关系的成熟模式。

参考模板: CcdiStaffEnterpriseRelationImportServiceImpl 修改对象: CcdiCustFmyRelationImportServiceImpl

设计目标

  1. 提升代码质量和可维护性
  2. 优化性能,避免 N+1 查询问题
  3. 改善用户体验,提供详细的导入进度和状态反馈
  4. 统一日志记录和错误处理机制

架构调整

1. 引入导入工具类

复用 ImportLogUtils 进行统一的日志记录:

  • 导入开始/结束日志
  • 批量查询日志
  • 进度跟踪日志
  • 验证错误日志
  • 批量操作日志

2. Redis 状态管理升级

现状: 简单 String 值存储状态

"COMPLETED:10:5"

优化: Hash 结构存储详细状态

{
  "taskId": "uuid",
  "status": "SUCCESS" | "PARTIAL_SUCCESS" | "PROCESSING",
  "totalCount": 100,
  "successCount": 95,
  "failureCount": 5,
  "progress": 100,
  "startTime": 1234567890,
  "endTime": 1234567900,
  "message": "成功95条,失败5条"
}
  • 过期时间: 7 天
  • 失败记录: 单独 Key, JSON 序列化, 7 天过期

3. 批量查询优化

实现 batchExistsByCombinations 方法:

  • 提取所有 person_id + relation_type + relation_cert_no 组合
  • 一次性批量查询已存在的组合
  • 避免循环查询导致的 N+1 问题

4. 导入结果封装

创建/复用统一的 VO:

  • ImportStatusVO: 导入状态详情
  • ImportResultVO: 导入提交结果
  • CustFmyRelationImportFailureVO: 失败记录详情

数据验证逻辑

唯一性检查

优化前: 每条记录单独查询

for (excel : excels) {
    CcdiCustFmyRelation existing = mapper.selectExistingRelations(...);
    // N 次数据库查询
}

优化后: 批量查询

Set<String> existingCombinations = getExistingCombinations(excels);
// 1 次数据库查询

for (excel : excels) {
    String combination = excel.getPersonId() + "|" + ...;
    if (existingCombinations.contains(combination)) {
        throw new RuntimeException("该关系已存在");
    }
}

Excel 内部重复检查

Set<String> processedCombinations = new HashSet<>();

for (excel : excels) {
    String combination = ...;

    if (processedCombinations.contains(combination)) {
        throw new RuntimeException("该关系在导入文件中重复");
    }

    processedCombinations.add(combination);
}

验证规则

必填字段:

  • 信贷客户身份证号
  • 关系类型
  • 关系人姓名
  • 关系人证件类型
  • 关系人证件号码

格式验证:

  • 身份证号: 18位有效格式
  • 证件号码: 根据证件类型验证

长度限制:

  • 关系人姓名: ≤ 50
  • 关系类型: ≤ 20
  • 证件号码: ≤ 50

批量操作优化

分批插入策略

private void saveBatch(List<CcdiCustFmyRelation> list, int batchSize) {
    for (int i = 0; i < list.size(); i += batchSize) {
        int end = Math.min(i + batchSize, list.size());
        List<CcdiCustFmyRelation> subList = list.subList(i, end);
        mapper.insertBatch(subList);
    }
}

// 调用: 每 500 条为一批
saveBatch(newRecords, 500);

批量操作日志

开始批量插入: 总批次数=5, 每批大小=500
批量插入完成: 成功=2500, 耗时=1234ms

失败记录处理

失败记录数据结构

public class CustFmyRelationImportFailureVO {
    private Integer rowNum;           // Excel 行号
    private String personId;          // 信贷客户身份证号
    private String relationType;      // 关系类型
    private String relationName;      // 关系人姓名
    private String errorMessage;      // 错误消息
}

Redis 存储优化

Key: import:custFmyRelation:{taskId}:failures 序列化: JSON 过期时间: 7 天 反序列化:

return JSON.parseArray(
    JSON.toJSONString(failuresObj),
    CustFmyRelationImportFailureVO.class
);

Controller 层调整

导入接口

@PostMapping("/importData")
public AjaxResult importData(@RequestParam("file") MultipartFile file) {
    List<CcdiCustFmyRelationExcel> excels =
        EasyExcelUtil.importExcel(file.getInputStream(), ...);

    if (excels == null || excels.isEmpty()) {
        return error("至少需要一条数据");
    }

    String taskId = relationService.importRelations(excels);

    ImportResultVO result = new ImportResultVO();
    result.setTaskId(taskId);
    result.setStatus("PROCESSING");
    result.setMessage("导入任务已提交,正在后台处理");

    return AjaxResult.success("导入任务已提交,正在后台处理", result);
}

导入状态查询

@GetMapping("/importStatus/{taskId}")
public AjaxResult getImportStatus(@PathVariable String taskId) {
    ImportStatusVO statusVO = relationImportService.getImportStatus(taskId);
    return success(statusVO);
}

失败记录查询

@GetMapping("/importFailures/{taskId}")
public TableDataInfo getImportFailures(
    @PathVariable String taskId,
    @RequestParam(defaultValue = "1") Integer pageNum,
    @RequestParam(defaultValue = "10") Integer pageSize
) {
    List<CustFmyRelationImportFailureVO> failures =
        relationImportService.getImportFailures(taskId);

    // 手动分页
    int fromIndex = (pageNum - 1) * pageSize;
    int toIndex = Math.min(fromIndex + pageSize, failures.size());

    if (fromIndex >= failures.size()) {
        return getDataTable(new ArrayList<>(), failures.size());
    }

    List<CustFmyRelationImportFailureVO> pageData =
        failures.subList(fromIndex, toIndex);

    return getDataTable(pageData, failures.size());
}

导入模板改进

使用字典下拉框

@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
    EasyExcelUtil.importTemplateWithDictDropdown(
        response,
        CcdiCustFmyRelationExcel.class,
        "信贷客户家庭关系"
    );
}

Excel 实体注解增强

@DictDropdown(type = "ccdi_relation_type")
private String relationType;

@DictDropdown(type = "ccdi_cert_type")
private String relationCertType;

修改文件清单

1. Service 层

  • CcdiCustFmyRelationImportServiceImpl.java - 核心导入逻辑重构
  • CcdiCustFmyRelationServiceImpl.java - 导入入口方法调整

2. Controller 层

  • CcdiCustFmyRelationController.java - 接口返回值优化

3. Mapper 层

  • CcdiCustFmyRelationMapper.java - 添加批量查询方法
  • Mapper XML - 实现批量查询 SQL

4. VO 类

  • 检查/创建 ImportStatusVO.java
  • 检查/创建 ImportResultVO.java
  • 优化 CustFmyRelationImportFailureVO.java

5. Excel 实体

  • CcdiCustFmyRelationExcel.java - 添加字典注解

6. 工具类

  • 复用 ImportLogUtils.java
  • 复用 EasyExcelUtil.java

关键代码片段

Mapper 批量查询

// Mapper 接口
List<String> batchExistsByCombinations(
    @Param("combinations") List<String> combinations
);

// XML 实现
<select id="batchExistsByCombinations" resultType="string">
    SELECT CONCAT(person_id, '|', relation_type, '|', relation_cert_no)
    FROM ccdi_cust_fmy_relation
    WHERE CONCAT(person_id, '|', relation_type, '|', relation_cert_no) IN
    <foreach collection="combinations" item="combo" open="(" separator="," close=")">
        #{combo}
    </foreach>
</select>

异步导入方法

@Async
@Transactional(rollbackFor = Exception.class)
public void importRelationsAsync(
    List<CcdiCustFmyRelationExcel> excels,
    String taskId,
    String userName  // 新增参数,用于审计
) {
    // 实现逻辑...
}

实施步骤

  1. 添加 Mapper 批量查询方法

    • 在 Mapper 接口添加 batchExistsByCombinations
    • 在 XML 实现 SQL
  2. 重构 ImportServiceImpl

    • 引入 ImportLogUtils
    • 实现批量查询逻辑
    • 添加 Excel 内部重复检查
    • 优化 Redis 状态管理
    • 改进失败记录存储
  3. 创建/优化 VO 类

    • 检查并复用已有的 ImportStatusVO
    • 检查并复用已有的 ImportResultVO
    • 优化失败记录 VO
  4. 调整 Controller

    • 修改导入接口返回值
    • 优化状态查询接口
    • 优化失败记录查询接口
  5. 更新 Excel 实体

    • 添加 @DictDropdown 注解
  6. 测试验证

    • 单元测试
    • 集成测试
    • 性能对比测试

预期效果

性能提升

  • 批量查询: 从 N 次减少到 1 次
  • 导入 1000 条数据预计提升 50-70%

用户体验

  • 实时进度反馈
  • 详细的错误信息
  • 清晰的成功/失败统计

代码质量

  • 统一的日志记录
  • 完善的错误处理
  • 更好的可维护性

创建日期

2026-02-11