# 招聘信息异步导入功能设计文档

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

---

## 一、架构概述

### 1.1 核心架构

招聘信息异步导入完全复用员工信息异步导入的架构模式:

- **异步处理层**: 使用Spring `@Async`注解,通过现有的`importExecutor`线程池执行异步任务
- **状态存储层**: 使用Redis Hash存储导入状态,Key格式为`import:recruitment:{taskId}`,TTL为7天
- **失败记录层**: 使用Redis String存储失败记录,Key格式为`import:recruitment:{taskId}:failures`
- **API层**: 提供三个接口 - 导入接口(返回taskId)、状态查询接口、失败记录查询接口

### 1.2 数据流程

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

### 1.3 Redis Key设计

- **状态Key**: `import:recruitment:{taskId}` (Hash结构)
- **失败记录Key**: `import:recruitment:{taskId}:failures` (String结构,存储JSON数组)
- **TTL**: 7天

### 1.4 状态枚举

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

---

## 二、组件设计

### 2.1 VO类设计

#### 2.1.1 ImportResultVO (复用员工导入)

```java
@Data
@Schema(description = "导入结果")
public class ImportResultVO {
    @Schema(description = "任务ID")
    private String taskId;

    @Schema(description = "状态: PROCESSING-处理中, SUCCESS-成功, PARTIAL_SUCCESS-部分成功, FAILED-失败")
    private String status;

    @Schema(description = "消息")
    private String message;
}
```

#### 2.1.2 ImportStatusVO (复用员工导入)

```java
@Data
@Schema(description = "导入状态")
public class ImportStatusVO {
    @Schema(description = "任务ID")
    private String taskId;

    @Schema(description = "状态")
    private String status;

    @Schema(description = "总记录数")
    private Integer totalCount;

    @Schema(description = "成功数")
    private Integer successCount;

    @Schema(description = "失败数")
    private Integer failureCount;

    @Schema(description = "进度百分比")
    private Integer progress;

    @Schema(description = "开始时间戳")
    private Long startTime;

    @Schema(description = "结束时间戳")
    private Long endTime;

    @Schema(description = "状态消息")
    private String message;
}
```

#### 2.1.3 RecruitmentImportFailureVO (新建,适配招聘信息)

```java
@Data
@Schema(description = "招聘信息导入失败记录")
public class RecruitmentImportFailureVO {

    @Schema(description = "招聘项目编号")
    private String recruitId;

    @Schema(description = "招聘项目名称")
    private String recruitName;

    @Schema(description = "应聘人员姓名")
    private String candName;

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

    @Schema(description = "录用情况")
    private String admitStatus;

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

### 2.2 Service层设计

#### 2.2.1 接口定义

```java
public interface ICcdiStaffRecruitmentImportService {

    /**
     * 异步导入招聘信息数据
     *
     * @param excelList        Excel数据列表
     * @param isUpdateSupport  是否更新已存在的数据
     * @param taskId           任务ID
     */
    void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> excelList,
                                Boolean isUpdateSupport,
                                String taskId);

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

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

#### 2.2.2 实现类核心逻辑

**类注解:**

```java
@Service
@EnableAsync
public class CcdiStaffRecruitmentImportServiceImpl
    implements ICcdiStaffRecruitmentImportService {

    @Resource
    private CcdiStaffRecruitmentMapper recruitmentMapper;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;
}
```

**异步导入方法:**

```java
@Override
@Async
public void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> excelList,
                                   Boolean isUpdateSupport,
                                   String taskId) {
    List<CcdiStaffRecruitment> newRecords = new ArrayList<>();
    List<CcdiStaffRecruitment> updateRecords = new ArrayList<>();
    List<RecruitmentImportFailureVO> failures = new ArrayList<>();

    // 1. 批量查询已存在的招聘项目编号
    Set<String> existingRecruitIds = getExistingRecruitIds(excelList);

    // 2. 分类数据
    for (CcdiStaffRecruitmentExcel excel : excelList) {
        try {
            // 验证数据
            validateRecruitmentData(excel, isUpdateSupport, existingRecruitIds);

            CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment();
            BeanUtils.copyProperties(excel, recruitment);

            if (existingRecruitIds.contains(excel.getRecruitId())) {
                if (isUpdateSupport) {
                    updateRecords.add(recruitment);
                } else {
                    throw new RuntimeException("该招聘项目编号已存在");
                }
            } else {
                newRecords.add(recruitment);
            }
        } catch (Exception e) {
            RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO();
            BeanUtils.copyProperties(excel, failure);
            failure.setErrorMessage(e.getMessage());
            failures.add(failure);
        }
    }

    // 3. 批量插入新数据
    if (!newRecords.isEmpty()) {
        recruitmentMapper.insertBatch(newRecords);
    }

    // 4. 批量更新已有数据
    if (!updateRecords.isEmpty() && isUpdateSupport) {
        recruitmentMapper.updateBatch(updateRecords);
    }

    // 5. 保存失败记录到Redis
    if (!failures.isEmpty()) {
        String failuresKey = "import:recruitment:" + 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(taskId, finalStatus, result);
}
```

