Merge branch 'feat/staff-transfer-staff-id-validation' into dev_1
功能: 员工调动导入员工ID校验 新增功能: - 批量预验证员工ID存在性(1次数据库查询) - 错误信息包含Excel行号 - 主循环跳过已失败记录 - 完整的日志记录 技术实现: - 添加 CcdiBaseStaffMapper 依赖注入 - 新增 batchValidateStaffIds() 方法 - 新增 isRowAlreadyFailed() 方法 - 修改 importTransferAsync() 主流程 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
210
doc/interface-doc/ccdi/staff-transfer.md
Normal file
210
doc/interface-doc/ccdi/staff-transfer.md
Normal file
@@ -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)
|
||||||
|
- 初始版本
|
||||||
508
doc/plans/2026-02-11-staff-transfer-validation-implementation.md
Normal file
508
doc/plans/2026-02-11-staff-transfer-validation-implementation.md
Normal file
@@ -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<Long> batchValidateStaffIds(List<CcdiStaffTransferExcel> excelList,
|
||||||
|
String taskId,
|
||||||
|
List<StaffTransferImportFailureVO> failures) {
|
||||||
|
// 1. 提取并去重员工ID
|
||||||
|
Set<Long> 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<CcdiBaseStaff> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.select(CcdiBaseStaff::getStaffId)
|
||||||
|
.in(CcdiBaseStaff::getStaffId, allStaffIds);
|
||||||
|
|
||||||
|
List<CcdiBaseStaff> existingStaff = baseStaffMapper.selectList(wrapper);
|
||||||
|
Set<Long> 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<StaffTransferImportFailureVO> 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<CcdiStaffTransfer> newRecords = new ArrayList<>();
|
||||||
|
List<StaffTransferImportFailureVO> failures = new ArrayList<>();
|
||||||
|
|
||||||
|
// 批量查询已存在的唯一键组合
|
||||||
|
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的调动记录", excelList.size());
|
||||||
|
Set<String> existingKeys = getExistingTransferKeys(excelList);
|
||||||
|
ImportLogUtils.logBatchQueryComplete(log, taskId, "调动记录", existingKeys.size());
|
||||||
|
```
|
||||||
|
|
||||||
|
修改为:
|
||||||
|
|
||||||
|
```java
|
||||||
|
List<CcdiStaffTransfer> newRecords = new ArrayList<>();
|
||||||
|
List<StaffTransferImportFailureVO> failures = new ArrayList<>();
|
||||||
|
|
||||||
|
// 批量验证员工ID是否存在
|
||||||
|
Set<Long> existingStaffIds = batchValidateStaffIds(excelList, taskId, failures);
|
||||||
|
|
||||||
|
// 批量查询已存在的唯一键组合
|
||||||
|
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的调动记录", excelList.size());
|
||||||
|
Set<String> 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 <noreply@anthropic.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实施后任务
|
||||||
|
|
||||||
|
### 合并到主分支
|
||||||
|
|
||||||
|
**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都无效的调动记录
|
||||||
@@ -29,6 +29,14 @@ public class StaffTransferImportFailureVO implements Serializable {
|
|||||||
@Schema(description = "员工姓名")
|
@Schema(description = "员工姓名")
|
||||||
private String staffName;
|
private String staffName;
|
||||||
|
|
||||||
|
/** 调动前部门ID */
|
||||||
|
@Schema(description = "调动前部门ID")
|
||||||
|
private Long deptIdBefore;
|
||||||
|
|
||||||
|
/** 调动后部门ID */
|
||||||
|
@Schema(description = "调动后部门ID")
|
||||||
|
private Long deptIdAfter;
|
||||||
|
|
||||||
/** 调动类型 */
|
/** 调动类型 */
|
||||||
@Schema(description = "调动类型")
|
@Schema(description = "调动类型")
|
||||||
private String transferType;
|
private String transferType;
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package com.ruoyi.ccdi.service.impl;
|
|||||||
|
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
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.CcdiStaffTransfer;
|
||||||
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferAddDTO;
|
import com.ruoyi.ccdi.domain.dto.CcdiStaffTransferAddDTO;
|
||||||
import com.ruoyi.ccdi.domain.excel.CcdiStaffTransferExcel;
|
import com.ruoyi.ccdi.domain.excel.CcdiStaffTransferExcel;
|
||||||
import com.ruoyi.ccdi.domain.vo.ImportResult;
|
import com.ruoyi.ccdi.domain.vo.ImportResult;
|
||||||
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
|
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
|
||||||
import com.ruoyi.ccdi.domain.vo.StaffTransferImportFailureVO;
|
import com.ruoyi.ccdi.domain.vo.StaffTransferImportFailureVO;
|
||||||
|
import com.ruoyi.ccdi.mapper.CcdiBaseStaffMapper;
|
||||||
import com.ruoyi.ccdi.mapper.CcdiStaffTransferMapper;
|
import com.ruoyi.ccdi.mapper.CcdiStaffTransferMapper;
|
||||||
import com.ruoyi.ccdi.service.ICcdiStaffTransferImportService;
|
import com.ruoyi.ccdi.service.ICcdiStaffTransferImportService;
|
||||||
import com.ruoyi.ccdi.utils.ImportLogUtils;
|
import com.ruoyi.ccdi.utils.ImportLogUtils;
|
||||||
@@ -16,8 +18,7 @@ import com.ruoyi.common.utils.DictUtils;
|
|||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.system.mapper.SysDeptMapper;
|
import com.ruoyi.system.mapper.SysDeptMapper;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.slf4j.Logger;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
@@ -35,12 +36,11 @@ import java.util.stream.Collectors;
|
|||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
* @date 2026-02-10
|
* @date 2026-02-10
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImportService {
|
public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImportService {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(CcdiStaffTransferImportServiceImpl.class);
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private CcdiStaffTransferMapper transferMapper;
|
private CcdiStaffTransferMapper transferMapper;
|
||||||
|
|
||||||
@@ -50,6 +50,9 @@ public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImp
|
|||||||
@Resource
|
@Resource
|
||||||
private RedisTemplate<String, Object> redisTemplate;
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CcdiBaseStaffMapper baseStaffMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Async
|
@Async
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -62,6 +65,9 @@ public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImp
|
|||||||
List<CcdiStaffTransfer> newRecords = new ArrayList<>();
|
List<CcdiStaffTransfer> newRecords = new ArrayList<>();
|
||||||
List<StaffTransferImportFailureVO> failures = new ArrayList<>();
|
List<StaffTransferImportFailureVO> failures = new ArrayList<>();
|
||||||
|
|
||||||
|
// 批量验证员工ID是否存在
|
||||||
|
Set<Long> existingStaffIds = batchValidateStaffIds(excelList, taskId, failures);
|
||||||
|
|
||||||
// 批量查询已存在的唯一键组合
|
// 批量查询已存在的唯一键组合
|
||||||
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的调动记录", excelList.size());
|
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的调动记录", excelList.size());
|
||||||
Set<String> existingKeys = getExistingTransferKeys(excelList);
|
Set<String> existingKeys = getExistingTransferKeys(excelList);
|
||||||
@@ -74,6 +80,11 @@ public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImp
|
|||||||
for (int i = 0; i < excelList.size(); i++) {
|
for (int i = 0; i < excelList.size(); i++) {
|
||||||
CcdiStaffTransferExcel excel = excelList.get(i);
|
CcdiStaffTransferExcel excel = excelList.get(i);
|
||||||
|
|
||||||
|
// 跳过已在预验证阶段失败的记录
|
||||||
|
if (isRowAlreadyFailed(excel, failures)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 转换为AddDTO进行验证
|
// 转换为AddDTO进行验证
|
||||||
CcdiStaffTransferAddDTO addDTO = new CcdiStaffTransferAddDTO();
|
CcdiStaffTransferAddDTO addDTO = new CcdiStaffTransferAddDTO();
|
||||||
@@ -344,4 +355,75 @@ public class CcdiStaffTransferImportServiceImpl implements ICcdiStaffTransferImp
|
|||||||
|
|
||||||
return JSON.parseArray(JSON.toJSONString(failuresObj), StaffTransferImportFailureVO.class);
|
return JSON.parseArray(JSON.toJSONString(failuresObj), StaffTransferImportFailureVO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量验证员工ID是否存在
|
||||||
|
*
|
||||||
|
* @param excelList Excel数据列表
|
||||||
|
* @param taskId 任务ID
|
||||||
|
* @param failures 失败记录列表(会追加验证失败的记录)
|
||||||
|
* @return 存在的员工ID集合
|
||||||
|
*/
|
||||||
|
private Set<Long> batchValidateStaffIds(List<CcdiStaffTransferExcel> excelList,
|
||||||
|
String taskId,
|
||||||
|
List<StaffTransferImportFailureVO> failures) {
|
||||||
|
// 1. 提取并去重员工ID
|
||||||
|
Set<Long> 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<CcdiBaseStaff> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.select(CcdiBaseStaff::getStaffId)
|
||||||
|
.in(CcdiBaseStaff::getStaffId, allStaffIds);
|
||||||
|
|
||||||
|
List<CcdiBaseStaff> existingStaff = baseStaffMapper.selectList(wrapper);
|
||||||
|
Set<Long> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查某行数据是否已在失败列表中
|
||||||
|
*
|
||||||
|
* @param excel Excel数据
|
||||||
|
* @param failures 失败记录列表
|
||||||
|
* @return true-已失败,false-未失败
|
||||||
|
*/
|
||||||
|
private boolean isRowAlreadyFailed(CcdiStaffTransferExcel excel,
|
||||||
|
List<StaffTransferImportFailureVO> failures) {
|
||||||
|
return failures.stream()
|
||||||
|
.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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user