完成功能: - 新增异步导入方法 importEmployeeAsync,使用@Async注解实现异步处理 - 新增查询导入状态方法 getImportStatus - 新增查询导入失败记录方法 getImportFailures - 实现完整的导入逻辑,包括数据分类、批量操作、进度跟踪 - 使用Redis存储导入状态和失败记录,TTL设置为7天 - 支持增量更新模式,批量插入新数据,批量更新已有数据 - 实时更新导入进度到Redis 技术要点: - 使用RedisTemplate操作Redis,Hash结构存储状态 - 使用importExecutor线程池异步执行导入任务 - 使用UUID生成唯一任务ID - 使用CompletableFuture包装返回结果 - 批量操作提高性能(saveBatch每500条一批) - 失败记录只保存到Redis,不保存成功记录 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
35 KiB
员工信息异步导入功能实施计划
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-ccdi/src/main/java/com/ruoyi/ccdi/config/AsyncConfig.java
步骤 1: 创建AsyncConfig配置类
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: 提交配置
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/config/AsyncConfig.java
git commit -m "feat: 添加异步配置类,配置导入任务专用线程池"
Task 2: 创建VO类
目标: 创建导入结果、状态和失败记录的VO类
文件:
- 创建:
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/ImportResultVO.java - 创建:
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/ImportStatusVO.java - 创建:
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/ImportFailureVO.java
步骤 1: 创建ImportResultVO
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
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
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类
git add ruoyi-ccdi/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: 检查现有约束
-- 使用MCP连接数据库查询
SHOW INDEX FROM ccdi_employee WHERE Key_name = 'uk_employee_id';
步骤 2: 添加UNIQUE约束(如果不存在)
-- 如果不存在uk_employee_id索引,则添加
ALTER TABLE ccdi_employee
ADD UNIQUE KEY uk_employee_id (employee_id);
步骤 3: 验证约束
-- 验证索引已创建
SHOW INDEX FROM ccdi_employee WHERE Key_name = 'uk_employee_id';
步骤 4: 记录SQL变更
在项目 sql 目录下创建 sql/update_ccdi_employee_20260206.sql:
-- 员工信息表添加柜员号唯一索引
-- 日期: 2026-02-06
-- 目的: 支持批量更新时使用ON DUPLICATE KEY UPDATE语法
ALTER TABLE ccdi_employee
ADD UNIQUE KEY uk_employee_id (employee_id)
COMMENT '柜员号唯一索引';
步骤 5: 提交SQL变更
git add sql/update_ccdi_employee_20260206.sql
git commit -m "feat: 添加员工表柜员号唯一索引,支持批量更新"
Task 4: 添加Mapper方法
目标: 在Mapper接口和XML中添加批量查询和批量插入更新的方法
文件:
- 修改:
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java - 修改:
ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml
步骤 1: 在Mapper接口中添加方法
在 CcdiEmployeeMapper.java 中添加:
/**
* 批量插入或更新员工信息
* 使用ON DUPLICATE KEY UPDATE语法
*
* @param list 员工信息列表
* @return 影响行数
*/
int insertOrUpdateBatch(@Param("list") List<CcdiEmployee> list);
步骤 2: 在Mapper.xml中添加SQL
在 CcdiEmployeeMapper.xml 中添加:
<!-- 批量插入或更新员工信息 -->
<insert id="insertOrUpdateBatch" parameterType="java.util.List">
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
<foreach collection="list" item="item" separator=",">
(#{item.employeeId}, #{item.name}, #{item.deptId}, #{item.idCard},
#{item.phone}, #{item.hireDate}, #{item.status}, NOW(),
#{item.createBy}, #{item.updateBy}, NOW(), #{item.remark})
</foreach>
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)
</insert>
步骤 3: 提交Mapper变更
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java
git add ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml
git commit -m "feat: 添加批量插入或更新员工信息方法"
Task 5: 实现Service层异步导入方法
目标: 实现异步导入逻辑,包括数据分类、批量操作、Redis存储
文件:
- 修改:
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiEmployeeService.java - 修改:
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeServiceImpl.java
步骤 1: 在Service接口中添加方法声明
在 ICcdiEmployeeService.java 中添加:
/**
* 异步导入员工数据
*
* @param excelList Excel数据列表
* @param isUpdateSupport 是否更新已存在的数据
* @return CompletableFuture包含导入结果
*/
CompletableFuture<ImportResultVO> importEmployeeAsync(List<CcdiEmployeeExcel> excelList, Boolean isUpdateSupport);
/**
* 查询导入状态
*
* @param taskId 任务ID
* @return 导入状态信息
*/
ImportStatusVO getImportStatus(String taskId);
/**
* 获取导入失败记录
*
* @param taskId 任务ID
* @return 失败记录列表
*/
List<ImportFailureVO> getImportFailures(String taskId);
步骤 2: 在ServiceImpl中实现异步方法
在 CcdiEmployeeServiceImpl.java 中添加依赖注入:
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource(name = "importExecutor")
private Executor importExecutor;
步骤 3: 实现异步导入核心逻辑
在 CcdiEmployeeServiceImpl.java 中添加:
/**
* 异步导入员工数据
*/
@Override
@Async("importExecutor")
public CompletableFuture<ImportResultVO> importEmployeeAsync(List<CcdiEmployeeExcel> excelList, Boolean isUpdateSupport) {
// 生成任务ID
String taskId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
// 初始化Redis状态
String statusKey = "import:employee:" + taskId;
Map<String, Object> 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<String, Object> 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<CcdiEmployeeExcel> excelList, Boolean isUpdateSupport, String taskId) {
List<CcdiEmployee> newRecords = new ArrayList<>();
List<CcdiEmployee> updateRecords = new ArrayList<>();
List<ImportFailureVO> failures = new ArrayList<>();
// 批量查询已存在的柜员号
Set<Long> 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<Long> getExistingEmployeeIds(List<CcdiEmployeeExcel> excelList) {
List<Long> employeeIds = excelList.stream()
.map(CcdiEmployeeExcel::getEmployeeId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (employeeIds.isEmpty()) {
return Collections.emptySet();
}
List<CcdiEmployee> 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<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);
}
/**
* 导入结果内部类
*/
@Data
private static class ImportResult {
private Integer totalCount;
private Integer successCount;
private Integer failureCount;
}
步骤 4: 实现查询导入状态方法
@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<Object, Object> 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: 实现查询失败记录方法
@Override
public List<ImportFailureVO> 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层代码
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/
git commit -m "feat: 实现员工信息异步导入服务"
Task 6: 修改Controller层接口
目标: 修改导入接口为异步,添加状态查询和失败记录查询接口
文件:
- 修改:
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiEmployeeController.java
步骤 1: 添加Resource注入
在Controller中添加:
@Resource
private RedisTemplate<String, Object> redisTemplate;
步骤 2: 修改导入接口
将现有的 importData 方法改为:
/**
* 导入员工信息(异步)
*/
@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<CcdiEmployeeExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiEmployeeExcel.class);
if (list == null || list.isEmpty()) {
return error("至少需要一条数据");
}
// 异步导入
CompletableFuture<ImportResultVO> future = employeeService.importEmployeeAsync(list, updateSupport);
// 立即返回taskId
ImportResultVO result = future.get();
return success("导入任务已提交,正在后台处理", result);
}
步骤 3: 添加状态查询接口
/**
* 查询导入状态
*/
@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: 添加失败记录查询接口
/**
* 查询导入失败记录
*/
@Operation(summary = "查询导入失败记录")
@GetMapping("/importFailures/{taskId}")
public TableDataInfo getImportFailures(
@PathVariable String taskId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
List<ImportFailureVO> failures = employeeService.getImportFailures(taskId);
// 手动分页
int fromIndex = (pageNum - 1) * pageSize;
int toIndex = Math.min(fromIndex + pageSize, failures.size());
List<ImportFailureVO> pageData = failures.subList(fromIndex, toIndex);
return getDataTable(pageData, failures.size());
}
步骤 5: 提交Controller变更
git add ruoyi-ccdi/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 中添加:
// 查询导入状态
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变更
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() 中添加:
data() {
return {
// ...现有data
pollingTimer: null,
showFailureButton: false,
currentTaskId: null,
failureDialogVisible: false,
failureList: [],
failureLoading: false,
failureTotal: 0,
failureQueryParams: {
pageNum: 1,
pageSize: 10
}
}
}
步骤 2: 修改handleFileSuccess方法
将现有的 handleFileSuccess 方法替换为:
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: 添加轮询方法
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: 添加生命周期销毁钩子
beforeDestroy() {
// 组件销毁时清除定时器
if (this.pollingTimer) {
clearInterval(this.pollingTimer);
this.pollingTimer = null;
}
}
步骤 5: 提交前端轮询逻辑
git add ruoyi-ui/src/views/ccdiEmployee/index.vue
git commit -m "feat: 实现导入状态轮询和完成通知"
Task 9: 添加失败记录查询UI
目标: 在页面添加查看失败记录按钮和对话框
文件:
- 修改:
ruoyi-ui/src/views/ccdiEmployee/index.vue
步骤 1: 在搜索表单区域添加按钮
在 <el-row :gutter="10" class="mb8"> 中添加:
<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>
步骤 2: 添加失败记录对话框
在模板最后、</template> 标签前添加:
<!-- 导入失败记录对话框 -->
<el-dialog
title="导入失败记录"
:visible.sync="failureDialogVisible"
width="1200px"
append-to-body
>
<el-table :data="failureList" v-loading="failureLoading">
<el-table-column label="姓名" prop="name" align="center" />
<el-table-column label="柜员号" prop="employeeId" align="center" />
<el-table-column label="身份证号" prop="idCard" align="center" />
<el-table-column label="电话" prop="phone" 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>
步骤 3: 添加方法
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: 添加样式(可选)
<style scoped>
/* ...现有样式 */
.failure-dialog .el-table {
margin-top: 10px;
}
</style>
步骤 5: 提交失败记录UI
git add ruoyi-ui/src/views/ccdiEmployee/index.vue
git commit -m "feat: 添加导入失败记录查询对话框"
Task 10: 生成API文档
目标: 更新Swagger文档,记录新增的接口
文件:
- 修改:
doc/api/ccdi-employee-api.md
步骤 1: 添加接口文档
## 员工信息导入相关接口
### 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: 提交文档
git add doc/api/ccdi-employee-api.md
git commit -m "docs: 更新员工导入相关接口文档"
Task 11: 编写测试脚本
目标: 生成完整的测试脚本,验证导入功能
文件:
- 创建:
test/test_employee_import.py
步骤 1: 创建测试脚本
完整的测试脚本将在任务实施过程中生成,包括:
- 登录获取token
- 测试正常导入
- 测试导入状态查询
- 测试失败记录查询
- 测试重复导入(验证批量更新)
- 生成测试报告
步骤 2: 提交测试脚本
git add test/test_employee_import.py
git commit -m "test: 添加员工导入功能测试脚本"
Task 12: 最终集成测试
目标: 完整功能测试,确保前后端联调正常
步骤 1: 启动后端服务
cd ruoyi-admin
mvn spring-boot:run
步骤 2: 启动前端服务
cd ruoyi-ui
npm run dev
步骤 3: 手动测试流程
- 访问
http://localhost登录系统 - 进入员工信息管理页面
- 点击"导入"按钮
- 下载模板并填写测试数据
- 上传文件,观察是否显示"导入任务已提交"通知
- 等待2-5秒,观察是否显示"导入完成"通知
- 如果有失败数据,验证"查看导入失败记录"按钮是否显示
- 点击按钮查看失败记录对话框
- 验证失败记录是否正确显示
步骤 4: 性能测试
使用包含100、1000、10000条数据的Excel文件进行测试,验证:
- 导入响应时间 < 500ms(立即返回taskId)
- 1000条数据处理时间 < 10秒
- 10000条数据处理时间 < 60秒
步骤 5: Redis验证
使用Redis客户端查看:
# 连接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 中记录测试结果:
# 员工导入功能测试报告
**测试日期:** 2026-02-06
**测试人员:**
## 测试环境
- 后端版本:
- 前端版本:
- Redis版本:
- 浏览器版本:
## 功能测试
### 1. 正常导入测试
- [ ] 100条数据导入成功
- [ ] 1000条数据导入成功
- [ ] 状态查询正常
- [ ] 轮询通知正常
### 2. 失败数据处理测试
- [ ] 部分数据失败时正确显示失败记录
- [ ] 失误原因准确
- [ ] 失败记录可以正常查询
### 3. 批量更新测试
- [ ] 启用更新支持时,已有数据正确更新
- [ ] 未启用更新支持时,已有数据跳过
### 4. 并发测试
- [ ] 同时导入多个文件互不影响
- [ ] 任务ID唯一
- [ ] 各自状态独立
## 性能测试
| 数据量 | 响应时间 | 处理时间 | 结果 |
|--------|----------|----------|------|
| 100条 | | | |
| 1000条 | | | |
| 10000条 | | | |
## 问题记录
记录测试中发现的问题和解决方案。
## 测试结论
- [ ] 通过
- [ ] 不通过
步骤 7: 提交测试报告
git add doc/test/employee-import-test-report.md
git commit -m "test: 添加员工导入功能测试报告"
Task 13: 代码审查和优化
目标: 代码review,优化性能和代码质量
检查项:
- 代码符合项目编码规范
- 异常处理完善
- 日志记录充分
- Redis操作性能优化(考虑使用Pipeline)
- 批量操作批次大小合理
- 前端轮询间隔合理
- 内存使用合理,无内存泄漏风险
- 事务边界清晰
- 并发安全性
步骤 1: 代码审查
使用IDE的代码检查工具或人工review代码。
步骤 2: 性能优化
根据测试结果进行针对性优化:
- 调整批量操作批次大小
- 优化Redis操作
- 调整线程池参数
步骤 3: 提交优化代码
git add .
git commit -m "refactor: 代码审查和性能优化"
Task 14: 文档完善
目标: 完善用户手册和开发文档
文件:
- 修改:
README.md - 创建:
doc/user-guide/employee-import-guide.md
步骤 1: 更新README
在项目README中添加导入功能说明。
步骤 2: 编写用户指南
# 员工信息导入用户指南
## 导入流程
1. 准备Excel文件
2. 登录系统
3. 进入员工信息管理
4. 点击"导入"按钮
5. 选择文件并上传
6. 等待后台处理完成
7. 查看导入结果
8. 如有失败,查看并修正失败记录
## 常见问题
### Q1: 导入后多久能看到结果?
A: 通常2-10秒,具体取决于数据量。
### Q2: 失败记录会保留多久?
A: 7天,过期自动删除。
### Q3: 如何修正失败的数据?
A: 点击"查看导入失败记录",修正后重新导入。
步骤 3: 提交文档
git add README.md doc/user-guide/employee-import-guide.md
git commit -m "docs: 完善员工导入功能文档"
Task 15: 最终提交和发布
目标: 合并代码,发布新版本
步骤 1: 查看所有变更
git status
git log --oneline -10
步骤 2: 创建合并请求
如果使用Git Flow,创建feature分支合并请求。
步骤 3: 代码最终review
进行最后一次代码review。
步骤 4: 合并到主分支
git checkout dev
git merge feature/employee-async-import
git push origin dev
步骤 5: 打Tag(可选)
git tag -a v1.x.x -m "员工信息异步导入功能"
git push origin v1.x.x
步骤 6: 部署到测试环境
按照部署流程部署到测试环境。
步骤 7: 用户验收测试
邀请用户进行验收测试。
步骤 8: 部署到生产环境
验收通过后,部署到生产环境。
附录
A. 相关文件清单
后端:
ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/config/AsyncConfig.javaruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/ImportResultVO.javaruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/ImportStatusVO.javaruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/ImportFailureVO.javaruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiEmployeeService.javaruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeServiceImpl.javaruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.javaruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xmlruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiEmployeeController.java
前端:
ruoyi-ui/src/api/ccdiEmployee.jsruoyi-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小时