### 2.3 Controller层设计

#### 2.3.1 修改导入接口

```java
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
    List<CcdiStaffRecruitmentExcel> list = EasyExcelUtil.importExcel(
        file.getInputStream(),
        CcdiStaffRecruitmentExcel.class
    );

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

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

    // 提交异步任务
    importAsyncService.importRecruitmentAsync(list, updateSupport, taskId);

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

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

#### 2.3.2 新增状态查询接口

```java
@GetMapping("/importStatus/{taskId}")
public AjaxResult getImportStatus(@PathVariable String taskId) {
    try {
        ImportStatusVO status = importAsyncService.getImportStatus(taskId);
        return success(status);
    } catch (Exception e) {
        return error(e.getMessage());
    }
}
```

#### 2.3.3 新增失败记录查询接口

```java
@GetMapping("/importFailures/{taskId}")
public TableDataInfo getImportFailures(
        @PathVariable String taskId,
        @RequestParam(defaultValue = "1") Integer pageNum,
        @RequestParam(defaultValue = "10") Integer pageSize) {

    List<RecruitmentImportFailureVO> failures =
        importAsyncService.getImportFailures(taskId);

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

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

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

---

## 三、数据验证与错误处理

### 3.1 数据验证规则

#### 3.1.1 必填字段验证

- 招聘项目编号 (`recruitId`)
- 招聘项目名称 (`recruitName`)
- 职位名称 (`posName`)
- 职位类别 (`posCategory`)
- 职位描述 (`posDesc`)
- 应聘人员姓名 (`candName`)
- 应聘人员学历 (`candEdu`)
- 证件号码 (`candId`)
- 应聘人员毕业院校 (`candSchool`)
- 应聘人员专业 (`candMajor`)
- 应聘人员毕业年月 (`candGrad`)
- 录用情况 (`admitStatus`)

#### 3.1.2 格式验证

```java
// 证件号码格式验证
String idCardError = IdCardUtil.getErrorMessage(excel.getCandId());
if (idCardError != null) {
    throw new RuntimeException("证件号码" + idCardError);
}

// 毕业年月格式验证(YYYYMM)
if (!excel.getCandGrad().matches("^((19|20)\\d{2})(0[1-9]|1[0-2])$")) {
    throw new RuntimeException("毕业年月格式不正确,应为YYYYMM");
}

// 录用情况验证
if (AdmitStatus.getDescByCode(excel.getAdmitStatus()) == null) {
    throw new RuntimeException("录用情况只能填写'录用'、'未录用'或'放弃'");
}
```

#### 3.1.3 唯一性验证

```java
// 批量查询已存在的招聘项目编号
private Set<String> getExistingRecruitIds(List<CcdiStaffRecruitmentExcel> excelList) {
    List<String> recruitIds = excelList.stream()
            .map(CcdiStaffRecruitmentExcel::getRecruitId)
            .filter(StringUtils::isNotEmpty)
            .collect(Collectors.toList());

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

    LambdaQueryWrapper<CcdiStaffRecruitment> wrapper = new LambdaQueryWrapper<>();
    wrapper.in(CcdiStaffRecruitment::getRecruitId, recruitIds);
    List<CcdiStaffRecruitment> existingRecruitments =
        recruitmentMapper.selectList(wrapper);

    return existingRecruitments.stream()
            .map(CcdiStaffRecruitment::getRecruitId)
            .collect(Collectors.toSet());
}
```

### 3.2 错误处理流程

#### 3.2.1 单条数据错误

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

#### 3.2.2 状态更新逻辑

```java
private void updateImportStatus(String taskId, String status, ImportResult result) {
    String key = "import:recruitment:" + 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);
}
```

---

## 四、前端实现

### 4.1 API定义

在 `ruoyi-ui/src/api/ccdiStaffRecruitment.js` 中添加:

```javascript
// 查询导入状态
export function getImportStatus(taskId) {
  return request({
    url: '/ccdi/staffRecruitment/importStatus/' + taskId,
    method: 'get'
  })
}

// 查询导入失败记录
export function getImportFailures(taskId, pageNum, pageSize) {
  return request({
    url: '/ccdi/staffRecruitment/importFailures/' + taskId,
    method: 'get',
    params: { pageNum, pageSize }
  })
}
```

### 4.2 Vue组件修改

在 `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue` 中修改:

#### 4.2.1 data属性

```javascript
data() {
  return {
    // ...现有data
    pollingTimer: null,
    showFailureButton: false,
    currentTaskId: null,
    failureDialogVisible: false,
    failureList: [],
    failureLoading: false,
    failureTotal: 0,
    failureQueryParams: {
      pageNum: 1,
      pageSize: 10
    }
  }
}
```

#### 4.2.2 handleFileSuccess方法

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

  if (response.code === 200) {
    const taskId = response.data.taskId;

    // 显示后台处理提示
    this.$notify({
      title: '导入任务已提交',
      message: '正在后台处理中,处理完成后将通知您',
      type: 'info',
      duration: 3000
    });

    // 开始轮询检查状态
    this.startImportStatusPolling(taskId);
  } else {
    this.$modal.msgError(response.msg);
  }
}
```

#### 4.2.3 轮询方法

```javascript
methods: {
  startImportStatusPolling(taskId) {
    this.pollingTimer = setInterval(async () => {
      try {
        const response = await getImportStatus(taskId);

        if (response.data && response.data.status !== 'PROCESSING') {
          clearInterval(this.pollingTimer);
          this.handleImportComplete(response.data);
        }
      } catch (error) {
        clearInterval(this.pollingTimer);
        this.$modal.msgError('查询导入状态失败: ' + error.message);
      }
    }, 2000); // 每2秒轮询一次
  },

  handleImportComplete(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.showFailureButton = true;
      this.currentTaskId = statusResult.taskId;

      // 刷新列表
      this.getList();
    }
  }
}
```

#### 4.2.4 生命周期销毁钩子

```javascript
beforeDestroy() {
  // 组件销毁时清除定时器
  if (this.pollingTimer) {
    clearInterval(this.pollingTimer);
    this.pollingTimer = null;
  }
}
```

#### 4.2.5 失败记录对话框

**模板部分:**

```vue
<!-- 查看失败记录按钮 -->
<el-col :span="1.5" v-if="showFailureButton">
  <el-button
    type="warning"
    plain
    icon="el-icon-warning"
    size="mini"
    @click="viewImportFailures"
  >查看导入失败记录</el-button>
