Files
ccdi/doc/requirements/plans/2026-02-06-employee-async-import.md
wkc 1cd87d2695 refactor: 重命名 ruoyi-ccdi 模块为 ruoyi-info-collection
- Maven 模块从 ruoyi-ccdi 重命名为 ruoyi-info-collection
- Java 包名从 com.ruoyi.ccdi 改为 com.ruoyi.info.collection
- MyBatis XML 命名空间同步更新
- 保留数据库表名、API URL、权限标识中的 ccdi 前缀
- 更新项目文档中的模块引用
2026-02-24 17:12:11 +08:00

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-info-collection/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-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

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-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: 检查现有约束

-- 使用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-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 中添加:

/**
 * 批量插入或更新员工信息
 * 使用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-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 中添加:

/**
 * 异步导入员工数据
 *
 * @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-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中添加:

@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-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 中添加:

// 查询导入状态
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: 手动测试流程

  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客户端查看:

# 连接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-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小时