diff --git a/doc/plans/2026-02-06-recruitment-async-import-design.md b/doc/plans/2026-02-06-recruitment-async-import-design.md new file mode 100644 index 0000000..b2da827 --- /dev/null +++ b/doc/plans/2026-02-06-recruitment-async-import-design.md @@ -0,0 +1,846 @@ +# 招聘信息异步导入功能设计文档 + +**创建日期:** 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 excelList, + Boolean isUpdateSupport, + String taskId); + + /** + * 查询导入状态 + * + * @param taskId 任务ID + * @return 导入状态信息 + */ + ImportStatusVO getImportStatus(String taskId); + + /** + * 获取导入失败记录 + * + * @param taskId 任务ID + * @return 失败记录列表 + */ + List getImportFailures(String taskId); +} +``` + +#### 2.2.2 实现类核心逻辑 + +**类注解:** +```java +@Service +@EnableAsync +public class CcdiStaffRecruitmentImportServiceImpl + implements ICcdiStaffRecruitmentImportService { + + @Resource + private CcdiStaffRecruitmentMapper recruitmentMapper; + + @Resource + private RedisTemplate redisTemplate; +} +``` + +**异步导入方法:** +```java +@Override +@Async +public void importRecruitmentAsync(List excelList, + Boolean isUpdateSupport, + String taskId) { + List newRecords = new ArrayList<>(); + List updateRecords = new ArrayList<>(); + List failures = new ArrayList<>(); + + // 1. 批量查询已存在的招聘项目编号 + Set 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 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 failures = + importAsyncService.getImportFailures(taskId); + + // 手动分页 + int fromIndex = (pageNum - 1) * pageSize; + int toIndex = Math.min(fromIndex + pageSize, failures.size()); + + List 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 getExistingRecruitIds(List excelList) { + List recruitIds = excelList.stream() + .map(CcdiStaffRecruitmentExcel::getRecruitId) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList()); + + if (recruitIds.isEmpty()) { + return Collections.emptySet(); + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(CcdiStaffRecruitment::getRecruitId, recruitIds); + List 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 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 + + + 查看导入失败记录 + + + + + + + + + + + + + + + + + +``` + +**方法部分:** +```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-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/RecruitmentImportFailureVO.java` + +**操作:** +- 创建`RecruitmentImportFailureVO`类 +- 添加招聘信息相关字段 +- 复用`ImportResultVO`和`ImportStatusVO` + +#### 步骤2: 创建Service接口 + +**文件:** +- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiStaffRecruitmentImportService.java` + +**操作:** +- 创建Service接口 +- 定义三个方法:异步导入、查询状态、查询失败记录 + +#### 步骤3: 实现Service + +**文件:** +- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentImportServiceImpl.java` + +**操作:** +- 实现`ICcdiStaffRecruitmentImportService`接口 +- 添加`@EnableAsync`注解 +- 注入`CcdiStaffRecruitmentMapper`和`RedisTemplate` +- 实现异步导入逻辑 +- 实现状态查询逻辑 +- 实现失败记录查询逻辑 + +#### 步骤4: 修改Controller + +**文件:** +- `ruoyi-ccdi/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-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/RecruitmentImportFailureVO.java` | 招聘信息导入失败记录VO | +| `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiStaffRecruitmentImportService.java` | 招聘信息异步导入Service接口 | +| `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentImportServiceImpl.java` | 招聘信息异步导入Service实现 | +| `test/test_recruitment_import.py` | 测试脚本 | + +### 7.2 修改文件 + +| 文件路径 | 修改内容 | +|---------|---------| +| `ruoyi-ccdi/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 +**审核状态:** 待审核 diff --git a/doc/scripts/generate_recruitment_test_data.py b/doc/scripts/generate_recruitment_test_data.py new file mode 100644 index 0000000..d0edcf2 --- /dev/null +++ b/doc/scripts/generate_recruitment_test_data.py @@ -0,0 +1,271 @@ +""" +招聘信息测试数据生成器 +生成符合校验规则的招聘信息测试数据并保存到Excel文件 +""" + +import random +import string +from datetime import datetime, timedelta +from openpyxl import Workbook +from openpyxl.styles import Font, Alignment, PatternFill + +# 数据配置 +RECRUIT_COUNT = 2000 # 生成数据条数 + +# 招聘项目名称列表 +RECRUIT_NAMES = [ + "2025春季校园招聘", "2025秋季校园招聘", "2025社会招聘", "2025技术专项招聘", + "2025管培生招聘", "2025实习生招聘", "2025高端人才引进", "2025春季研发岗招聘", + "2025夏季校园招聘", "2025冬季校园招聘", "2025春季销售岗招聘", "2025秋季市场岗招聘", + "2025春季运营岗招聘", "2025秋季产品岗招聘", "2025春季客服岗招聘", "2025秋季人事岗招聘" +] + +# 职位名称列表 +POSITION_NAMES = [ + "Java开发工程师", "Python开发工程师", "前端开发工程师", "后端开发工程师", + "全栈工程师", "算法工程师", "数据分析师", "产品经理", + "UI设计师", "测试工程师", "运维工程师", "架构师", + "软件工程师", "系统分析师", "数据库管理员", "网络工程师", + "移动端开发工程师", "嵌入式开发工程师", "大数据工程师", "人工智能工程师" +] + +# 职位类别 +POSITION_CATEGORIES = [ + "技术类", "产品类", "设计类", "运营类", + "市场类", "销售类", "客服类", "人事类", + "财务类", "行政类", "管理类", "研发类" +] + +# 职位描述模板 +POSITION_DESCS = [ + "负责公司核心业务系统的设计和开发,要求熟悉相关技术栈,具备良好的编码规范和团队协作能力。", + "参与产品需求分析和技术方案设计,负责模块开发和维护,优化系统性能,保障系统稳定性。", + "负责系统架构设计和技术选型,解决技术难题,指导团队成员开发,推动技术创新。", + "负责数据采集、清洗、分析和可视化,为业务决策提供数据支持,优化业务流程。", + "负责产品规划、需求分析和产品设计,协调研发、测试、运营等团队,推动产品落地。", + "负责用户界面设计和用户体验优化,与产品经理和开发团队协作,确保设计还原度。", + "负责系统测试和质量保障,编写测试用例,执行测试,跟踪缺陷,保障产品质量。", + "负责系统运维和监控,保障系统稳定运行,优化系统性能,处理故障和应急响应。" +] + +# 常见姓氏和名字 +SURNAMES = ["王", "李", "张", "刘", "陈", "杨", "黄", "赵", "周", "吴", "徐", "孙", "马", "朱", "胡", "郭", "何", "高", "林", "罗"] +GIVEN_NAMES = ["伟", "芳", "娜", "敏", "静", "丽", "强", "磊", "军", "洋", "勇", "艳", "杰", "娟", "涛", "明", "超", "秀英", "华", "英"] + +# 学历列表 +EDUCATIONS = ["本科", "硕士", "博士", "大专", "高中"] + +# 毕业院校列表 +UNIVERSITIES = [ + "清华大学", "北京大学", "复旦大学", "上海交通大学", "浙江大学", "中国科学技术大学", + "南京大学", "中山大学", "华中科技大学", "哈尔滨工业大学", "西安交通大学", "北京理工大学", + "中国人民大学", "北京航空航天大学", "同济大学", "南开大学", "天津大学", "东南大学", + "武汉大学", "厦门大学", "山东大学", "四川大学", "吉林大学", "中南大学", + "华南理工大学", "西北工业大学", "华东师范大学", "北京师范大学", "重庆大学" +] + +# 专业列表 +MAJORS = [ + "计算机科学与技术", "软件工程", "人工智能", "数据科学与大数据技术", "物联网工程", + "电子信息工程", "通信工程", "自动化", "电气工程及其自动化", "机械工程", + "材料科学与工程", "化学工程与工艺", "生物工程", "环境工程", "土木工程", + "数学与应用数学", "统计学", "物理学", "化学", "生物学", + "工商管理", "市场营销", "会计学", "金融学", "国际经济与贸易", + "人力资源管理", "公共事业管理", "行政管理", "法学", "汉语言文学", + "英语", "日语", "新闻传播学", "广告学", "艺术设计" +] + +# 录用状态 +ADMIT_STATUSES = ["录用", "未录用", "放弃"] + +# 面试官姓名和工号 +INTERVIEWERS = [ + ("张伟", "INT001"), ("李芳", "INT002"), ("王磊", "INT003"), ("刘娜", "INT004"), + ("陈军", "INT005"), ("杨静", "INT006"), ("黄勇", "INT007"), ("赵丽", "INT008"), + ("周涛", "INT009"), ("吴明", "INT010"), ("徐超", "INT011"), ("孙杰", "INT012"), + ("马娟", "INT013"), ("朱华", "INT014"), ("胡英", "INT015"), ("郭强", "INT016") +] + + +def generate_chinese_name(): + """生成中文姓名""" + surname = random.choice(SURNAMES) + # 50%概率双字名,50%概率单字名 + if random.random() > 0.5: + given_name = random.choice(GIVEN_NAMES) + random.choice(GIVEN_NAMES) + else: + given_name = random.choice(GIVEN_NAMES) + return surname + given_name + + +def generate_id_number(): + """生成18位身份证号码""" + # 地区码(前6位) + area_code = f"{random.randint(110000, 659001):06d}" + + # 出生日期(8位) - 生成1990-2005年的出生日期 + birth_year = random.randint(1990, 2005) + birth_month = f"{random.randint(1, 12):02d}" + birth_day = f"{random.randint(1, 28):02d}" + birth_date = f"{birth_year}{birth_month}{birth_day}" + + # 顺序码(3位) + sequence_code = f"{random.randint(1, 999):03d}" + + # 前17位 + id_17 = area_code + birth_date + sequence_code + + # 计算校验码(最后1位) + weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] + check_codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] + + total = sum(int(id_17[i]) * weights[i] for i in range(17)) + check_code = check_codes[total % 11] + + return id_17 + check_code + + +def generate_graduation_date(): + """生成毕业年月(YYYYMM格式)""" + # 生成2020-2030年之间的毕业年月 + year = random.randint(2020, 2030) + month = f"{random.randint(1, 12):02d}" + return f"{year}{month}" + + +def generate_recruitment_data(start_index): + """生成招聘测试数据""" + data = [] + + for i in range(start_index, start_index + RECRUIT_COUNT): + # 生成招聘项目编号 + recruit_id = f"REC{datetime.now().strftime('%Y%m%d')}{i:06d}" + + # 选择面试官(50%概率有两个面试官,50%概率只有一个) + if random.random() > 0.5: + interviewer1_name, interviewer1_id = random.choice(INTERVIEWERS) + interviewer2_name, interviewer2_id = random.choice(INTERVIEWERS) + else: + interviewer1_name, interviewer1_id = random.choice(INTERVIEWERS) + interviewer2_name = "" + interviewer2_id = "" + + row_data = [ + recruit_id, # 招聘项目编号 + random.choice(RECRUIT_NAMES), # 招聘项目名称 + random.choice(POSITION_NAMES), # 职位名称 + random.choice(POSITION_CATEGORIES), # 职位类别 + random.choice(POSITION_DESCS), # 职位描述 + generate_chinese_name(), # 应聘人员姓名 + random.choice(EDUCATIONS), # 应聘人员学历 + generate_id_number(), # 应聘人员证件号码 + random.choice(UNIVERSITIES), # 应聘人员毕业院校 + random.choice(MAJORS), # 应聘人员专业 + generate_graduation_date(), # 应聘人员毕业年月 + random.choice(ADMIT_STATUSES), # 录用情况 + interviewer1_name, # 面试官1姓名 + interviewer1_id, # 面试官1工号 + interviewer2_name, # 面试官2姓名 + interviewer2_id # 面试官2工号 + ] + + data.append(row_data) + + return data + + +def create_excel(data, filename): + """创建Excel文件""" + wb = Workbook() + ws = wb.active + ws.title = "招聘信息" + + # 表头 + headers = [ + "招聘项目编号", "招聘项目名称", "职位名称", "职位类别", "职位描述", + "应聘人员姓名", "应聘人员学历", "应聘人员证件号码", "应聘人员毕业院校", + "应聘人员专业", "应聘人员毕业年月", "录用情况", + "面试官1姓名", "面试官1工号", "面试官2姓名", "面试官2工号" + ] + + # 写入表头 + ws.append(headers) + + # 设置表头样式 + header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") + header_font = Font(bold=True, color="FFFFFF") + + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=1, column=col_num) + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + + # 写入数据 + for row_data in data: + ws.append(row_data) + + # 设置列宽 + column_widths = [20, 20, 20, 15, 30, 15, 15, 20, 20, 15, 15, 10, 15, 15, 15, 15] + for col_num, width in enumerate(column_widths, 1): + ws.column_dimensions[chr(64 + col_num)].width = width + + # 设置所有单元格居中对齐 + for row in ws.iter_rows(min_row=1, max_row=ws.max_row, min_col=1, max_col=ws.max_column): + for cell in row: + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + + # 保存文件 + wb.save(filename) + print(f"✓ 已生成文件: {filename}") + print(f" 数据行数: {len(data)}") + + +def main(): + """主函数""" + print("=" * 70) + print("招聘信息测试数据生成器") + print("=" * 70) + + # 检查是否安装了openpyxl + try: + import openpyxl + except ImportError: + print("✗ 未安装openpyxl库,正在安装...") + import subprocess + subprocess.check_call(["pip", "install", "openpyxl"]) + print("✓ openpyxl库安装成功") + + print(f"\n配置信息:") + print(f" - 生成数据量: {RECRUIT_COUNT} 条/文件") + print(f" - 生成文件数: 2 个") + print(f" - 总数据量: {RECRUIT_COUNT * 2} 条") + + print(f"\n开始生成数据...") + + # 生成第一个文件 + print(f"\n正在生成第1个文件...") + data1 = generate_recruitment_data(1) + filename1 = "doc/test-data/recruitment/recruitment_test_data_2000_1.xlsx" + create_excel(data1, filename1) + + # 生成第二个文件 + print(f"\n正在生成第2个文件...") + data2 = generate_recruitment_data(RECRUIT_COUNT + 1) + filename2 = "doc/test-data/recruitment/recruitment_test_data_2000_2.xlsx" + create_excel(data2, filename2) + + print("\n" + "=" * 70) + print("✓ 所有文件生成完成!") + print("=" * 70) + print(f"\n生成的文件:") + print(f" 1. {filename1}") + print(f" 2. {filename2}") + print(f"\n数据统计:") + print(f" - 总数据量: {RECRUIT_COUNT * 2} 条") + print(f" - 文件1: {len(data1)} 条") + print(f" - 文件2: {len(data2)} 条") + + +if __name__ == "__main__": + main() diff --git a/doc/test-data/recruitment/recruitment_1770357421032.xlsx b/doc/test-data/recruitment/recruitment_1770357421032.xlsx new file mode 100644 index 0000000..6f44e7b Binary files /dev/null and b/doc/test-data/recruitment/recruitment_1770357421032.xlsx differ diff --git a/doc/test-data/recruitment/recruitment_test_data_2000_1.xlsx b/doc/test-data/recruitment/recruitment_test_data_2000_1.xlsx new file mode 100644 index 0000000..ceef6c4 Binary files /dev/null and b/doc/test-data/recruitment/recruitment_test_data_2000_1.xlsx differ diff --git a/doc/test-data/recruitment/recruitment_test_data_2000_2.xlsx b/doc/test-data/recruitment/recruitment_test_data_2000_2.xlsx new file mode 100644 index 0000000..7cfa0c7 Binary files /dev/null and b/doc/test-data/recruitment/recruitment_test_data_2000_2.xlsx differ diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/RecruitmentImportFailureVO.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/RecruitmentImportFailureVO.java new file mode 100644 index 0000000..a1cb6a6 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/RecruitmentImportFailureVO.java @@ -0,0 +1,33 @@ +package com.ruoyi.ccdi.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 招聘信息导入失败记录VO + * + * @author ruoyi + * @date 2026-02-06 + */ +@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; +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiStaffRecruitmentImportService.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiStaffRecruitmentImportService.java new file mode 100644 index 0000000..ecb19a4 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiStaffRecruitmentImportService.java @@ -0,0 +1,44 @@ +package com.ruoyi.ccdi.service; + +import com.ruoyi.ccdi.domain.excel.CcdiStaffRecruitmentExcel; +import com.ruoyi.ccdi.domain.vo.ImportStatusVO; +import com.ruoyi.ccdi.domain.vo.RecruitmentImportFailureVO; + +import java.util.List; + +/** + * 招聘信息异步导入Service + * + * @author ruoyi + * @date 2026-02-06 + */ +public interface ICcdiStaffRecruitmentImportService { + + /** + * 异步导入招聘信息数据 + * + * @param excelList Excel数据列表 + * @param isUpdateSupport 是否更新已存在的数据 + * @param taskId 任务ID + */ + void importRecruitmentAsync(List excelList, + Boolean isUpdateSupport, + String taskId, + String userName); + + /** + * 查询导入状态 + * + * @param taskId 任务ID + * @return 导入状态信息 + */ + ImportStatusVO getImportStatus(String taskId); + + /** + * 获取导入失败记录 + * + * @param taskId 任务ID + * @return 失败记录列表 + */ + List getImportFailures(String taskId); +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentImportServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentImportServiceImpl.java new file mode 100644 index 0000000..734efb8 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentImportServiceImpl.java @@ -0,0 +1,269 @@ +package com.ruoyi.ccdi.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.ccdi.domain.CcdiStaffRecruitment; +import com.ruoyi.ccdi.domain.excel.CcdiStaffRecruitmentExcel; +import com.ruoyi.ccdi.domain.vo.ImportResult; +import com.ruoyi.ccdi.domain.vo.ImportStatusVO; +import com.ruoyi.ccdi.domain.vo.RecruitmentImportFailureVO; +import com.ruoyi.ccdi.enums.AdmitStatus; +import com.ruoyi.ccdi.mapper.CcdiStaffRecruitmentMapper; +import com.ruoyi.ccdi.service.ICcdiStaffRecruitmentImportService; +import com.ruoyi.common.utils.IdCardUtil; +import com.ruoyi.common.utils.StringUtils; +import jakarta.annotation.Resource; +import org.springframework.beans.BeanUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 招聘信息异步导入Service实现 + * + * @author ruoyi + * @date 2026-02-06 + */ +@Service +@EnableAsync +public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitmentImportService { + + @Resource + private CcdiStaffRecruitmentMapper recruitmentMapper; + + @Resource + private RedisTemplate redisTemplate; + + @Override + @Async + public void importRecruitmentAsync(List excelList, + Boolean isUpdateSupport, + String taskId, + String userName) { + long startTime = System.currentTimeMillis(); + + // 初始化Redis状态 + String statusKey = "import:recruitment:" + taskId; + Map statusData = new HashMap<>(); + statusData.put("taskId", taskId); + statusData.put("status", "PROCESSING"); + statusData.put("totalCount", excelList.size()); + statusData.put("successCount", 0); + statusData.put("failureCount", 0); + statusData.put("progress", 0); + statusData.put("startTime", startTime); + statusData.put("message", "正在处理..."); + + redisTemplate.opsForHash().putAll(statusKey, statusData); + redisTemplate.expire(statusKey, 7, TimeUnit.DAYS); + + List newRecords = new ArrayList<>(); + List updateRecords = new ArrayList<>(); + List failures = new ArrayList<>(); + + // 批量查询已存在的招聘项目编号 + Set existingRecruitIds = getExistingRecruitIds(excelList); + + // 分类数据 + for (CcdiStaffRecruitmentExcel excel : excelList) { + try { + // 验证数据 + validateRecruitmentData(excel, isUpdateSupport, existingRecruitIds); + + CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment(); + BeanUtils.copyProperties(excel, recruitment); + + if (existingRecruitIds.contains(excel.getRecruitId())) { + if (isUpdateSupport) { + recruitment.setUpdatedBy(userName); + updateRecords.add(recruitment); + } else { + throw new RuntimeException("该招聘项目编号已存在"); + } + } else { + recruitment.setCreatedBy(userName); + recruitment.setUpdatedBy(userName); + newRecords.add(recruitment); + } + + } catch (Exception e) { + RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO(); + BeanUtils.copyProperties(excel, failure); + failure.setErrorMessage(e.getMessage()); + failures.add(failure); + } + } + + // 批量插入新数据 + if (!newRecords.isEmpty()) { + recruitmentMapper.insertBatch(newRecords); + } + + // 批量更新已有数据 + if (!updateRecords.isEmpty() && isUpdateSupport) { + recruitmentMapper.updateBatch(updateRecords); + } + + // 保存失败记录到Redis + if (!failures.isEmpty()) { + String failuresKey = "import:recruitment:" + taskId + ":failures"; + redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS); + } + + // 更新最终状态 + 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); + } + + @Override + public ImportStatusVO getImportStatus(String taskId) { + String key = "import:recruitment:" + taskId; + Boolean hasKey = redisTemplate.hasKey(key); + + if (Boolean.FALSE.equals(hasKey)) { + throw new RuntimeException("任务不存在或已过期"); + } + + Map statusMap = redisTemplate.opsForHash().entries(key); + + ImportStatusVO statusVO = new ImportStatusVO(); + statusVO.setTaskId((String) statusMap.get("taskId")); + statusVO.setStatus((String) statusMap.get("status")); + statusVO.setTotalCount((Integer) statusMap.get("totalCount")); + statusVO.setSuccessCount((Integer) statusMap.get("successCount")); + statusVO.setFailureCount((Integer) statusMap.get("failureCount")); + statusVO.setProgress((Integer) statusMap.get("progress")); + statusVO.setStartTime((Long) statusMap.get("startTime")); + statusVO.setEndTime((Long) statusMap.get("endTime")); + statusVO.setMessage((String) statusMap.get("message")); + + return statusVO; + } + + @Override + public List getImportFailures(String taskId) { + String key = "import:recruitment:" + taskId + ":failures"; + Object failuresObj = redisTemplate.opsForValue().get(key); + + if (failuresObj == null) { + return Collections.emptyList(); + } + + return JSON.parseArray(JSON.toJSONString(failuresObj), RecruitmentImportFailureVO.class); + } + + /** + * 批量查询已存在的招聘项目编号 + */ + private Set getExistingRecruitIds(List excelList) { + List recruitIds = excelList.stream() + .map(CcdiStaffRecruitmentExcel::getRecruitId) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.toList()); + + if (recruitIds.isEmpty()) { + return Collections.emptySet(); + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(CcdiStaffRecruitment::getRecruitId, recruitIds); + List existingRecruitments = recruitmentMapper.selectList(wrapper); + + return existingRecruitments.stream() + .map(CcdiStaffRecruitment::getRecruitId) + .collect(Collectors.toSet()); + } + + /** + * 验证招聘信息数据 + */ + private void validateRecruitmentData(CcdiStaffRecruitmentExcel excel, + Boolean isUpdateSupport, + Set existingRecruitIds) { + // 验证必填字段 + if (StringUtils.isEmpty(excel.getRecruitId())) { + throw new RuntimeException("招聘项目编号不能为空"); + } + if (StringUtils.isEmpty(excel.getRecruitName())) { + throw new RuntimeException("招聘项目名称不能为空"); + } + if (StringUtils.isEmpty(excel.getPosName())) { + throw new RuntimeException("职位名称不能为空"); + } + if (StringUtils.isEmpty(excel.getPosCategory())) { + throw new RuntimeException("职位类别不能为空"); + } + if (StringUtils.isEmpty(excel.getPosDesc())) { + throw new RuntimeException("职位描述不能为空"); + } + if (StringUtils.isEmpty(excel.getCandName())) { + throw new RuntimeException("应聘人员姓名不能为空"); + } + if (StringUtils.isEmpty(excel.getCandEdu())) { + throw new RuntimeException("应聘人员学历不能为空"); + } + if (StringUtils.isEmpty(excel.getCandId())) { + throw new RuntimeException("证件号码不能为空"); + } + if (StringUtils.isEmpty(excel.getCandSchool())) { + throw new RuntimeException("应聘人员毕业院校不能为空"); + } + if (StringUtils.isEmpty(excel.getCandMajor())) { + throw new RuntimeException("应聘人员专业不能为空"); + } + if (StringUtils.isEmpty(excel.getCandGrad())) { + throw new RuntimeException("应聘人员毕业年月不能为空"); + } + if (StringUtils.isEmpty(excel.getAdmitStatus())) { + throw new RuntimeException("录用情况不能为空"); + } + + // 验证证件号码格式 + 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("录用情况只能填写'录用'、'未录用'或'放弃'"); + } + } + + /** + * 更新导入状态 + */ + private void updateImportStatus(String taskId, String status, ImportResult result) { + String key = "import:recruitment:" + taskId; + Map statusData = new HashMap<>(); + statusData.put("status", status); + statusData.put("totalCount", result.getTotalCount()); + 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); + } +}