</el-col>

<!-- 导入失败记录对话框 -->
<el-dialog
  title="导入失败记录"
  :visible.sync="failureDialogVisible"
  width="1200px"
  append-to-body
>
  <el-table :data="failureList" v-loading="failureLoading">
    <el-table-column label="招聘项目编号" prop="recruitId" align="center" />
    <el-table-column label="招聘项目名称" prop="recruitName" align="center" />
    <el-table-column label="应聘人员姓名" prop="candName" align="center" />
    <el-table-column label="证件号码" prop="candId" align="center" />
    <el-table-column label="录用情况" prop="admitStatus" align="center" />
    <el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
  </el-table>

  <pagination
    v-show="failureTotal > 0"
    :total="failureTotal"
    :page.sync="failureQueryParams.pageNum"
    :limit.sync="failureQueryParams.pageSize"
    @pagination="getFailureList"
  />

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

**方法部分:**

```javascript
methods: {
  viewImportFailures() {
    this.failureDialogVisible = true;
    this.getFailureList();
  },

  getFailureList() {
    this.failureLoading = true;
    getImportFailures(
      this.currentTaskId,
      this.failureQueryParams.pageNum,
      this.failureQueryParams.pageSize
    ).then(response => {
      this.failureList = response.rows;
      this.failureTotal = response.total;
      this.failureLoading = false;
    }).catch(error => {
      this.failureLoading = false;
      this.$modal.msgError('查询失败记录失败: ' + error.message);
    });
  }
}
```

---

## 五、测试计划

### 5.1 功能测试

| 测试项      | 测试内容                             | 预期结果                    |
|----------|----------------------------------|-------------------------|
| 正常导入     | 导入100-500条有效数据                   | 全部成功,状态为SUCCESS         |
| 重复导入-不更新 | recruitId已存在,updateSupport=false | 导入失败,提示"该招聘项目编号已存在"     |
| 重复导入-更新  | recruitId已存在,updateSupport=true  | 更新已有数据,状态为SUCCESS       |
| 部分错误     | 混合有效数据和无效数据                      | 部分成功,状态为PARTIAL_SUCCESS |
| 状态查询     | 调用getImportStatus接口              | 返回正确状态和进度               |
| 失败记录查询   | 调用getImportFailures接口            | 返回失败记录列表,支持分页           |
| 前端轮询     | 导入后观察轮询行为                        | 每2秒查询一次,完成后停止           |
| 完成通知     | 导入完成后观察通知                        | 显示正确的成功/警告通知            |
| 失败记录UI   | 点击"查看失败记录"按钮                     | 显示对话框,正确展示失败数据          |

