Files
ccdi/doc/plans/2026-02-06-intermediary-async-import-design.md

34 KiB

中介库异步导入功能设计文档

创建日期: 2026-02-06 设计目标: 将中介库管理的文件导入功能改造为异步实现,完全复用员工信息/招聘信息异步导入的架构模式 数据量预期: 小批量(通常<500条) 架构模式: 拆分式设计(个人中介和实体中介分别实现)


一、架构概述

1.1 核心架构

采用拆分式设计,为个人中介和实体中介分别创建独立的异步导入服务:

  • 异步处理层: 使用Spring @Async注解,通过现有的importExecutor线程池执行异步任务
  • 状态存储层: 使用Redis Hash存储导入状态,TTL为7天
  • 失败记录层: 使用Redis String存储失败记录,存储JSON数组
  • API层: 为个人和实体分别提供三个接口(导入、状态查询、失败记录查询)

1.2 数据流程

前端上传Excel(个人/实体)
    ↓
Controller解析并立即返回taskId
    ↓
异步服务在后台处理:
  1. 数据验证
  2. 分类(新增/更新)
  3. 批量操作
  4. 保存结果到Redis
    ↓
前端每2秒轮询状态
    ↓
状态变为SUCCESS/PARTIAL_SUCCESS/FAILED
    ↓
如有失败,显示对应的"查看失败记录"按钮

1.3 Redis Key设计

个人中介导入:

  • 状态Key: import:intermediary-person:{taskId} (Hash结构)
  • 失败记录Key: import:intermediary-person:{taskId}:failures (String结构)
  • TTL: 7天

实体中介导入:

  • 状态Key: import:intermediary-entity:{taskId} (Hash结构)
  • 失败记录Key: import:intermediary-entity:{taskId}:failures (String结构)
  • TTL: 7天

1.4 状态枚举

状态值 说明 前端行为
PROCESSING 处理中 继续轮询
SUCCESS 全部成功 显示成功通知,刷新列表
PARTIAL_SUCCESS 部分成功 显示警告通知,显示失败按钮
FAILED 全部失败 显示错误通知,显示失败按钮

二、后端组件设计

2.1 VO类设计

2.1.1 IntermediaryPersonImportFailureVO (个人中介失败记录)

@Data
@Schema(description = "个人中介导入失败记录")
public class IntermediaryPersonImportFailureVO {

    @Schema(description = "姓名")
    private String name;

    @Schema(description = "证件号码")
    private String personId;

    @Schema(description = "人员类型")
    private String personType;

    @Schema(description = "性别")
    private String gender;

    @Schema(description = "手机号码")
    private String mobile;

    @Schema(description = "所在公司")
    private String company;

    @Schema(description = "错误信息")
    private String errorMessage;
}

2.1.2 IntermediaryEntityImportFailureVO (实体中介失败记录)

@Data
@Schema(description = "实体中介导入失败记录")
public class IntermediaryEntityImportFailureVO {

    @Schema(description = "机构名称")
    private String enterpriseName;

    @Schema(description = "统一社会信用代码")
    private String socialCreditCode;

    @Schema(description = "主体类型")
    private String enterpriseType;

    @Schema(description = "企业性质")
    private String enterpriseNature;

    @Schema(description = "法定代表人")
    private String legalRepresentative;

    @Schema(description = "成立日期")
    private Date establishDate;

    @Schema(description = "错误信息")
    private String errorMessage;
}

2.1.3 复用VO类

  • ImportResultVO - 导入结果VO(复用员工导入)
  • ImportStatusVO - 导入状态VO(复用员工导入)

2.2 Service层设计

2.2.1 个人中介导入Service接口

public interface ICcdiIntermediaryPersonImportService {

    /**
     * 异步导入个人中介数据
     *
     * @param excelList        Excel数据列表
     * @param isUpdateSupport  是否更新已存在的数据
     * @param taskId           任务ID
     * @param userName         当前用户名(用于审计字段)
     */
    void importPersonAsync(List<CcdiIntermediaryPersonExcel> excelList,
                           Boolean isUpdateSupport,
                           String taskId,
                           String userName);

