# 员工信息异步导入功能实施计划 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **目标:** 实现员工信息的异步导入功能,将导入结果存储在Redis中,前端通过轮询查询导入状态,并提供失败记录查询功能。 **架构:** 使用Spring @Async实现异步导入,MyBatis Plus批量插入新数据,自定义SQL使用ON DUPLICATE KEY UPDATE批量更新已有数据,Redis存储导入状态和失败记录(TTL 7天),前端轮询查询状态并显示结果。 **技术栈:** Spring Boot 3.5.8, MyBatis Plus 3.5.10, Redis, Vue 2.6.12, Element UI --- ## Task 1: 配置异步支持 **目标:** 创建异步配置类,设置专用线程池处理导入任务 **文件:** - 创建: `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/config/AsyncConfig.java` **步骤 1: 创建AsyncConfig配置类** ```java package com.ruoyi.ccdi.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; /** * 异步配置类 * * @author ruoyi */ @Configuration @EnableAsync public class AsyncConfig { /** * 导入任务专用线程池 */ @Bean("importExecutor") public Executor importExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 核心线程数 executor.setCorePoolSize(2); // 最大线程数 executor.setMaxPoolSize(5); // 队列容量 executor.setQueueCapacity(100); // 线程名前缀 executor.setThreadNamePrefix("import-async-"); // 拒绝策略:由调用线程执行 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 等待所有任务完成后再关闭线程池 executor.setWaitForTasksToCompleteOnShutdown(true); // 等待时间 executor.setAwaitTerminationSeconds(60); executor.initialize(); return executor; } } ``` **步骤 2: 验证配置是否生效** 启动应用后,查看日志中是否有线程池创建信息。或者在后续任务中测试异步方法。 **步骤 3: 提交配置** ```bash git add ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/config/AsyncConfig.java git commit -m "feat: 添加异步配置类,配置导入任务专用线程池" ``` --- ## Task 2: 创建VO类 **目标:** 创建导入结果、状态和失败记录的VO类 **文件:** - 创建: `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/domain/vo/ImportResultVO.java` - 创建: `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/domain/vo/ImportStatusVO.java` - 创建: `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/domain/vo/ImportFailureVO.java` **步骤 1: 创建ImportResultVO** ```java package com.ruoyi.ccdi.domain.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; /** * 导入结果VO * * @author ruoyi */ @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: 创建ImportStatusVO** ```java package com.ruoyi.ccdi.domain.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; /** * 导入状态VO * * @author ruoyi */ @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; } ``` **步骤 3: 创建ImportFailureVO** ```java package com.ruoyi.ccdi.domain.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; /** * 导入失败记录VO * * @author ruoyi */ @Data @Schema(description = "导入失败记录") public class ImportFailureVO { @Schema(description = "柜员号") private Long employeeId; @Schema(description = "姓名") private String name; @Schema(description = "身份证号") private String idCard; @Schema(description = "部门ID") private Long deptId; @Schema(description = "电话") private String phone; @Schema(description = "状态") private String status; @Schema(description = "入职时间") private String hireDate; @Schema(description = "错误信息") private String errorMessage; } ``` **步骤 4: 提交VO类** ```bash git add ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/domain/vo/ git commit -m "feat: 添加导入相关VO类(ImportResultVO, ImportStatusVO, ImportFailureVO)" ``` --- ## Task 3: 添加数据库UNIQUE约束 **目标:** 确保employee_id字段有UNIQUE约束,支持ON DUPLICATE KEY UPDATE **文件:** - 修改: 数据库表 `ccdi_employee` **步骤 1: 检查现有约束** ```sql -- 使用MCP连接数据库查询 SHOW INDEX FROM ccdi_employee WHERE Key_name = 'uk_employee_id'; ``` **步骤 2: 添加UNIQUE约束(如果不存在)** ```sql -- 如果不存在uk_employee_id索引,则添加 ALTER TABLE ccdi_employee ADD UNIQUE KEY uk_employee_id (employee_id); ``` **步骤 3: 验证约束** ```sql -- 验证索引已创建 SHOW INDEX FROM ccdi_employee WHERE Key_name = 'uk_employee_id'; ``` **步骤 4: 记录SQL变更** 在项目 `sql` 目录下创建 `sql/update_ccdi_employee_20260206.sql`: ```sql -- 员工信息表添加柜员号唯一索引 -- 日期: 2026-02-06 -- 目的: 支持批量更新时使用ON DUPLICATE KEY UPDATE语法 ALTER TABLE ccdi_employee ADD UNIQUE KEY uk_employee_id (employee_id) COMMENT '柜员号唯一索引'; ``` **步骤 5: 提交SQL变更** ```bash git add sql/update_ccdi_employee_20260206.sql git commit -m "feat: 添加员工表柜员号唯一索引,支持批量更新" ``` --- ## Task 4: 添加Mapper方法 **目标:** 在Mapper接口和XML中添加批量查询和批量插入更新的方法 **文件:** - 修改: `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java` - 修改: `ruoyi-info-collection/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml` **步骤 1: 在Mapper接口中添加方法** 在 `CcdiEmployeeMapper.java` 中添加: ```java /** * 批量插入或更新员工信息 * 使用ON DUPLICATE KEY UPDATE语法 * * @param list 员工信息列表 * @return 影响行数 */ int insertOrUpdateBatch(@Param("list") List list); ``` **步骤 2: 在Mapper.xml中添加SQL** 在 `CcdiEmployeeMapper.xml` 中添加: ```xml INSERT INTO ccdi_employee (employee_id, name, dept_id, id_card, phone, hire_date, status, create_time, create_by, update_by, update_time, remark) VALUES (#{item.employeeId}, #{item.name}, #{item.deptId}, #{item.idCard}, #{item.phone}, #{item.hireDate}, #{item.status}, NOW(), #{item.createBy}, #{item.updateBy}, NOW(), #{item.remark}) ON DUPLICATE KEY UPDATE name = VALUES(name), dept_id = VALUES(dept_id), phone = VALUES(phone), hire_date = VALUES(hire_date), status = VALUES(status), update_by = VALUES(update_by), update_time = NOW(), remark = VALUES(remark) ``` **步骤 3: 提交Mapper变更** ```bash git add ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java git add ruoyi-info-collection/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml git commit -m "feat: 添加批量插入或更新员工信息方法" ``` --- ## Task 5: 实现Service层异步导入方法 **目标:** 实现异步导入逻辑,包括数据分类、批量操作、Redis存储 **文件:** - 修改: `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/ICcdiEmployeeService.java` - 修改: `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeServiceImpl.java` **步骤 1: 在Service接口中添加方法声明** 在 `ICcdiEmployeeService.java` 中添加: ```java /** * 异步导入员工数据 * * @param excelList Excel数据列表 * @param isUpdateSupport 是否更新已存在的数据 * @return CompletableFuture包含导入结果 */ CompletableFuture importEmployeeAsync(List excelList, Boolean isUpdateSupport); /** * 查询导入状态 * * @param taskId 任务ID * @return 导入状态信息 */ ImportStatusVO getImportStatus(String taskId); /** * 获取导入失败记录 * * @param taskId 任务ID * @return 失败记录列表 */ List getImportFailures(String taskId); ``` **步骤 2: 在ServiceImpl中实现异步方法** 在 `CcdiEmployeeServiceImpl.java` 中添加依赖注入: ```java @Resource private RedisTemplate redisTemplate; @Resource(name = "importExecutor") private Executor importExecutor; ``` **步骤 3: 实现异步导入核心逻辑** 在 `CcdiEmployeeServiceImpl.java` 中添加: ```java /** * 异步导入员工数据 */ @Override @Async("importExecutor") public CompletableFuture importEmployeeAsync(List excelList, Boolean isUpdateSupport) { // 生成任务ID String taskId = UUID.randomUUID().toString(); long startTime = System.currentTimeMillis(); // 初始化Redis状态 String statusKey = "import:employee:" + 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); try { // 执行导入逻辑 ImportResult result = doImport(excelList, isUpdateSupport, taskId); // 更新最终状态 String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS"; updateImportStatus(taskId, finalStatus, result, startTime); // 返回结果 ImportResultVO resultVO = new ImportResultVO(); resultVO.setTaskId(taskId); resultVO.setStatus(finalStatus); resultVO.setMessage("导入任务已提交"); return CompletableFuture.completedFuture(resultVO); } catch (Exception e) { // 处理异常 Map errorData = new HashMap<>(); errorData.put("status", "FAILED"); errorData.put("message", "导入失败: " + e.getMessage()); errorData.put("endTime", System.currentTimeMillis()); redisTemplate.opsForHash().putAll(statusKey, errorData); ImportResultVO resultVO = new ImportResultVO(); resultVO.setTaskId(taskId); resultVO.setStatus("FAILED"); resultVO.setMessage("导入失败: " + e.getMessage()); return CompletableFuture.completedFuture(resultVO); } } /** * 执行导入逻辑 */ private ImportResult doImport(List excelList, Boolean isUpdateSupport, String taskId) { List newRecords = new ArrayList<>(); List updateRecords = new ArrayList<>(); List failures = new ArrayList<>(); // 批量查询已存在的柜员号 Set existingIds = getExistingEmployeeIds(excelList); // 分类数据 for (int i = 0; i < excelList.size(); i++) { CcdiEmployeeExcel excel = excelList.get(i); try { // 验证数据 validateEmployeeData(excel, existingIds); CcdiEmployee employee = convertToEntity(excel); if (existingIds.contains(excel.getEmployeeId())) { if (isUpdateSupport) { updateRecords.add(employee); } else { throw new RuntimeException("柜员号已存在且未启用更新支持"); } } else { newRecords.add(employee); } // 更新进度 int progress = (int) ((i + 1) * 100.0 / excelList.size()); updateImportProgress(taskId, progress); } catch (Exception e) { ImportFailureVO failure = new ImportFailureVO(); BeanUtils.copyProperties(excel, failure); failure.setErrorMessage(e.getMessage()); failures.add(failure); } } // 批量插入新数据 if (!newRecords.isEmpty()) { saveBatch(newRecords, 500); } // 批量更新已有数据 if (!updateRecords.isEmpty() && isUpdateSupport) { employeeMapper.insertOrUpdateBatch(updateRecords); } // 保存失败记录到Redis if (!failures.isEmpty()) { String failuresKey = "import:employee:" + 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()); return result; } /** * 获取已存在的员工ID集合 */ private Set getExistingEmployeeIds(List excelList) { List employeeIds = excelList.stream() .map(CcdiEmployeeExcel::getEmployeeId) .filter(Objects::nonNull) .collect(Collectors.toList()); if (employeeIds.isEmpty()) { return Collections.emptySet(); } List existingEmployees = employeeMapper.selectBatchIds(employeeIds); return existingEmployees.stream() .map(CcdiEmployee::getEmployeeId) .collect(Collectors.toSet()); } /** * 转换Excel对象为Entity */ private CcdiEmployee convertToEntity(CcdiEmployeeExcel excel) { CcdiEmployee employee = new CcdiEmployee(); BeanUtils.copyProperties(excel, employee); return employee; } /** * 更新导入进度 */ private void updateImportProgress(String taskId, Integer progress) { String key = "import:employee:" + taskId; redisTemplate.opsForHash().put(key, "progress", progress); } /** * 更新导入状态 */ private void updateImportStatus(String taskId, String status, ImportResult result, Long startTime) { String key = "import:employee:" + 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); } /** * 导入结果内部类 */ @Data private static class ImportResult { private Integer totalCount; private Integer successCount; private Integer failureCount; } ``` **步骤 4: 实现查询导入状态方法** ```java @Override public ImportStatusVO getImportStatus(String taskId) { String key = "import:employee:" + 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; } ``` **步骤 5: 实现查询失败记录方法** ```java @Override public List getImportFailures(String taskId) { String key = "import:employee:" + taskId + ":failures"; Object failuresObj = redisTemplate.opsForValue().get(key); if (failuresObj == null) { return Collections.emptyList(); } // 使用JSON转换 return JSON.parseArray(JSON.toJSONString(failuresObj), ImportFailureVO.class); } ``` **步骤 6: 提交Service层代码** ```bash git add ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/ git commit -m "feat: 实现员工信息异步导入服务" ``` --- ## Task 6: 修改Controller层接口 **目标:** 修改导入接口为异步,添加状态查询和失败记录查询接口 **文件:** - 修改: `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/controller/CcdiEmployeeController.java` **步骤 1: 添加Resource注入** 在Controller中添加: ```java @Resource private RedisTemplate redisTemplate; ``` **步骤 2: 修改导入接口** 将现有的 `importData` 方法改为: ```java /** * 导入员工信息(异步) */ @Operation(summary = "导入员工信息") @PreAuthorize("@ss.hasPermi('ccdi:employee:import')") @Log(title = "员工信息", businessType = BusinessType.IMPORT) @PostMapping("/importData") public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception { List list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiEmployeeExcel.class); if (list == null || list.isEmpty()) { return error("至少需要一条数据"); } // 异步导入 CompletableFuture future = employeeService.importEmployeeAsync(list, updateSupport); // 立即返回taskId ImportResultVO result = future.get(); return success("导入任务已提交,正在后台处理", result); } ``` **步骤 3: 添加状态查询接口** ```java /** * 查询导入状态 */ @Operation(summary = "查询员工导入状态") @GetMapping("/importStatus/{taskId}") public AjaxResult getImportStatus(@PathVariable String taskId) { try { ImportStatusVO status = employeeService.getImportStatus(taskId); return success(status); } catch (Exception e) { return error(e.getMessage()); } } ``` **步骤 4: 添加失败记录查询接口** ```java /** * 查询导入失败记录 */ @Operation(summary = "查询导入失败记录") @GetMapping("/importFailures/{taskId}") public TableDataInfo getImportFailures( @PathVariable String taskId, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { List failures = employeeService.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()); } ``` **步骤 5: 提交Controller变更** ```bash git add ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/controller/CcdiEmployeeController.java git commit -m "feat: 修改导入接口为异步,添加状态和失败记录查询接口" ``` --- ## Task 7: 前端API定义 **目标:** 在前端添加导入状态查询和失败记录查询的API方法 **文件:** - 修改: `ruoyi-ui/src/api/ccdiEmployee.js` **步骤 1: 添加API方法** 在 `ccdiEmployee.js` 中添加: ```javascript // 查询导入状态 export function getImportStatus(taskId) { return request({ url: '/ccdi/employee/importStatus/' + taskId, method: 'get' }) } // 查询导入失败记录 export function getImportFailures(taskId, pageNum, pageSize) { return request({ url: '/ccdi/employee/importFailures/' + taskId, method: 'get', params: { pageNum, pageSize } }) } ``` **步骤 2: 提交前端API变更** ```bash git add ruoyi-ui/src/api/ccdiEmployee.js git commit -m "feat: 添加导入状态和失败记录查询API" ``` --- ## Task 8: 前端导入流程优化 **目标:** 修改导入成功处理逻辑,实现后台处理+完成通知 **文件:** - 修改: `ruoyi-ui/src/views/ccdiEmployee/index.vue` **步骤 1: 添加data属性** 在 `data()` 中添加: ```javascript data() { return { // ...现有data pollingTimer: null, showFailureButton: false, currentTaskId: null, failureDialogVisible: false, failureList: [], failureLoading: false, failureTotal: 0, failureQueryParams: { pageNum: 1, pageSize: 10 } } } ``` **步骤 2: 修改handleFileSuccess方法** 将现有的 `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); } } ``` **步骤 3: 添加轮询方法** ```javascript methods: { // ...现有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: 添加生命周期销毁钩子** ```javascript beforeDestroy() { // 组件销毁时清除定时器 if (this.pollingTimer) { clearInterval(this.pollingTimer); this.pollingTimer = null; } } ``` **步骤 5: 提交前端轮询逻辑** ```bash git add ruoyi-ui/src/views/ccdiEmployee/index.vue git commit -m "feat: 实现导入状态轮询和完成通知" ``` --- ## Task 9: 添加失败记录查询UI **目标:** 在页面添加查看失败记录按钮和对话框 **文件:** - 修改: `ruoyi-ui/src/views/ccdiEmployee/index.vue` **步骤 1: 在搜索表单区域添加按钮** 在 `` 中添加: ```vue 查看导入失败记录 ``` **步骤 2: 添加失败记录对话框** 在模板最后、`` 标签前添加: ```vue ``` **步骤 3: 添加方法** ```javascript methods: { // ...现有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); }); } } ``` **步骤 4: 添加样式(可选)** ```vue ``` **步骤 5: 提交失败记录UI** ```bash git add ruoyi-ui/src/views/ccdiEmployee/index.vue git commit -m "feat: 添加导入失败记录查询对话框" ``` --- ## Task 10: 生成API文档 **目标:** 更新Swagger文档,记录新增的接口 **文件:** - 修改: `doc/api/ccdi-employee-api.md` **步骤 1: 添加接口文档** ```markdown ## 员工信息导入相关接口 ### 1. 导入员工信息(异步) **接口地址:** `POST /ccdi/employee/importData` **权限标识:** `ccdi:employee:import` **请求参数:** | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | file | File | 是 | Excel文件 | | updateSupport | boolean | 否 | 是否更新已存在的数据,默认false | **响应示例:** \`\`\`json { "code": 200, "msg": "导入任务已提交,正在后台处理", "data": { "taskId": "uuid-string", "status": "PROCESSING", "message": "任务已创建" } } \`\`\` ### 2. 查询导入状态 **接口地址:** `GET /ccdi/employee/importStatus/{taskId}` **权限标识:** 无 **路径参数:** | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | taskId | String | 是 | 任务ID | **响应示例:** \`\`\`json { "code": 200, "data": { "taskId": "uuid-string", "status": "SUCCESS", "totalCount": 100, "successCount": 95, "failureCount": 5, "progress": 100, "startTime": 1707225600000, "endTime": 1707225900000, "message": "导入完成" } } \`\`\` ### 3. 查询导入失败记录 **接口地址:** `GET /ccdi/employee/importFailures/{taskId}` **权限标识:** 无 **路径参数:** | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | taskId | String | 是 | 任务ID | **查询参数:** | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | pageNum | Integer | 否 | 页码,默认1 | | pageSize | Integer | 否 | 每页条数,默认10 | **响应示例:** \`\`\`json { "code": 200, "rows": [ { "employeeId": "1234567", "name": "张三", "idCard": "110101199001011234", "deptId": 100, "phone": "13800138000", "status": "0", "hireDate": "2020-01-01", "errorMessage": "身份证号格式错误" } ], "total": 5 } \`\`\` ``` **步骤 2: 提交文档** ```bash git add doc/api/ccdi-employee-api.md git commit -m "docs: 更新员工导入相关接口文档" ``` --- ## Task 11: 编写测试脚本 **目标:** 生成完整的测试脚本,验证导入功能 **文件:** - 创建: `test/test_employee_import.py` **步骤 1: 创建测试脚本** 完整的测试脚本将在任务实施过程中生成,包括: - 登录获取token - 测试正常导入 - 测试导入状态查询 - 测试失败记录查询 - 测试重复导入(验证批量更新) - 生成测试报告 **步骤 2: 提交测试脚本** ```bash git add test/test_employee_import.py git commit -m "test: 添加员工导入功能测试脚本" ``` --- ## Task 12: 最终集成测试 **目标:** 完整功能测试,确保前后端联调正常 **步骤 1: 启动后端服务** ```bash cd ruoyi-admin mvn spring-boot:run ``` **步骤 2: 启动前端服务** ```bash cd ruoyi-ui npm run dev ``` **步骤 3: 手动测试流程** 1. 访问 `http://localhost` 登录系统 2. 进入员工信息管理页面 3. 点击"导入"按钮 4. 下载模板并填写测试数据 5. 上传文件,观察是否显示"导入任务已提交"通知 6. 等待2-5秒,观察是否显示"导入完成"通知 7. 如果有失败数据,验证"查看导入失败记录"按钮是否显示 8. 点击按钮查看失败记录对话框 9. 验证失败记录是否正确显示 **步骤 4: 性能测试** 使用包含100、1000、10000条数据的Excel文件进行测试,验证: - 导入响应时间 < 500ms(立即返回taskId) - 1000条数据处理时间 < 10秒 - 10000条数据处理时间 < 60秒 **步骤 5: Redis验证** 使用Redis客户端查看: ```bash # 连接Redis redis-cli # 查看所有导入任务 KEYS import:employee:* # 查看某个任务状态 HGETALL import:employee:{taskId} # 查看失败记录 LRANGE import:employee:{taskId}:failures 0 -1 # 验证TTL TTL import:employee:{taskId} ``` **步骤 6: 记录测试结果** 在 `doc/test/employee-import-test-report.md` 中记录测试结果: ```markdown # 员工导入功能测试报告 **测试日期:** 2026-02-06 **测试人员:** ## 测试环境 - 后端版本: - 前端版本: - Redis版本: - 浏览器版本: ## 功能测试 ### 1. 正常导入测试 - [ ] 100条数据导入成功 - [ ] 1000条数据导入成功 - [ ] 状态查询正常 - [ ] 轮询通知正常 ### 2. 失败数据处理测试 - [ ] 部分数据失败时正确显示失败记录 - [ ] 失误原因准确 - [ ] 失败记录可以正常查询 ### 3. 批量更新测试 - [ ] 启用更新支持时,已有数据正确更新 - [ ] 未启用更新支持时,已有数据跳过 ### 4. 并发测试 - [ ] 同时导入多个文件互不影响 - [ ] 任务ID唯一 - [ ] 各自状态独立 ## 性能测试 | 数据量 | 响应时间 | 处理时间 | 结果 | |--------|----------|----------|------| | 100条 | | | | | 1000条 | | | | | 10000条 | | | | ## 问题记录 记录测试中发现的问题和解决方案。 ## 测试结论 - [ ] 通过 - [ ] 不通过 ``` **步骤 7: 提交测试报告** ```bash git add doc/test/employee-import-test-report.md git commit -m "test: 添加员工导入功能测试报告" ``` --- ## Task 13: 代码审查和优化 **目标:** 代码review,优化性能和代码质量 **检查项:** - [ ] 代码符合项目编码规范 - [ ] 异常处理完善 - [ ] 日志记录充分 - [ ] Redis操作性能优化(考虑使用Pipeline) - [ ] 批量操作批次大小合理 - [ ] 前端轮询间隔合理 - [ ] 内存使用合理,无内存泄漏风险 - [ ] 事务边界清晰 - [ ] 并发安全性 **步骤 1: 代码审查** 使用IDE的代码检查工具或人工review代码。 **步骤 2: 性能优化** 根据测试结果进行针对性优化: - 调整批量操作批次大小 - 优化Redis操作 - 调整线程池参数 **步骤 3: 提交优化代码** ```bash git add . git commit -m "refactor: 代码审查和性能优化" ``` --- ## Task 14: 文档完善 **目标:** 完善用户手册和开发文档 **文件:** - 修改: `README.md` - 创建: `doc/user-guide/employee-import-guide.md` **步骤 1: 更新README** 在项目README中添加导入功能说明。 **步骤 2: 编写用户指南** ```markdown # 员工信息导入用户指南 ## 导入流程 1. 准备Excel文件 2. 登录系统 3. 进入员工信息管理 4. 点击"导入"按钮 5. 选择文件并上传 6. 等待后台处理完成 7. 查看导入结果 8. 如有失败,查看并修正失败记录 ## 常见问题 ### Q1: 导入后多久能看到结果? A: 通常2-10秒,具体取决于数据量。 ### Q2: 失败记录会保留多久? A: 7天,过期自动删除。 ### Q3: 如何修正失败的数据? A: 点击"查看导入失败记录",修正后重新导入。 ``` **步骤 3: 提交文档** ```bash git add README.md doc/user-guide/employee-import-guide.md git commit -m "docs: 完善员工导入功能文档" ``` --- ## Task 15: 最终提交和发布 **目标:** 合并代码,发布新版本 **步骤 1: 查看所有变更** ```bash git status git log --oneline -10 ``` **步骤 2: 创建合并请求** 如果使用Git Flow,创建feature分支合并请求。 **步骤 3: 代码最终review** 进行最后一次代码review。 **步骤 4: 合并到主分支** ```bash git checkout dev git merge feature/employee-async-import git push origin dev ``` **步骤 5: 打Tag(可选)** ```bash git tag -a v1.x.x -m "员工信息异步导入功能" git push origin v1.x.x ``` **步骤 6: 部署到测试环境** 按照部署流程部署到测试环境。 **步骤 7: 用户验收测试** 邀请用户进行验收测试。 **步骤 8: 部署到生产环境** 验收通过后,部署到生产环境。 --- ## 附录 ### A. 相关文件清单 **后端:** - `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/config/AsyncConfig.java` - `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/domain/vo/ImportResultVO.java` - `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/domain/vo/ImportStatusVO.java` - `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/domain/vo/ImportFailureVO.java` - `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/ICcdiEmployeeService.java` - `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeServiceImpl.java` - `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java` - `ruoyi-info-collection/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml` - `ruoyi-info-collection/src/main/java/com/ruoyi/ccdi/controller/CcdiEmployeeController.java` **前端:** - `ruoyi-ui/src/api/ccdiEmployee.js` - `ruoyi-ui/src/views/ccdiEmployee/index.vue` **数据库:** - `sql/update_ccdi_employee_20260206.sql` ### B. Redis Key命名规范 ``` import:employee:{taskId} # 导入状态 import:employee:{taskId}:failures # 失败记录列表 ``` ### C. 状态枚举 | 状态值 | 说明 | 前端行为 | |--------|------|----------| | PROCESSING | 处理中 | 继续轮询 | | SUCCESS | 全部成功 | 显示成功通知,刷新列表 | | PARTIAL_SUCCESS | 部分成功 | 显示警告通知,显示失败按钮 | | FAILED | 全部失败 | 显示错误通知,显示失败按钮 | --- **计划版本:** 1.0 **创建日期:** 2026-02-06 **预计总工时:** 8-12小时