### 5.2 性能测试

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

### 5.3 异常测试

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

### 5.4 数据验证测试

| 测试项      | 测试内容                      | 预期结果                  |
|----------|---------------------------|-----------------------|
| 必填字段缺失   | 缺少recruitId、candName等必填字段 | 记录到失败列表,提示具体字段不能为空    |
| 证件号格式错误  | 填写错误的身份证号                 | 记录到失败列表,提示证件号码格式错误    |
| 毕业年月格式错误 | 填写非YYYYMM格式               | 记录到失败列表,提示毕业年月格式不正确   |
| 录用情况无效   | 填写"录用"、"未录用"、"放弃"之外的值     | 记录到失败列表,提示录用情况只能填写指定值 |

---

## 六、实施步骤

### 6.1 后端实施步骤

#### 步骤1: 创建VO类

**文件:**

- `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/domain/vo/RecruitmentImportFailureVO.java`

**操作:**

- 创建`RecruitmentImportFailureVO`类
- 添加招聘信息相关字段
- 复用`ImportResultVO`和`ImportStatusVO`

#### 步骤2: 创建Service接口

**文件:**

- `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/ICcdiStaffRecruitmentImportService.java`

**操作:**

- 创建Service接口
- 定义三个方法:异步导入、查询状态、查询失败记录

#### 步骤3: 实现Service

**文件:**

- `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentImportServiceImpl.java`

**操作:**

- 实现`ICcdiStaffRecruitmentImportService`接口
- 添加`@EnableAsync`注解
- 注入`CcdiStaffRecruitmentMapper`和`RedisTemplate`
- 实现异步导入逻辑
- 实现状态查询逻辑
- 实现失败记录查询逻辑

#### 步骤4: 修改Controller

**文件:**

- `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/controller/CcdiStaffRecruitmentController.java`

**操作:**

- 注入`ICcdiStaffRecruitmentImportService`
- 修改`importData()`方法:调用异步服务,返回taskId
- 添加`getImportStatus()`方法
- 添加`getImportFailures()`方法
- 添加Swagger注解

### 6.2 前端实施步骤

#### 步骤5: 修改API定义

**文件:**

- `ruoyi-ui/src/api/ccdiStaffRecruitment.js`

**操作:**

- 添加`getImportStatus()`方法
- 添加`getImportFailures()`方法

#### 步骤6: 修改Vue组件

**文件:**

- `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`

**操作:**

- 添加data属性(pollingTimer、showFailureButton等)
- 修改`handleFileSuccess()`方法
- 添加`startImportStatusPolling()`方法
- 添加`handleImportComplete()`方法
- 添加`viewImportFailures()`方法
- 添加`getFailureList()`方法
- 添加`beforeDestroy()`生命周期钩子
- 添加"查看失败记录"按钮
- 添加失败记录对话框

### 6.3 测试与文档

#### 步骤7: 生成测试脚本

**文件:**

- `test/test_recruitment_import.py`

**操作:**

- 编写测试脚本
- 包含:登录、导入、状态查询、失败记录查询等测试用例

#### 步骤8: 手动测试

**操作:**

- 启动后端服务
- 启动前端服务
- 执行完整功能测试
- 记录测试结果

#### 步骤9: 更新API文档

**文件:**

- `doc/api/ccdi_staff_recruitment_api.md`

**操作:**

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

#### 步骤10: 代码提交

**操作:**

```bash
git add .
git commit -m "feat: 实现招聘信息异步导入功能"
```

---

## 七、文件清单

### 7.1 新增文件

| 文件路径                                                                                                         | 说明                |
|--------------------------------------------------------------------------------------------------------------|-------------------|
| `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/domain/vo/RecruitmentImportFailureVO.java`               | 招聘信息导入失败记录VO      |
| `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/ICcdiStaffRecruitmentImportService.java`         | 招聘信息异步导入Service接口 |
| `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentImportServiceImpl.java` | 招聘信息异步导入Service实现 |
| `test/test_recruitment_import.py`                                                                            | 测试脚本              |

### 7.2 修改文件

| 文件路径                                                                                                | 修改内容                   |
|-----------------------------------------------------------------------------------------------------|------------------------|
| `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/controller/CcdiStaffRecruitmentController.java` | 修改导入接口,添加状态查询和失败记录查询接口 |
| `ruoyi-ui/src/api/ccdiStaffRecruitment.js`                                                          | 添加导入状态和失败记录查询API       |
| `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue`                                                 | 添加轮询逻辑和失败记录UI          |
| `doc/api/ccdi_staff_recruitment_api.md`                                                             | 更新API文档                |

### 7.3 复用组件

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

---

## 八、参考文档

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

---

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