    /**
     * 查询导入状态
     *
     * @param taskId 任务ID
     * @return 导入状态信息
     */
    ImportStatusVO getImportStatus(String taskId);

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

2.2.2 实体中介导入Service接口

public interface ICcdiIntermediaryEntityImportService {

    /**
     * 异步导入实体中介数据
     *
     * @param excelList        Excel数据列表
     * @param isUpdateSupport  是否更新已存在的数据
     * @param taskId           任务ID
     * @param userName         当前用户名(用于审计字段)
     */
    void importEntityAsync(List<CcdiIntermediaryEntityExcel> excelList,
                           Boolean isUpdateSupport,
                           String taskId,
                           String userName);

    /**
     * 查询导入状态
     *
     * @param taskId 任务ID
     * @return 导入状态信息
     */
    ImportStatusVO getImportStatus(String taskId);

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

2.2.3 实现类核心逻辑

个人中介导入实现:

@Service
@EnableAsync
public class CcdiIntermediaryPersonImportServiceImpl
    implements ICcdiIntermediaryPersonImportService {

    @Resource
    private CcdiBizIntermediaryMapper intermediaryMapper;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    @Async
    @Transactional
    public void importPersonAsync(List<CcdiIntermediaryPersonExcel> excelList,
                                  Boolean isUpdateSupport,
                                  String taskId,
                                  String userName) {
        List<CcdiBizIntermediary> newRecords = new ArrayList<>();
        List<CcdiBizIntermediary> updateRecords = new ArrayList<>();
        List<IntermediaryPersonImportFailureVO> failures = new ArrayList<>();

        // 1. 批量查询已存在的证件号
        Set<String> existingPersonIds = getExistingPersonIds(excelList);

        // 2. 分类数据
        for (CcdiIntermediaryPersonExcel excel : excelList) {
            try {
                // 验证必填字段
                if (StringUtils.isEmpty(excel.getName())) {
                    throw new RuntimeException("姓名不能为空");
                }
                if (StringUtils.isEmpty(excel.getPersonId())) {
                    throw new RuntimeException("证件号码不能为空");
                }

                CcdiBizIntermediary person = new CcdiBizIntermediary();
                BeanUtils.copyProperties(excel, person);
                person.setPersonType("中介");
                person.setDataSource("IMPORT");

                if (existingPersonIds.contains(excel.getPersonId())) {
                    if (isUpdateSupport) {
                        person.setUpdateBy(userName);
                        updateRecords.add(person);
                    } else {
                        throw new RuntimeException("该证件号已存在");
                    }
                } else {
                    person.setCreateBy(userName);
                    person.setUpdateBy(userName);
                    newRecords.add(person);
                }
            } catch (Exception e) {
                IntermediaryPersonImportFailureVO failure = new IntermediaryPersonImportFailureVO();
                BeanUtils.copyProperties(excel, failure);
                failure.setErrorMessage(e.getMessage());
                failures.add(failure);
            }
        }

        // 3. 批量插入
        if (!newRecords.isEmpty()) {
            intermediaryMapper.insertBatch(newRecords);
        }

        // 4. 批量更新
        if (!updateRecords.isEmpty()) {
            intermediaryMapper.updateBatch(updateRecords);
        }

        // 5. 保存失败记录到Redis
        if (!failures.isEmpty()) {
            String failuresKey = "import:intermediary-person:" + taskId + ":failures";
            redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
        }

        // 6. 更新最终状态
        ImportResult result = new ImportResult();
        result.setTotalCount(excelList.size());
        result.setSuccessCount(newRecords.size() + updateRecords.size());
        result.setFailureCount(failures.size());

        String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
        updateImportStatus("intermediary-person", taskId, finalStatus, result);
    }
}

实体中介导入实现:

  • 与个人中介类似
  • 使用CcdiEnterpriseBaseInfo实体
  • 唯一键是socialCreditCode
  • Redis Key使用intermediary-entity

2.3 Controller层设计

2.3.1 修改个人中介导入接口

@Resource
private ICcdiIntermediaryPersonImportService personImportService;

@PostMapping("/importPersonData")
public AjaxResult importPersonData(MultipartFile file,
                                   @RequestParam(defaultValue = "false") boolean updateSupport)
    throws Exception {
    List<CcdiIntermediaryPersonExcel> list = EasyExcelUtil.importExcel(
        file.getInputStream(),
        CcdiIntermediaryPersonExcel.class
    );

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

    // 生成任务ID
    String taskId = UUID.randomUUID().toString();

    // 获取当前用户名
    String userName = getUsername();

    // 初始化导入状态到Redis
    initImportStatus("intermediary-person", taskId, list.size());

    // 提交异步任务
    personImportService.importPersonAsync(list, updateSupport, taskId, userName);

    // 立即返回,不等待后台任务完成
    ImportResultVO result = new ImportResultVO();
    result.setTaskId(taskId);
    result.setStatus("PROCESSING");
    result.setMessage("导入任务已提交,正在后台处理");

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

2.3.2 新增个人中介状态查询接口

@GetMapping("/importPersonStatus/{taskId}")
public AjaxResult getPersonImportStatus(@PathVariable String taskId) {
    try {
        ImportStatusVO status = personImportService.getImportStatus(taskId);
        return success(status);
    } catch (Exception e) {
        return error(e.getMessage());
    }
}

2.3.3 新增个人中介失败记录查询接口

@GetMapping("/importPersonFailures/{taskId}")
public TableDataInfo getPersonImportFailures(
        @PathVariable String taskId,
        @RequestParam(defaultValue = "1") Integer pageNum,
        @RequestParam(defaultValue = "10") Integer pageSize) {

    List<IntermediaryPersonImportFailureVO> failures =
        personImportService.getImportFailures(taskId);

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

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

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

2.3.4 实体中介相关接口

类似地,为实体中介添加三个接口:

  • /importEntityData - 导入接口
  • /importEntityStatus/{taskId} - 状态查询接口
  • /importEntityFailures/{taskId} - 失败记录查询接口

三、前端实现设计

3.1 API定义

ruoyi-ui/src/api/ccdiIntermediary.js 中添加:

// 查询个人中介导入状态
export function getPersonImportStatus(taskId) {
  return request({
    url: '/ccdi/intermediary/importPersonStatus/' + taskId,
    method: 'get'
  })
}

// 查询个人中介导入失败记录
export function getPersonImportFailures(taskId, pageNum, pageSize) {
  return request({
    url: '/ccdi/intermediary/importPersonFailures/' + taskId,
    method: 'get',
    params: { pageNum, pageSize }
  })
}

// 查询实体中介导入状态
export function getEntityImportStatus(taskId) {
  return request({
    url: '/ccdi/intermediary/importEntityStatus/' + taskId,
    method: 'get'
  })
}

// 查询实体中介导入失败记录
export function getEntityImportFailures(taskId, pageNum, pageSize) {
  return request({
    url: '/ccdi/intermediary/importEntityFailures/' + taskId,
    method: 'get',
    params: { pageNum, pageSize }
  })
}

3.2 Vue组件修改

3.2.1 新增data属性

data() {
  return {
    // ...现有data

    // 个人中介导入相关
    personPollingTimer: null,
    personShowFailureButton: false,
    personCurrentTaskId: null,
    personFailureDialogVisible: false,
    personFailureList: [],
    personFailureLoading: false,
    personFailureTotal: 0,
    personFailureQueryParams: {
      pageNum: 1,
      pageSize: 10
    },

    // 实体中介导入相关
    entityPollingTimer: null,
    entityShowFailureButton: false,
    entityCurrentTaskId: null,
    entityFailureDialogVisible: false,
    entityFailureList: [],
    entityFailureLoading: false,
    entityFailureTotal: 0,
    entityFailureQueryParams: {
      pageNum: 1,
      pageSize: 10
    }
  }
}

3.2.2 修改handleFileSuccess方法

handleFileSuccess(response, file, fileList) {
  this.upload.isUploading = false;
  this.upload.open = false;

  if (response.code === 200) {
    const taskId = response.data.taskId;
    const importType = this.upload.importType; // 'person' 或 'entity'

    // 显示后台处理提示
    const typeName = importType === 'person' ? '个人中介' : '实体中介';
    this.$notify({
      title: '导入任务已提交',
      message: `${typeName}数据正在后台处理中,处理完成后将通知您`,
      type: 'info',
      duration: 3000
    });

    // 根据类型开始轮询
    if (importType === 'person') {
      this.startPersonImportPolling(taskId);
    } else {
      this.startEntityImportPolling(taskId);
    }
  } else {
    this.$modal.msgError(response.msg);
  }
}

3.2.3 轮询方法

methods: {
  // 个人中介轮询
  startPersonImportPolling(taskId) {
    this.personPollingTimer = setInterval(async () => {
      try {
        const response = await getPersonImportStatus(taskId);

        if (response.data && response.data.status !== 'PROCESSING') {
          clearInterval(this.personPollingTimer);
          this.handlePersonImportComplete(response.data);
        }
      } catch (error) {
        clearInterval(this.personPollingTimer);
        this.$modal.msgError('查询导入状态失败: ' + error.message);
      }
    }, 2000);
  },

  // 实体中介轮询
  startEntityImportPolling(taskId) {
    this.entityPollingTimer = setInterval(async () => {
      try {
        const response = await getEntityImportStatus(taskId);

        if (response.data && response.data.status !== 'PROCESSING') {
          clearInterval(this.entityPollingTimer);
          this.handleEntityImportComplete(response.data);
        }
      } catch (error) {
        clearInterval(this.entityPollingTimer);
        this.$modal.msgError('查询导入状态失败: ' + error.message);
      }
    }, 2000);
  },

  // 个人中介导入完成处理
  handlePersonImportComplete(statusResult) {
    if (statusResult.status === 'SUCCESS') {
      this.$notify({
        title: '导入完成',
        message: `个人中介数据全部成功!共导入${statusResult.totalCount}条`,
        type: 'success',
        duration: 5000
      });
      this.getList();
    } else if (statusResult.failureCount > 0) {
      this.$notify({
        title: '导入完成',
        message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}条`,
        type: 'warning',
        duration: 5000
      });

      this.personShowFailureButton = true;
      this.personCurrentTaskId = statusResult.taskId;
      this.getList();
    }
  },

  // 实体中介导入完成处理
  handleEntityImportComplete(statusResult) {
    if (statusResult.status === 'SUCCESS') {
      this.$notify({
        title: '导入完成',
        message: `实体中介数据全部成功!共导入${statusResult.totalCount}条`,
        type: 'success',
        duration: 5000
      });
      this.getList();
    } else if (statusResult.failureCount > 0) {
      this.$notify({
        title: '导入完成',
        message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}条`,
        type: 'warning',
        duration: 5000
      });

      this.entityShowFailureButton = true;
      this.entityCurrentTaskId = statusResult.taskId;
      this.getList();
    }
  }
}

3.2.4 生命周期销毁钩子

beforeDestroy() {
  // 清除个人中介轮询定时器
  if (this.personPollingTimer) {
    clearInterval(this.personPollingTimer);
    this.personPollingTimer = null;
  }

  // 清除实体中介轮询定时器
  if (this.entityPollingTimer) {
    clearInterval(this.entityPollingTimer);
    this.entityPollingTimer = null;
  }
}

3.3 UI组件设计

3.3.1 查看失败记录按钮

<!-- 个人中介导入失败记录按钮 -->
<el-col :span="1.5" v-if="personShowFailureButton">
  <el-button
    type="warning"
    plain
    icon="el-icon-warning"
    size="mini"
    @click="viewPersonImportFailures"
  >查看个人中介导入失败记录</el-button>
</el-col>

<!-- 实体中介导入失败记录按钮 -->
<el-col :span="1.5" v-if="entityShowFailureButton">
  <el-button
    type="warning"
    plain
    icon="el-icon-warning"
    size="mini"
    @click="viewEntityImportFailures"
  >查看实体中介导入失败记录</el-button>
</el-col>

3.3.2 个人中介失败记录对话框

<el-dialog
  title="个人中介导入失败记录"
  :visible.sync="personFailureDialogVisible"
  width="1200px"
  append-to-body
>
  <el-table :data="personFailureList" v-loading="personFailureLoading">
    <el-table-column label="姓名" prop="name" align="center" width="120" />
    <el-table-column label="证件号码" prop="personId" align="center" width="180" />
    <el-table-column label="人员类型" prop="personType" align="center" width="120" />
    <el-table-column label="性别" prop="gender" align="center" width="80" />
    <el-table-column label="手机号码" prop="mobile" align="center" width="130" />
    <el-table-column label="所在公司" prop="company" align="center" width="150" />
    <el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
  </el-table>

  <pagination
    v-show="personFailureTotal > 0"
    :total="personFailureTotal"
    :page.sync="personFailureQueryParams.pageNum"
    :limit.sync="personFailureQueryParams.pageSize"
    @pagination="getPersonFailureList"
  />

  <div slot="footer" class="dialog-footer">
    <el-button @click="personFailureDialogVisible = false">关闭</el-button>
  </div>
</el-dialog>

3.3.3 实体中介失败记录对话框

<el-dialog
  title="实体中介导入失败记录"
  :visible.sync="entityFailureDialogVisible"
  width="1200px"
  append-to-body
>
  <el-table :data="entityFailureList" v-loading="entityFailureLoading">
    <el-table-column label="机构名称" prop="enterpriseName" align="center" width="200" />
    <el-table-column label="统一社会信用代码" prop="socialCreditCode" align="center" width="180" />
    <el-table-column label="主体类型" prop="enterpriseType" align="center" width="120" />
    <el-table-column label="企业性质" prop="enterpriseNature" align="center" width="120" />
    <el-table-column label="法定代表人" prop="legalRepresentative" align="center" width="120" />
    <el-table-column label="成立日期" prop="establishDate" align="center" width="120" />
    <el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
  </el-table>

  <pagination
    v-show="entityFailureTotal > 0"
    :total="entityFailureTotal"
    :page.sync="entityFailureQueryParams.pageNum"
    :limit.sync="entityFailureQueryParams.pageSize"
    @pagination="getEntityFailureList"
  />

  <div slot="footer" class="dialog-footer">
    <el-button @click="entityFailureDialogVisible = false">关闭</el-button>
  </div>
</el-dialog>

3.3.4 查询失败记录方法

methods: {
  // 查看个人中介导入失败记录
  viewPersonImportFailures() {
    this.personFailureDialogVisible = true;
    this.getPersonFailureList();
  },

  // 获取个人中介失败记录列表
  getPersonFailureList() {
    this.personFailureLoading = true;
    getPersonImportFailures(
      this.personCurrentTaskId,
      this.personFailureQueryParams.pageNum,
      this.personFailureQueryParams.pageSize
    ).then(response => {
      this.personFailureList = response.rows;
      this.personFailureTotal = response.total;
      this.personFailureLoading = false;
    }).catch(error => {
      this.personFailureLoading = false;
      this.$modal.msgError('查询失败记录失败: ' + error.message);
    });
  },

  // 查看实体中介导入失败记录
  viewEntityImportFailures() {
    this.entityFailureDialogVisible = true;
    this.getEntityFailureList();
  },

  // 获取实体中介失败记录列表
  getEntityFailureList() {
    this.entityFailureLoading = true;
    getEntityImportFailures(
      this.entityCurrentTaskId,
      this.entityFailureQueryParams.pageNum,
      this.entityFailureQueryParams.pageSize
    ).then(response => {
      this.entityFailureList = response.rows;
      this.entityFailureTotal = response.total;
      this.entityFailureLoading = false;
    }).catch(error => {
      this.entityFailureLoading = false;
      this.$modal.msgError('查询失败记录失败: ' + error.message);
    });
  }
}

四、数据验证与错误处理

4.1 个人中介数据验证规则

4.1.1 必填字段验证

  • 姓名 (name)
  • 证件号码 (personId)

4.1.2 唯一性验证

  • 证件号码(personId)必须唯一
  • 批量查询已存在的证件号:
private Set<String> getExistingPersonIds(List<CcdiIntermediaryPersonExcel> excelList) {
    List<String> personIds = excelList.stream()
            .map(CcdiIntermediaryPersonExcel::getPersonId)
            .filter(StringUtils::isNotEmpty)
            .collect(Collectors.toList());

    if (personIds.isEmpty()) {
        return Collections.emptySet();
    }

    LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
    wrapper.in(CcdiBizIntermediary::getPersonId, personIds);
    List<CcdiBizIntermediary> existingList = intermediaryMapper.selectList(wrapper);

    return existingList.stream()
            .map(CcdiBizIntermediary::getPersonId)
            .collect(Collectors.toSet());
}

4.2 实体中介数据验证规则

4.2.1 必填字段验证

  • 机构名称 (enterpriseName)

4.2.2 唯一性验证

  • 统一社会信用代码(socialCreditCode)必须唯一(如果不为空)
  • 批量查询已存在的统一社会信用代码:
private Set<String> getExistingSocialCreditCodes(List<CcdiIntermediaryEntityExcel> excelList) {
    List<String> codes = excelList.stream()
            .map(CcdiIntermediaryEntityExcel::getSocialCreditCode)
            .filter(StringUtils::isNotEmpty)
            .collect(Collectors.toList());

    if (codes.isEmpty()) {
        return Collections.emptySet();
    }

    LambdaQueryWrapper<CcdiEnterpriseBaseInfo> wrapper = new LambdaQueryWrapper<>();
    wrapper.in(CcdiEnterpriseBaseInfo::getSocialCreditCode, codes);
    List<CcdiEnterpriseBaseInfo> existingList = enterpriseMapper.selectList(wrapper);

    return existingList.stream()
            .map(CcdiEnterpriseBaseInfo::getSocialCreditCode)
            .collect(Collectors.toSet());
}

4.3 错误处理流程

4.3.1 单条数据错误

try {
    // 验证和处理数据
} catch (Exception e) {
    IntermediaryPersonImportFailureVO failure = new IntermediaryPersonImportFailureVO();
    BeanUtils.copyProperties(excel, failure);
    failure.setErrorMessage(e.getMessage());
    failures.add(failure);
    // 继续处理下一条数据
}

4.3.2 状态更新逻辑

private void updateImportStatus(String taskType, String taskId, String status, ImportResult result) {
    String key = "import:" + taskType + ":" + taskId;
    Map<String, Object> statusData = new HashMap<>();
    statusData.put("status", status);
    statusData.put("successCount", result.getSuccessCount());
    statusData.put("failureCount", result.getFailureCount());
    statusData.put("progress", 100);
    statusData.put("endTime", System.currentTimeMillis());

    if ("SUCCESS".equals(status)) {
        statusData.put("message", "全部成功!共导入" + result.getTotalCount() + "条数据");
    } else {
        statusData.put("message", "成功" + result.getSuccessCount() +
                            "条,失败" + result.getFailureCount() + "条");
    }

    redisTemplate.opsForHash().putAll(key, statusData);
}

五、文件清单

5.1 新增文件

文件路径 说明
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/IntermediaryPersonImportFailureVO.java 个人中介导入失败记录VO
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/IntermediaryEntityImportFailureVO.java 实体中介导入失败记录VO
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryPersonImportService.java 个人中介异步导入Service接口
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryEntityImportService.java 实体中介异步导入Service接口
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryPersonImportServiceImpl.java 个人中介异步导入Service实现
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryEntityImportServiceImpl.java 实体中介异步导入Service实现
test/test_intermediary_import.py 测试脚本

5.2 修改文件

文件路径 修改内容
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java 修改导入接口,添加状态查询和失败记录查询接口(个人+实体共6个接口)
ruoyi-ui/src/api/ccdiIntermediary.js 添加导入状态和失败记录查询API(4个新方法)
ruoyi-ui/src/views/ccdiIntermediary/index.vue 添加轮询逻辑、失败记录UI(两套独立组件)
doc/api/ccdi_intermediary_api.md 更新API文档(新增导入相关接口文档)

5.3 复用组件

组件 说明
ImportResultVO 导入结果VO(复用员工导入)
ImportStatusVO 导入状态VO(复用员工导入)
AsyncConfig 异步配置(复用员工导入)
importExecutor 导入任务线程池(复用员工导入)

六、实施步骤

6.1 后端实施步骤

步骤1: 创建失败记录VO类

文件:

  • ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/IntermediaryPersonImportFailureVO.java
  • ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/IntermediaryEntityImportFailureVO.java

步骤2: 创建Service接口

文件:

  • ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryPersonImportService.java
  • ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryEntityImportService.java

步骤3: 实现Service

文件:

  • ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryPersonImportServiceImpl.java
  • ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryEntityImportServiceImpl.java

操作:

  • 实现ICcdiIntermediaryPersonImportService接口
  • 实现ICcdiIntermediaryEntityImportService接口
  • 添加@EnableAsync注解
  • 注入Mapper和RedisTemplate
  • 实现异步导入逻辑(包含userName审计字段)
  • 实现状态查询逻辑
  • 实现失败记录查询逻辑

步骤4: 修改Controller

文件:

  • ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java

操作:

  • 注入两个导入Service
  • 修改importPersonData()方法(改为异步)
  • 修改importEntityData()方法(改为异步)
  • 添加getPersonImportStatus()方法
  • 添加getPersonImportFailures()方法
  • 添加getEntityImportStatus()方法
  • 添加getEntityImportFailures()方法
  • 添加Swagger注解

6.2 前端实施步骤

步骤5: 修改API定义

文件:

  • ruoyi-ui/src/api/ccdiIntermediary.js

操作:

  • 添加getPersonImportStatus()方法
  • 添加getPersonImportFailures()方法
  • 添加getEntityImportStatus()方法
  • 添加getEntityImportFailures()方法

步骤6: 修改Vue组件

文件:

  • ruoyi-ui/src/views/ccdiIntermediary/index.vue

操作:

  • 添加data属性(个人+实体两套)
  • 修改handleFileSuccess()方法
  • 添加轮询方法(个人+实体)
  • 添加完成处理方法(个人+实体)
  • 添加失败记录查询方法(个人+实体)
  • 添加beforeDestroy()生命周期钩子
  • 添加两个"查看失败记录"按钮
  • 添加两个失败记录对话框

6.3 测试与文档

步骤7: 生成测试脚本

文件:

  • test/test_intermediary_import.py

操作:

  • 编写测试脚本
  • 包含:登录、个人/实体导入、状态查询、失败记录查询等测试用例

步骤8: 手动测试

操作:

  • 测试个人中介导入(全部成功、部分失败)
  • 测试实体中介导入(全部成功、部分失败)
  • 测试轮询机制
  • 测试失败记录UI

步骤9: 更新API文档

文件:

  • doc/api/ccdi_intermediary_api.md

操作:

  • 添加导入相关接口文档
  • 包含:请求参数、响应示例、错误码说明

步骤10: 代码提交

操作:

git add .
git commit -m "feat: 实现中介库异步导入功能"

七、测试计划

7.1 功能测试

测试项 测试内容 预期结果
个人中介-正常导入 导入100-500条有效个人中介数据 全部成功,状态为SUCCESS
个人中介-重复导入不更新 personId已存在,updateSupport=false 导入失败,提示"该证件号已存在"
个人中介-重复导入更新 personId已存在,updateSupport=true 更新已有数据,状态为SUCCESS
个人中介-部分错误 混合有效数据和无效数据 部分成功,状态为PARTIAL_SUCCESS
实体中介-正常导入 导入100-500条有效实体中介数据 全部成功,状态为SUCCESS
实体中介-重复导入不更新 socialCreditCode已存在,updateSupport=false 导入失败,提示"该统一社会信用代码已存在"
实体中介-重复导入更新 socialCreditCode已存在,updateSupport=true 更新已有数据,状态为SUCCESS
实体中介-部分错误 混合有效数据和无效数据 部分成功,状态为PARTIAL_SUCCESS
状态查询 调用getImportStatus接口 返回正确状态和进度
失败记录查询 调用getImportFailures接口 返回失败记录列表,支持分页
前端轮询 导入后观察轮询行为 每2秒查询一次,完成后停止
完成通知 导入完成后观察通知 显示正确的成功/警告通知
前端UI分离 个人和实体导入分别显示不同按钮 按钮文案清晰,不会混淆
失败记录UI-个人 点击"查看个人中介导入失败记录"按钮 显示个人中介对话框,正确展示失败数据
失败记录UI-实体 点击"查看实体中介导入失败记录"按钮 显示实体中介对话框,正确展示失败数据

7.2 性能测试

测试项 测试数据量 性能要求
导入接口响应时间 任意 < 500ms(立即返回taskId)
个人中介数据处理 500条 < 5秒
个人中介数据处理 1000条 < 10秒
实体中介数据处理 500条 < 5秒
实体中介数据处理 1000条 < 10秒
Redis存储 任意 数据正确存储,TTL为7天
前端轮询 任意 不阻塞UI,不影响用户操作

7.3 异常测试

测试项 测试内容 预期结果
空文件 上传空Excel文件 返回错误提示"至少需要一条数据"
格式错误 上传非Excel文件 解析失败,返回错误提示
不存在的taskId 查询导入状态时传入随机UUID 返回错误提示"任务不存在或已过期"
并发导入 同时上传3个Excel文件(个人+实体) 生成3个不同的taskId,各自独立处理,互不影响
网络中断 导入过程中断开网络 异步任务继续执行,恢复后可查询状态

7.4 数据验证测试

个人中介

测试项 测试内容 预期结果
姓名缺失 缺少name字段 记录到失败列表,提示"姓名不能为空"
证件号缺失 缺少personId字段 记录到失败列表,提示"证件号码不能为空"
证件号重复 personId已存在且updateSupport=false 记录到失败列表,提示"该证件号已存在"

实体中介

测试项 测试内容 预期结果
机构名称缺失 缺少enterpriseName字段 记录到失败列表,提示"机构名称不能为空"
统一社会信用代码重复 socialCreditCode已存在且updateSupport=false 记录到失败列表,提示"该统一社会信用代码已存在"

八、参考文档

  • 员工信息异步导入实施计划: doc/plans/2026-02-06-employee-async-import.md
  • 员工信息异步导入设计文档: doc/plans/2026-02-06-employee-async-import-design.md
  • 招聘信息异步导入设计文档: doc/plans/2026-02-06-recruitment-async-import-design.md
  • 员工信息导入API文档: doc/api/ccdi-employee-import-api.md

设计版本: 1.0 创建日期: 2026-02-06 设计人员: Claude 审核状态: 待审核