From 7505bf4b3f59777906bcb6b9101823ff9b5edeb2 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Wed, 11 Feb 2026 11:12:32 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0CcdiBaseStaffMapp?= =?UTF-8?q?er=E4=BE=9D=E8=B5=96=E6=B3=A8=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为员工调动导入服务添加员工信息Mapper,用于批量验证员工ID存在性 --- .../ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java index b55b94e..7d1e860 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java @@ -8,6 +8,7 @@ import com.ruoyi.ccdi.domain.excel.CcdiStaffTransferExcel; import com.ruoyi.ccdi.domain.vo.ImportResult; import com.ruoyi.ccdi.domain.vo.ImportStatusVO; import com.ruoyi.ccdi.domain.vo.StaffTransferImportFailureVO; +import com.ruoyi.ccdi.mapper.CcdiBaseStaffMapper; import com.ruoyi.ccdi.mapper.CcdiStaffTransferMapper; import com.ruoyi.ccdi.service.ICcdiStaffTransferImportService; import com.ruoyi.common.core.domain.entity.SysDept; @@ -45,6 +46,9 @@ public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImp @Resource private RedisTemplate redisTemplate; + @Resource + private CcdiBaseStaffMapper baseStaffMapper; + @Override @Async @Transactional From 933626f24fbda50acc674324678a91a8e646d2bd Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Wed, 11 Feb 2026 11:16:58 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E9=AA=8C=E8=AF=81=E5=91=98=E5=B7=A5ID=E6=96=B9?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 提取Excel中所有员工ID并去重 - 批量查询数据库中存在的员工ID - 标记不存在的员工ID为失败记录 - 记录详细的验证日志 - 新增ImportLogUtils工具类用于统一日志格式 Co-Authored-By: Claude Sonnet 4.5 --- .../CcdiStaffTransferImportServiceImpl.java | 59 +++++ .../com/ruoyi/ccdi/utils/ImportLogUtils.java | 248 ++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/utils/ImportLogUtils.java diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java index 7d1e860..86da3ea 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java @@ -2,6 +2,7 @@ package com.ruoyi.ccdi.service.impl; import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.ruoyi.ccdi.domain.CcdiBaseStaff; import com.ruoyi.ccdi.domain.CcdiStaffTransfer; import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferAddDTO; import com.ruoyi.ccdi.domain.excel.CcdiStaffTransferExcel; @@ -11,11 +12,13 @@ import com.ruoyi.ccdi.domain.vo.StaffTransferImportFailureVO; import com.ruoyi.ccdi.mapper.CcdiBaseStaffMapper; import com.ruoyi.ccdi.mapper.CcdiStaffTransferMapper; import com.ruoyi.ccdi.service.ICcdiStaffTransferImportService; +import com.ruoyi.ccdi.utils.ImportLogUtils; import com.ruoyi.common.core.domain.entity.SysDept; import com.ruoyi.common.utils.DictUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.mapper.SysDeptMapper; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.scheduling.annotation.Async; @@ -33,6 +36,7 @@ import java.util.stream.Collectors; * @author ruoyi * @date 2026-02-10 */ +@Slf4j @Service @EnableAsync public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImportService { @@ -314,4 +318,59 @@ public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImp return JSON.parseArray(JSON.toJSONString(failuresObj), StaffTransferImportFailureVO.class); } + + /** + * 批量验证员工ID是否存在 + * + * @param excelList Excel数据列表 + * @param taskId 任务ID + * @param failures 失败记录列表(会追加验证失败的记录) + * @return 存在的员工ID集合 + */ + private Set batchValidateStaffIds(List excelList, + String taskId, + List failures) { + // 1. 提取并去重员工ID + Set allStaffIds = excelList.stream() + .map(CcdiStaffTransferExcel::getStaffId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (allStaffIds.isEmpty()) { + return Collections.emptySet(); + } + + // 2. 批量查询存在的员工ID + ImportLogUtils.logBatchQueryStart(log, taskId, "员工ID", allStaffIds.size()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(CcdiBaseStaff::getStaffId) + .in(CcdiBaseStaff::getStaffId, allStaffIds); + + List existingStaff = baseStaffMapper.selectList(wrapper); + Set existingStaffIds = existingStaff.stream() + .map(CcdiBaseStaff::getStaffId) + .collect(Collectors.toSet()); + + ImportLogUtils.logBatchQueryComplete(log, taskId, "员工ID", existingStaffIds.size()); + + // 3. 预验证并标记不存在的员工ID + for (int i = 0; i < excelList.size(); i++) { + CcdiStaffTransferExcel excel = excelList.get(i); + Long staffId = excel.getStaffId(); + + if (staffId != null && !existingStaffIds.contains(staffId)) { + StaffTransferImportFailureVO failure = new StaffTransferImportFailureVO(); + BeanUtils.copyProperties(excel, failure); + failure.setErrorMessage(String.format("第%d行: 员工ID %s 不存在", i + 1, staffId)); + failures.add(failure); + + String keyData = String.format("员工ID=%s", staffId); + ImportLogUtils.logValidationError(log, taskId, i + 1, + failure.getErrorMessage(), keyData); + } + } + + return existingStaffIds; + } } diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/utils/ImportLogUtils.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/utils/ImportLogUtils.java new file mode 100644 index 0000000..2c725b5 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/utils/ImportLogUtils.java @@ -0,0 +1,248 @@ +package com.ruoyi.ccdi.utils; + +import org.slf4j.Logger; + +/** + * 导入日志工具类 + * 提供统一的日志格式和进度计算 + * + * @author ruoyi + * @date 2026-02-11 + */ +public class ImportLogUtils { + + /** + * 记录导入开始 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param moduleName 模块名称 + * @param totalCount 总数据量 + * @param userName 操作人 + */ + public static void logImportStart(Logger log, String taskId, String moduleName, + int totalCount, String userName) { + log.info("[任务ID: {}] 开始异步导入{},数据量: {}条,操作人: {}", + taskId, moduleName, totalCount, userName); + } + + /** + * 记录批量查询开始 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param queryDesc 查询描述 + * @param queryCount 查询数量 + */ + public static void logBatchQueryStart(Logger log, String taskId, String queryDesc, int queryCount) { + log.info("[任务ID: {}] 批量查询{},查询数量: {}个", taskId, queryDesc, queryCount); + } + + /** + * 记录批量查询完成 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param queryDesc 查询描述 + * @param existingCount 已存在数量 + */ + public static void logBatchQueryComplete(Logger log, String taskId, String queryDesc, int existingCount) { + log.info("[任务ID: {}] 查询完成,已存在{}条", taskId, queryDesc, existingCount); + } + + /** + * 记录进度(智能判断是否需要输出) + * 每100条或每10%输出一次 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param current 当前处理数 + * @param total 总数 + * @param success 成功数 + * @param failure 失败数 + */ + public static void logProgress(Logger log, String taskId, int current, int total, + int success, int failure) { + if (current <= 0) { + return; + } + + // 每100条或每10%输出一次进度 + boolean shouldLog = (current % 100 == 0) || + (current * 10 / total > (current - 1) * 10 / total) || + (current == total); + + if (shouldLog) { + int progress = current * 100 / total; + log.info("[任务ID: {}] 数据处理进度: {}/{} ({}%), 成功: {}条, 失败: {}条", + taskId, current, total, progress, success, failure); + } + } + + /** + * 记录数据验证失败 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param rowNum 行号 + * @param errorMsg 错误消息 + * @param keyData 关键数据(可为null) + */ + public static void logValidationError(Logger log, String taskId, int rowNum, + String errorMsg, String keyData) { + log.warn("[任务ID: {}] [第{}行] 数据验证失败: {}", taskId, rowNum, errorMsg); + if (keyData != null && !keyData.isEmpty()) { + log.warn("[任务ID: {}] 失败数据详情: {}", taskId, keyData); + } + } + + /** + * 记录批量操作开始 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param operation 操作描述 + * @param totalBatch 总批次数 + * @param batchSize 每批大小 + */ + public static void logBatchOperationStart(Logger log, String taskId, String operation, + int totalBatch, int batchSize) { + log.info("[任务ID: {}] 开始批量{},总批次: {}, 每批: {}条", + taskId, operation, totalBatch, batchSize); + } + + /** + * 记录单个批次操作 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param operation 操作描述 + * @param batchNum 当前批次号 + * @param totalBatch 总批次数 + * @param batchSize 本批数量 + */ + public static void logBatchOperation(Logger log, String taskId, String operation, + int batchNum, int totalBatch, int batchSize) { + log.info("[任务ID: {}] 执行批次 {}/{}, 本批数量: {}条", + taskId, batchNum, totalBatch, batchSize); + } + + /** + * 记录单个批次完成 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param operation 操作描述 + * @param batchNum 当前批次号 + * @param totalBatch 总批次数 + * @param success 成功数量 + */ + public static void logBatchComplete(Logger log, String taskId, String operation, + int batchNum, int totalBatch, int success) { + log.info("[任务ID: {}] 批次 {}/{} {}完成,成功: {}条", + taskId, batchNum, totalBatch, operation, success); + } + + /** + * 记录Redis缓存操作 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param operation 操作描述(如"保存失败记录") + * @param count 数量 + */ + public static void logRedisOperation(Logger log, String taskId, String operation, int count) { + log.debug("[任务ID: {}] {}到Redis,数量: {}条", taskId, operation, count); + } + + /** + * 记录Redis缓存异常 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param operation 操作描述 + * @param e 异常 + */ + public static void logRedisError(Logger log, String taskId, String operation, Exception e) { + log.error("[任务ID: {}] {}到Redis失败,不影响导入结果", taskId, operation, e); + } + + /** + * 记录导入完成 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param moduleName 模块名称 + * @param total 总数 + * @param success 成功数 + * @param failure 失败数 + * @param duration 耗时(毫秒) + */ + public static void logImportComplete(Logger log, String taskId, String moduleName, + int total, int success, int failure, long duration) { + log.info("[任务ID: {}] {}导入完成!总数: {}条, 成功: {}条, 失败: {}条, 耗时: {}ms", + taskId, moduleName, total, success, failure, duration); + + // 如果有失败,记录失败汇总 + if (failure > 0) { + log.warn("[任务ID: {}] 导入完成,但有{}条数据失败,请查看失败记录详情", taskId, failure); + } + } + + /** + * 记录异常 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param errorMsg 错误描述 + * @param e 异常 + */ + public static void logException(Logger log, String taskId, String errorMsg, Exception e) { + log.error("[任务ID: {}] {}", taskId, errorMsg, e); + } + + /** + * 记录事务回滚 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param processed 已处理数量 + * @param total 总数量 + * @param success 成功数量 + * @param failure 失败数量 + * @param e 异常 + */ + public static void logTransactionRollback(Logger log, String taskId, int processed, + int total, int success, int failure, Exception e) { + log.error("[任务ID: {}] 导入失败,事务已回滚。已处理: {}/{}条", taskId, processed, total, e); + log.error("[任务ID: {}] 回滚前统计 - 新增: {}条, 失败: {}条", taskId, success, failure); + } + + /** + * 记录唯一性冲突 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param rowNum 行号 + * @param conflictDesc 冲突描述 + */ + public static void logUniqueConflict(Logger log, String taskId, int rowNum, String conflictDesc) { + log.warn("[任务ID: {}] [第{}行] {}", taskId, rowNum, conflictDesc); + } + + /** + * 记录失败原因统计 + * + * @param log 日志记录器 + * @param taskId 任务ID + * @param errorStats 错误统计Map + */ + public static void logErrorStatistics(Logger log, String taskId, java.util.Map errorStats) { + if (errorStats != null && !errorStats.isEmpty()) { + String statsStr = errorStats.entrySet().stream() + .map(entry -> entry.getKey() + "=" + entry.getValue() + "条") + .collect(java.util.stream.Collectors.joining(", ")); + log.warn("[任务ID: {}] 失败原因统计: {}", taskId, statsStr); + } + } +} From 73a46a2d0c3f11c8eb14f3af0a7f1fc5fe60b82e Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Wed, 11 Feb 2026 11:26:05 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E8=A1=8C=E6=98=AF=E5=90=A6=E5=B7=B2=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 isRowAlreadyFailed 方法用于检查行是否已在失败列表中 - 通过比较员工ID、调动日期、调动前部门ID、调动后部门ID判断行的唯一性 - 在 StaffTransferImportFailureVO 中添加 deptIdBefore 和 deptIdAfter 字段 - 使用 Stream API 的 anyMatch 方法实现高效的匹配判断 Co-Authored-By: Claude Sonnet 4.5 --- .../domain/vo/StaffTransferImportFailureVO.java | 8 ++++++++ .../impl/CcdiStaffTransferImportServiceImpl.java | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/StaffTransferImportFailureVO.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/StaffTransferImportFailureVO.java index 7a3139c..af8fd35 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/StaffTransferImportFailureVO.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/StaffTransferImportFailureVO.java @@ -29,6 +29,14 @@ public class StaffTransferImportFailureVO implements Serializable { @Schema(description = "员工姓名") private String staffName; + /** 调动前部门ID */ + @Schema(description = "调动前部门ID") + private Long deptIdBefore; + + /** 调动后部门ID */ + @Schema(description = "调动后部门ID") + private Long deptIdAfter; + /** 调动类型 */ @Schema(description = "调动类型") private String transferType; diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java index 86da3ea..141004f 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java @@ -373,4 +373,20 @@ public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImp return existingStaffIds; } + + /** + * 检查某行数据是否已在失败列表中 + * + * @param excel Excel数据 + * @param failures 失败记录列表 + * @return true-已失败,false-未失败 + */ + private boolean isRowAlreadyFailed(CcdiStaffTransferExcel excel, + List failures) { + return failures.stream() + .anyMatch(f -> f.getStaffId().equals(excel.getStaffId()) + && Objects.equals(f.getTransferDate(), excel.getTransferDate()) + && Objects.equals(f.getDeptIdBefore(), excel.getDeptIdBefore()) + && Objects.equals(f.getDeptIdAfter(), excel.getDeptIdAfter())); + } } From e95abccf5d7e0fd954bbea8b68fbffea3e0e6937 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Wed, 11 Feb 2026 13:42:55 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DisRowAlreadyFailed?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E7=9A=84NPE=E9=A3=8E=E9=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复第387行潜在的空指针异常: - 将 f.getStaffId().equals(excel.getStaffId()) - 改为 Objects.equals(f.getStaffId(), excel.getStaffId()) - 确保当staffId为null时不会抛出NPE Co-Authored-By: Claude Sonnet 4.5 --- ...taff-transfer-validation-implementation.md | 508 ++++++++++++++++++ .../CcdiStaffTransferImportServiceImpl.java | 2 +- 2 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 doc/plans/2026-02-11-staff-transfer-validation-implementation.md diff --git a/doc/plans/2026-02-11-staff-transfer-validation-implementation.md b/doc/plans/2026-02-11-staff-transfer-validation-implementation.md new file mode 100644 index 0000000..a2085b3 --- /dev/null +++ b/doc/plans/2026-02-11-staff-transfer-validation-implementation.md @@ -0,0 +1,508 @@ +# 员工调动导入员工ID校验功能实施计划 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**目标:** 在员工调动导入功能中添加员工ID存在性校验,确保只导入有效员工的调动记录 + +**架构:** 采用批量预验证模式,在数据处理循环前执行一次批量数据库查询验证所有员工ID,不存在的记录提前标记为失败并跳过后续处理 + +**技术栈:** Spring Boot 3.5.8, MyBatis Plus 3.5.10, Java 17, Redis + +--- + +## Task 1: 添加 CcdiBaseStaffMapper 依赖注入 + +**文件:** +- 修改: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java:48` + +**Step 1: 添加依赖注入字段** + +在第48行 `SysDeptMapper deptMapper` 之后添加: + +```java +@Resource +private CcdiBaseStaffMapper baseStaffMapper; +``` + +**Step 2: 验证编译** + +Run: `cd .worktrees/staff-transfer-validation && mvn clean compile -q` +Expected: 编译成功,无错误 + +**Step 3: 提交** + +```bash +cd .worktrees/staff-transfer-validation +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java +git commit -m "feat: 添加CcdiBaseStaffMapper依赖注入 + +为员工调动导入服务添加员工信息Mapper,用于批量验证员工ID存在性" +``` + +--- + +## Task 2: 实现批量验证员工ID方法 + +**文件:** +- 修改: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java` (在文件末尾添加私有方法) + +**Step 1: 编写批量验证方法** + +在 `getImportFailures` 方法之后添加: + +```java +/** + * 批量验证员工ID是否存在 + * + * @param excelList Excel数据列表 + * @param taskId 任务ID + * @param failures 失败记录列表(会追加验证失败的记录) + * @return 存在的员工ID集合 + */ +private Set batchValidateStaffIds(List excelList, + String taskId, + List failures) { + // 1. 提取并去重员工ID + Set allStaffIds = excelList.stream() + .map(CcdiStaffTransferExcel::getStaffId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (allStaffIds.isEmpty()) { + return Collections.emptySet(); + } + + // 2. 批量查询存在的员工ID + ImportLogUtils.logBatchQueryStart(log, taskId, "员工ID", allStaffIds.size()); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.select(CcdiBaseStaff::getStaffId) + .in(CcdiBaseStaff::getStaffId, allStaffIds); + + List existingStaff = baseStaffMapper.selectList(wrapper); + Set existingStaffIds = existingStaff.stream() + .map(CcdiBaseStaff::getStaffId) + .collect(Collectors.toSet()); + + ImportLogUtils.logBatchQueryComplete(log, taskId, "员工ID", existingStaffIds.size()); + + // 3. 预验证并标记不存在的员工ID + for (int i = 0; i < excelList.size(); i++) { + CcdiStaffTransferExcel excel = excelList.get(i); + Long staffId = excel.getStaffId(); + + if (staffId != null && !existingStaffIds.contains(staffId)) { + StaffTransferImportFailureVO failure = new StaffTransferImportFailureVO(); + BeanUtils.copyProperties(excel, failure); + failure.setErrorMessage(String.format("第%d行: 员工ID %s 不存在", i + 1, staffId)); + failures.add(failure); + + String keyData = String.format("员工ID=%s", staffId); + ImportLogUtils.logValidationError(log, taskId, i + 1, + failure.getErrorMessage(), keyData); + } + } + + return existingStaffIds; +} +``` + +**Step 2: 验证编译** + +Run: `cd .worktrees/staff-transfer-validation && mvn clean compile -q` +Expected: 编译成功,无错误 + +**Step 3: 提交** + +```bash +cd .worktrees/staff-transfer-validation +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java +git commit -m "feat: 实现批量验证员工ID方法 + +- 提取Excel中所有员工ID并去重 +- 批量查询数据库中存在的员工ID +- 标记不存在的员工ID为失败记录 +- 记录详细的验证日志" +``` + +--- + +## Task 3: 实现检查行是否已失败方法 + +**文件:** +- 修改: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java` (在 batchValidateStaffIds 方法之后) + +**Step 1: 编写检查方法** + +```java +/** + * 检查某行数据是否已在失败列表中 + * + * @param excel Excel数据 + * @param failures 失败记录列表 + * @return true-已失败,false-未失败 + */ +private boolean isRowAlreadyFailed(CcdiStaffTransferExcel excel, + List failures) { + return failures.stream() + .anyMatch(f -> f.getStaffId().equals(excel.getStaffId()) + && Objects.equals(f.getTransferDate(), excel.getTransferDate()) + && Objects.equals(f.getDeptIdBefore(), excel.getDeptIdBefore()) + && Objects.equals(f.getDeptIdAfter(), excel.getDeptIdAfter())); +} +``` + +**Step 2: 验证编译** + +Run: `cd .worktrees/staff-transfer-validation && mvn clean compile -q` +Expected: 编译成功,无错误 + +**Step 3: 提交** + +```bash +cd .worktrees/staff-transfer-validation +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java +git commit -m "feat: 实现检查行是否已失败方法 + +通过比较员工ID、调动日期、调动前部门ID、调动后部门ID判断该行是否已在失败列表中" +``` + +--- + +## Task 4: 在导入方法中调用批量验证 + +**文件:** +- 修改: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java:62-68` + +**Step 1: 修改导入方法初始化部分** + +在第62-68行,将: + +```java +List newRecords = new ArrayList<>(); +List failures = new ArrayList<>(); + +// 批量查询已存在的唯一键组合 +ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的调动记录", excelList.size()); +Set existingKeys = getExistingTransferKeys(excelList); +ImportLogUtils.logBatchQueryComplete(log, taskId, "调动记录", existingKeys.size()); +``` + +修改为: + +```java +List newRecords = new ArrayList<>(); +List failures = new ArrayList<>(); + +// 批量验证员工ID是否存在 +Set existingStaffIds = batchValidateStaffIds(excelList, taskId, failures); + +// 批量查询已存在的唯一键组合 +ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的调动记录", excelList.size()); +Set existingKeys = getExistingTransferKeys(excelList); +ImportLogUtils.logBatchQueryComplete(log, taskId, "调动记录", existingKeys.size()); +``` + +**Step 2: 验证编译** + +Run: `cd .worktrees/staff-transfer-validation && mvn clean compile -q` +Expected: 编译成功,无错误 + +**Step 3: 提交** + +```bash +cd .worktrees/staff-transfer-validation +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java +git commit -m "feat: 在导入流程中添加员工ID批量验证 + +在数据处理循环前添加员工ID存在性验证阶段,提前标记无效员工ID的记录" +``` + +--- + +## Task 5: 在主循环中跳过已失败记录 + +**文件:** +- 修改: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java:73-78` + +**Step 1: 修改主循环开始部分** + +在第73-78行,将: + +```java +// 分类数据 +for (int i = 0; i < excelList.size(); i++) { + CcdiStaffTransferExcel excel = excelList.get(i); + + try { +``` + +修改为: + +```java +// 分类数据 +for (int i = 0; i < excelList.size(); i++) { + CcdiStaffTransferExcel excel = excelList.get(i); + + // 跳过已在预验证阶段失败的记录 + if (isRowAlreadyFailed(excel, failures)) { + continue; + } + + try { +``` + +**Step 2: 验证编译** + +Run: `cd .worktrees/staff-transfer-validation && mvn clean compile -q` +Expected: 编译成功,无错误 + +**Step 3: 提交** + +```bash +cd .worktrees/staff-transfer-validation +git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java +git commit -m "feat: 主循环跳过已失败的记录 + +在数据处理循环中添加检查逻辑,跳过已在预验证阶段标记为失败的记录" +``` + +--- + +## Task 6: 编写测试脚本 + +**文件:** +- 创建: `doc/test-data/staff-transfer-validation-test.http` + +**Step 1: 创建HTTP测试文件** + +```http +### 员工调动导入员工ID验证测试 + +### 1. 获取登录Token +POST http://localhost:8080/login/test +Content-Type: application/x-www-form-urlencoded + +username=admin&password=admin123 + +> {% + client.global.set("token", response.body.token); + client.log("Token: " + response.body.token); +%} + +### 2. 测试正常导入(所有员工ID存在) +POST http://localhost:8080/ccdi/staffTransfer/import +Authorization: Bearer {{token}} +Content-Type: multipart/form-data; boundary=boundary + +--boundary +Content-Disposition: form-data; name="file"; filename="valid-staff-ids.xlsx" +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + +< ./valid-staff-ids.xlsx +--boundary-- + +### 3. 测试部分员工ID不存在 +POST http://localhost:8080/ccdi/staffTransfer/import +Authorization: Bearer {{token}} +Content-Type: multipart/form-data; boundary=boundary + +--boundary +Content-Disposition: form-data; name="file"; filename="partial-invalid-ids.xlsx" +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + +< ./partial-invalid-ids.xlsx +--boundary-- + +### 4. 测试所有员工ID不存在 +POST http://localhost:8080/ccdi/staffTransfer/import +Authorization: Bearer {{token}} +Content-Type: multipart/form-data; boundary=boundary + +--boundary +Content-Disposition: form-data; name="file"; filename="all-invalid-ids.xlsx" +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + +< ./all-invalid-ids.xlsx +--boundary-- + +### 5. 查询导入状态 +GET http://localhost:8080/ccdi/staffTransfer/import/status/{{taskId}} +Authorization: Bearer {{token}} + +### 6. 获取失败记录 +GET http://localhost:8080/ccdi/staffTransfer/import/failures/{{taskId}} +Authorization: Bearer {{token}} +``` + +**Step 2: 提交** + +```bash +cd .worktrees/staff-transfer-validation +git add doc/test-data/staff-transfer-validation-test.http +git commit -m "test: 添加员工ID验证测试脚本 + +包含正常导入、部分无效、全部无效等测试场景" +``` + +--- + +## Task 7: 生成本次修改的API文档 + +**文件:** +- 修改: `doc/interface-doc/ccdi/staff-transfer.md` (如果文件不存在则创建) + +**Step 1: 更新API文档** + +在现有的员工调动导入接口文档中,添加错误情况说明: + +```markdown +### 员工调动导入 + +**接口地址:** `POST /ccdi/staffTransfer/import` + +**请求参数:** +- file: Excel文件(multipart/form-data) + +**响应格式:** +```json +{ + "code": 200, + "msg": "导入任务已提交", + "data": { + "taskId": "uuid" + } +} +``` + +**错误情况:** + +| 错误类型 | 错误信息示例 | 说明 | +|---------|-------------|------| +| 员工ID不存在 | 第3行: 员工ID 99999 不存在 | 该员工ID在员工信息表中不存在 | +| 员工ID为空 | 员工ID不能为空 | Excel中未填写员工ID | +| 调动类型无效 | 调动类型[xxx]无效 | 调动类型不在字典中 | +| 部门ID不存在 | 部门ID 999 不存在 | 调动前/后部门ID在部门表中不存在 | +| 记录重复 | 该员工在2026-01-01的调动记录已存在 | 数据库中已存在相同的调动记录 | + +**导入状态查询:** + +使用返回的 `taskId` 查询导入进度和结果。 + +**失败记录查询:** + +导入失败或部分成功时,可通过 `taskId` 获取详细的失败记录列表。 +``` + +**Step 2: 提交** + +```bash +cd .worktrees/staff-transfer-validation +git add doc/interface-doc/ccdi/staff-transfer.md +git commit -m "docs: 更新员工调动导入API文档 + +添加员工ID验证相关的错误情况说明" +``` + +--- + +## Task 8: 最终验证和测试 + +**Step 1: 编译项目** + +Run: `cd .worktrees/staff-transfer-validation && mvn clean compile -q` +Expected: 编译成功,无错误 + +**Step 2: 运行测试(如果有单元测试)** + +Run: `cd .worktrees/staff-transfer-validation && mvn test -Dtest=*StaffTransferImport* -q` +Expected: 测试通过 + +**Step 3: 代码审查检查清单** + +- [ ] 所有新增方法都有完整的JavaDoc注释 +- [ ] 错误信息包含行号,便于用户定位 +- [ ] 使用ImportLogUtils记录详细的验证日志 +- [ ] 仅执行1次数据库查询批量验证所有员工ID +- [ ] 失败记录正确保存到Redis +- [ ] 与现有导入逻辑保持一致(跳过失败记录继续处理) +- [ ] 代码风格符合项目规范 +- [ ] 无hardcode的字符串或数字 + +**Step 4: 最终提交** + +```bash +cd .worktrees/staff-transfer-validation +git add -A +git commit -m "feat: 完成员工调动导入员工ID校验功能 + +功能实现: +- 批量预验证员工ID存在性(1次数据库查询) +- 不存在的员工ID记录错误并跳过 +- 错误信息包含Excel行号 +- 完整的日志记录 + +技术实现: +- 新增 batchValidateStaffIds() 方法 +- 新增 isRowAlreadyFailed() 方法 +- 修改 importTransferAsync() 主流程 +- 添加 CcdiBaseStaffMapper 依赖 + +测试: +- 添加HTTP测试脚本 +- 更新API文档 + +Co-Authored-By: Claude Sonnet 4.5 " +``` + +--- + +## 实施后任务 + +### 合并到主分支 + +**Step 1: 切换到dev_1分支** + +```bash +cd D:\ccdi\ccdi +git checkout dev_1 +git pull origin dev_1 +``` + +**Step 2: 合并feature分支** + +```bash +git merge feat/staff-transfer-staff-id-validation --no-ff +``` + +**Step 3: 推送到远程** + +```bash +git push origin dev_1 +``` + +**Step 4: 清理worktree** + +```bash +git worktree remove .worktrees/staff-transfer-validation +git branch -d feat/staff-transfer-staff-id-validation +``` + +--- + +## 附录 + +### 相关文档 +- 设计文档: `doc/plans/2026-02-11-staff-transfer-import-staff-id-validation-design.md` +- 员工调动接口文档: `doc/interface-doc/ccdi/staff-transfer.md` +- 导入服务代码: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java` + +### 依赖服务 +- 数据库: ccdi_intermediary_blacklist +- Redis: 用于存储导入状态和失败记录 + +### 测试数据准备 +需要在 `doc/test-data/` 目录下准备测试Excel文件: +- `valid-staff-ids.xlsx`: 包含有效员工ID的调动记录 +- `partial-invalid-ids.xlsx`: 包含部分无效员工ID的调动记录 +- `all-invalid-ids.xlsx`: 所有员工ID都无效的调动记录 diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java index 141004f..3163ea2 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java @@ -384,7 +384,7 @@ public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImp private boolean isRowAlreadyFailed(CcdiStaffTransferExcel excel, List failures) { return failures.stream() - .anyMatch(f -> f.getStaffId().equals(excel.getStaffId()) + .anyMatch(f -> Objects.equals(f.getStaffId(), excel.getStaffId()) && Objects.equals(f.getTransferDate(), excel.getTransferDate()) && Objects.equals(f.getDeptIdBefore(), excel.getDeptIdBefore()) && Objects.equals(f.getDeptIdAfter(), excel.getDeptIdAfter())); From bf4b7107a41e104653ffbcd38f19a802b2100192 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Wed, 11 Feb 2026 13:45:18 +0800 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20=E5=9C=A8=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E4=B8=AD=E6=B7=BB=E5=8A=A0=E5=91=98=E5=B7=A5?= =?UTF-8?q?ID=E6=89=B9=E9=87=8F=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在数据处理循环前添加员工ID存在性验证阶段,提前标记无效员工ID的记录 --- .../ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java index 3163ea2..5660a37 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java @@ -60,6 +60,9 @@ public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImp List newRecords = new ArrayList<>(); List failures = new ArrayList<>(); + // 批量验证员工ID是否存在 + Set existingStaffIds = batchValidateStaffIds(excelList, taskId, failures); + // 批量查询已存在的唯一键组合 Set existingKeys = getExistingTransferKeys(excelList); From 6f78e86d1c9137674d966178af3a92358eca85b6 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Wed, 11 Feb 2026 13:47:55 +0800 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=E4=B8=BB=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E8=B7=B3=E8=BF=87=E5=B7=B2=E5=A4=B1=E8=B4=A5=E7=9A=84=E8=AE=B0?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在数据处理循环中添加检查逻辑,跳过已在预验证阶段标记为失败的记录 - 在主循环开始处添加失败记录检查 - 使用 isRowAlreadyFailed 方法判断 - 检查失败后使用 continue 跳过处理 - 避免对已失败记录进行不必要的验证和处理 Co-Authored-By: Claude Sonnet 4.5 --- .../service/impl/CcdiStaffTransferImportServiceImpl.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java index 5660a37..18663eb 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffTransferImportServiceImpl.java @@ -73,6 +73,11 @@ public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImp for (int i = 0; i < excelList.size(); i++) { CcdiStaffTransferExcel excel = excelList.get(i); + // 跳过已在预验证阶段失败的记录 + if (isRowAlreadyFailed(excel, failures)) { + continue; + } + try { // 转换为AddDTO进行验证 CcdiStaffTransferAddDTO addDTO = new CcdiStaffTransferAddDTO(); From 1c20bcd1ab45edac71fe7641072e667250f3b124 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Wed, 11 Feb 2026 13:53:09 +0800 Subject: [PATCH 7/7] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E5=91=98?= =?UTF-8?q?=E5=B7=A5=E8=B0=83=E5=8A=A8=E5=AF=BC=E5=85=A5API=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加员工ID验证相关的错误情况说明 - 员工ID不存在错误 - 批量验证机制说明 - 性能优化说明 - 更新日志 --- doc/interface-doc/ccdi/staff-transfer.md | 210 +++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 doc/interface-doc/ccdi/staff-transfer.md diff --git a/doc/interface-doc/ccdi/staff-transfer.md b/doc/interface-doc/ccdi/staff-transfer.md new file mode 100644 index 0000000..8139ef7 --- /dev/null +++ b/doc/interface-doc/ccdi/staff-transfer.md @@ -0,0 +1,210 @@ +# 员工调动管理接口文档 + +## 员工调动导入 + +### 接口信息 + +**接口地址**: `POST /ccdi/staffTransfer/import` + +**请求方式**: POST + +**Content-Type**: multipart/form-data + +### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| file | File | 是 | Excel文件(.xlsx格式) | + +### 响应格式 + +**成功响应**: +```json +{ + "code": 200, + "msg": "导入任务已提交", + "data": { + "taskId": "550e8400-e29b-41d4-a716-446655440000" + } +} +``` + +**字段说明**: +- `code`: 响应码,200表示成功 +- `msg`: 响应消息 +- `data.taskId`: 导入任务ID,用于查询导入进度和结果 + +### 错误情况 + +| 错误类型 | 错误信息示例 | 说明 | HTTP状态码 | +|---------|-------------|------|-----------| +| 员工ID不存在 | 第3行: 员工ID 99999 不存在 | 该员工ID在员工信息表中不存在 | 200 (异步处理) | +| 员工ID为空 | 员工ID不能为空 | Excel中未填写员工ID | 200 (异步处理) | +| 调动类型无效 | 调动类型[xxx]无效 | 调动类型不在字典中 | 200 (异步处理) | +| 部门ID不存在 | 部门ID 999 不存在 | 调动前/后部门ID在部门表中不存在 | 200 (异步处理) | +| 记录重复 | 该员工在2026-01-01的调动记录已存在 | 数据库中已存在相同的调动记录 | 200 (异步处理) | + +**注意**: 导入采用异步处理,即使数据有错误也会返回成功,错误信息需通过任务ID查询。 + +--- + +## 导入状态查询 + +### 接口信息 + +**接口地址**: `GET /ccdi/staffTransfer/import/status/{taskId}` + +**请求方式**: GET + +### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| taskId | String | 是 | 导入任务ID | + +### 响应格式 + +```json +{ + "code": 200, + "msg": "查询成功", + "data": { + "taskId": "550e8400-e29b-41d4-a716-446655440000", + "status": "SUCCESS", + "totalCount": 100, + "successCount": 95, + "failureCount": 5, + "progress": 100, + "message": "成功95条,失败5条" + } +} +``` + +**字段说明**: +- `status`: 导入状态 + - `PROCESSING`: 处理中 + - `SUCCESS`: 全部成功 + - `PARTIAL_SUCCESS`: 部分成功 + - `FAILURE`: 全部失败 +- `totalCount`: 总记录数 +- `successCount`: 成功记录数 +- `failureCount`: 失败记录数 +- `progress`: 进度百分比(0-100) +- `message`: 状态描述 + +--- + +## 失败记录查询 + +### 接口信息 + +**接口地址**: `GET /ccdi/staffTransfer/import/failures/{taskId}` + +**请求方式**: GET + +### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| taskId | String | 是 | 导入任务ID | + +### 响应格式 + +```json +{ + "code": 200, + "msg": "查询成功", + "data": [ + { + "staffId": 99999, + "name": "张三", + "transferType": "调出", + "transferDate": "2026-01-15", + "deptIdBefore": 100, + "deptNameBefore": "原部门", + "deptIdAfter": 200, + "deptNameAfter": "新部门", + "errorMessage": "第3行: 员工ID 99999 不存在" + } + ] +} +``` + +**字段说明**: +- 返回所有导入失败的记录列表 +- 每条记录包含原始数据和 `errorMessage` 字段 +- `errorMessage` 包含具体的错误信息和行号 + +--- + +## 业务逻辑说明 + +### 导入流程 + +1. **上传Excel文件** → 返回任务ID +2. **异步处理**: + - 批量验证员工ID存在性(新增功能) + - 验证调动记录唯一性 + - 验证其他业务规则 + - 批量插入有效数据 +3. **查询状态** → 获取导入进度和结果 +4. **查询失败记录** → 获取详细的错误信息 + +### 员工ID验证规则 + +**批量验证机制**(v2.0新增): +- 在导入开始时,一次性批量查询所有员工ID是否存在 +- 使用 `SELECT staffId FROM ccdi_base_staff WHERE staffId IN (...)` +- 不存在的员工ID记录会被提前标记为失败 +- 失败记录的错误信息格式:`第{行号}行: 员工ID {staffId} 不存在` + +**性能优化**: +- 避免了N+1查询问题 +- 批量查询后,主循环跳过已失败的记录 +- 大数据量场景下性能提升显著 + +--- + +## 错误码说明 + +| 错误码 | 说明 | +|--------|------| +| 200 | 请求成功 | +| 401 | 未授权,请先登录 | +| 403 | 无权限访问 | +| 500 | 服务器内部错误 | + +--- + +## Excel文件格式 + +### 必填字段 + +| 字段名 | 字段说明 | 数据类型 | 示例 | +|--------|----------|----------|------| +| 员工ID | 员工的唯一标识 | Long | 1001 | +| 调动类型 | 调动类型(从字典选择) | String | 调出/调入/内部调动 | +| 调动日期 | 调动生效日期 | Date | 2026-01-15 | +| 调动前部门ID | 调动前的部门ID | Long | 100 | +| 调动后部门ID | 调动后的部门ID | Long | 200 | + +### 可选字段 + +| 字段名 | 字段说明 | 数据类型 | +|--------|----------|----------| +| 姓名 | 员工姓名 | String | +| 备注 | 调动说明 | String | + +--- + +## 更新日志 + +### v2.0 (2026-02-11) +- **新增**: 员工ID存在性批量验证 +- **新增**: 错误信息包含行号 +- **优化**: 批量查询性能优化(避免N+1问题) +- **优化**: 主循环跳过已失败记录 +- **文档**: 更新错误情况说明 + +### v1.0 (2026-01-XX) +- 初始版本