chore: 添加.worktrees/到gitignore
为使用git worktree功能做准备,防止意外提交worktree内容。 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -44,6 +44,9 @@ nbdist/
|
|||||||
*.swp
|
*.swp
|
||||||
nul
|
nul
|
||||||
|
|
||||||
|
# Git Worktrees
|
||||||
|
.worktrees/
|
||||||
|
|
||||||
test/
|
test/
|
||||||
|
|
||||||
!*/build/*.java
|
!*/build/*.java
|
||||||
|
|||||||
743
doc/plans/2026-02-08-中介导入异步化改造设计.md
Normal file
743
doc/plans/2026-02-08-中介导入异步化改造设计.md
Normal file
@@ -0,0 +1,743 @@
|
|||||||
|
# 中介库管理导入功能异步化改造设计文档
|
||||||
|
|
||||||
|
## 文档信息
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| **文档标题** | 中介库管理导入功能异步化改造 |
|
||||||
|
| **创建日期** | 2026-02-08 |
|
||||||
|
| **参考实现** | 员工信息导入功能 (CcdiEmployeeController) |
|
||||||
|
| **涉及模块** | 中介库管理 (ccdiIntermediary) |
|
||||||
|
| **改造范围** | 个人中介导入、实体中介导入 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
### 1.1 当前问题
|
||||||
|
|
||||||
|
**现状**: 中介库管理的导入功能采用**同步处理**方式,用户上传文件后需要等待所有数据处理完成才能收到响应。
|
||||||
|
|
||||||
|
**存在问题**:
|
||||||
|
- ⏱️ 大数据量导入时,用户需要长时间等待(可能数十秒甚至数分钟)
|
||||||
|
- 🚫 请求可能因超时而中断
|
||||||
|
- 😰 用户体验不佳,无法查看导入进度
|
||||||
|
- ❌ 导入失败后无法查看详细的失败记录
|
||||||
|
|
||||||
|
### 1.2 改造目标
|
||||||
|
|
||||||
|
将中介库管理的导入功能改造为**异步处理模式**,参考员工导入的成功实现:
|
||||||
|
|
||||||
|
**核心目标**:
|
||||||
|
- ⚡ **即时响应**: 用户上传文件后立即获得taskId,无需等待
|
||||||
|
- 📊 **进度追踪**: 前端轮询查询导入进度和状态
|
||||||
|
- 💾 **失败重试**: 失败记录保存在Redis,支持7天内查询和重试
|
||||||
|
- 🔄 **并发处理**: 支持多个用户同时导入,互不阻塞
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 三层架构模式
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Layer 1: Controller (CcdiIntermediaryController) │
|
||||||
|
│ - 解析Excel文件 │
|
||||||
|
│ - 调用主Service的importIntermediaryPerson/Entity() │
|
||||||
|
│ - 接收taskId │
|
||||||
|
│ - 封装ImportResultVO返回 │
|
||||||
|
└──────────────────┬──────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Layer 2: 主Service (CcdiIntermediaryServiceImpl) │
|
||||||
|
│ - 生成UUID作为taskId │
|
||||||
|
│ - 初始化Redis状态(PROCESSING) │
|
||||||
|
│ - 获取当前用户名(SecurityUtils.getUsername()) │
|
||||||
|
│ - 调用异步Service的importPersonAsync/EntityAsync() │
|
||||||
|
│ - 立即返回taskId │
|
||||||
|
└──────────────────┬──────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Layer 3: 异步Service (CcdiIntermediaryPersonImport │
|
||||||
|
│ /EntityImportServiceImpl) │
|
||||||
|
│ - @Async异步执行 │
|
||||||
|
│ - 批量验证、插入、更新数据 │
|
||||||
|
│ - 保存失败记录到Redis │
|
||||||
|
│ - 更新最终状态(SUCCESS/PARTIAL_SUCCESS) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 数据流转
|
||||||
|
|
||||||
|
```
|
||||||
|
用户上传文件
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Controller解析Excel
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
主Service生成taskId + 初始化Redis
|
||||||
|
│
|
||||||
|
├──► 立即返回taskId给Controller
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ Controller封装ImportResultVO返回
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ 前端收到响应,开始轮询查询状态
|
||||||
|
│
|
||||||
|
└──► 异步Service后台执行导入
|
||||||
|
│
|
||||||
|
├──► 批量验证数据
|
||||||
|
├──► 批量插入/更新数据
|
||||||
|
├──► 保存失败记录到Redis
|
||||||
|
└──► 更新Redis状态为SUCCESS/PARTIAL_SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Redis状态管理
|
||||||
|
|
||||||
|
**状态Key设计**:
|
||||||
|
|
||||||
|
| 类型 | 个人中介 | 实体中介 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| **导入状态** | `import:intermediary:{taskId}` | `import:intermediary-entity:{taskId}` |
|
||||||
|
| **失败记录** | `import:intermediary:{taskId}:failures` | `import:intermediary-entity:{taskId}:failures` |
|
||||||
|
| **过期时间** | 7天 | 7天 |
|
||||||
|
|
||||||
|
**状态字段结构** (Hash):
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
taskId: "uuid-string",
|
||||||
|
status: "PROCESSING" | "SUCCESS" | "PARTIAL_SUCCESS",
|
||||||
|
totalCount: 100,
|
||||||
|
successCount: 95,
|
||||||
|
failureCount: 5,
|
||||||
|
progress: 100,
|
||||||
|
startTime: 1234567890,
|
||||||
|
endTime: 1234567900,
|
||||||
|
message: "成功95条,失败5条"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 详细实现方案
|
||||||
|
|
||||||
|
### 3.1 后端改造
|
||||||
|
|
||||||
|
#### 文件1: CcdiIntermediaryServiceImpl.java
|
||||||
|
|
||||||
|
**路径**: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java`
|
||||||
|
|
||||||
|
**需要添加的依赖注入**:
|
||||||
|
```java
|
||||||
|
@Resource
|
||||||
|
private ICcdiIntermediaryPersonImportService personImportService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ICcdiIntermediaryEntityImportService entityImportService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
```
|
||||||
|
|
||||||
|
**改造1: importIntermediaryPerson方法**
|
||||||
|
|
||||||
|
**原实现** (同步,第251行开始):
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public String importIntermediaryPerson(List<...> list, boolean updateSupport) {
|
||||||
|
// 同步执行所有导入逻辑
|
||||||
|
// 返回消息字符串
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**新实现** (异步):
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public String importIntermediaryPerson(List<CcdiIntermediaryPersonExcel> list,
|
||||||
|
boolean updateSupport) {
|
||||||
|
String taskId = UUID.randomUUID().toString();
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// 初始化Redis状态
|
||||||
|
String statusKey = "import:intermediary:" + taskId;
|
||||||
|
Map<String, Object> statusData = new HashMap<>();
|
||||||
|
statusData.put("taskId", taskId);
|
||||||
|
statusData.put("status", "PROCESSING");
|
||||||
|
statusData.put("totalCount", list.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);
|
||||||
|
|
||||||
|
// 获取当前用户名
|
||||||
|
String userName = SecurityUtils.getUsername();
|
||||||
|
|
||||||
|
// 调用异步方法
|
||||||
|
personImportService.importPersonAsync(list, updateSupport, taskId, userName);
|
||||||
|
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**改造2: importIntermediaryEntity方法**
|
||||||
|
|
||||||
|
与个人中介类似,只需修改:
|
||||||
|
- Redis Key前缀为 `import:intermediary-entity:`
|
||||||
|
- 调用 `entityImportService.importEntityAsync()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 文件2: CcdiIntermediaryController.java
|
||||||
|
|
||||||
|
**路径**: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java`
|
||||||
|
|
||||||
|
**需要添加的依赖注入**:
|
||||||
|
```java
|
||||||
|
@Resource
|
||||||
|
private ICcdiIntermediaryPersonImportService personImportService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ICcdiIntermediaryEntityImportService entityImportService;
|
||||||
|
```
|
||||||
|
|
||||||
|
**需要添加的import**:
|
||||||
|
```java
|
||||||
|
import com.ruoyi.ccdi.domain.vo.ImportResultVO;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.ImportStatusVO;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.IntermediaryPersonImportFailureVO;
|
||||||
|
import com.ruoyi.ccdi.domain.vo.IntermediaryEntityImportFailureVO;
|
||||||
|
import com.ruoyi.ccdi.service.ICcdiIntermediaryPersonImportService;
|
||||||
|
import com.ruoyi.ccdi.service.ICcdiIntermediaryEntityImportService;
|
||||||
|
```
|
||||||
|
|
||||||
|
**改造1: importPersonData方法** (第183-188行)
|
||||||
|
|
||||||
|
**原实现**:
|
||||||
|
```java
|
||||||
|
@PostMapping("/importPersonData")
|
||||||
|
public AjaxResult importPersonData(MultipartFile file, boolean updateSupport) throws Exception {
|
||||||
|
List<CcdiIntermediaryPersonExcel> list = EasyExcelUtil.importExcel(...);
|
||||||
|
String message = intermediaryService.importIntermediaryPerson(list, updateSupport);
|
||||||
|
return success(message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**新实现**:
|
||||||
|
```java
|
||||||
|
@PostMapping("/importPersonData")
|
||||||
|
public AjaxResult importPersonData(MultipartFile file,
|
||||||
|
@RequestParam(defaultValue = "false") boolean updateSupport)
|
||||||
|
throws Exception {
|
||||||
|
List<CcdiIntermediaryPersonExcel> list = EasyExcelUtil.importExcel(
|
||||||
|
file.getInputStream(), CcdiIntermediaryPersonExcel.class);
|
||||||
|
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
return error("至少需要一条数据");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交异步任务
|
||||||
|
String taskId = intermediaryService.importIntermediaryPerson(list, updateSupport);
|
||||||
|
|
||||||
|
// 立即返回,不等待后台任务完成
|
||||||
|
ImportResultVO result = new ImportResultVO();
|
||||||
|
result.setTaskId(taskId);
|
||||||
|
result.setStatus("PROCESSING");
|
||||||
|
result.setMessage("导入任务已提交,正在后台处理");
|
||||||
|
|
||||||
|
return AjaxResult.success("导入任务已提交,正在后台处理", result);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**改造2: importEntityData方法** (第196-201行)
|
||||||
|
|
||||||
|
与个人中介类似,只需修改:
|
||||||
|
- Excel类为 `CcdiIntermediaryEntityExcel`
|
||||||
|
- 调用 `importIntermediaryEntity()`
|
||||||
|
|
||||||
|
**新增3: 查询个人中介导入状态**
|
||||||
|
```java
|
||||||
|
@GetMapping("/importPersonStatus/{taskId}")
|
||||||
|
public AjaxResult getPersonImportStatus(@PathVariable String taskId) {
|
||||||
|
try {
|
||||||
|
ImportStatusVO status = personImportService.getImportStatus(taskId);
|
||||||
|
return success(status);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**新增4: 查询个人中介导入失败记录**
|
||||||
|
```java
|
||||||
|
@GetMapping("/importPersonFailures/{taskId}")
|
||||||
|
public TableDataInfo getPersonImportFailures(
|
||||||
|
@PathVariable String taskId,
|
||||||
|
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||||
|
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||||
|
|
||||||
|
List<IntermediaryPersonImportFailureVO> failures =
|
||||||
|
personImportService.getImportFailures(taskId);
|
||||||
|
|
||||||
|
// 手动分页
|
||||||
|
int fromIndex = (pageNum - 1) * pageSize;
|
||||||
|
int toIndex = Math.min(fromIndex + pageSize, failures.size());
|
||||||
|
|
||||||
|
List<IntermediaryPersonImportFailureVO> pageData = failures.subList(fromIndex, toIndex);
|
||||||
|
|
||||||
|
return getDataTable(pageData, failures.size());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**新增5-6: 实体中介的状态和失败记录查询接口**
|
||||||
|
|
||||||
|
与个人中介完全对称,只需:
|
||||||
|
- URL中的`Person`改为`Entity`
|
||||||
|
- Service改为`entityImportService`
|
||||||
|
- VO改为`IntermediaryEntityImportFailureVO`
|
||||||
|
|
||||||
|
**接口路径对照表**:
|
||||||
|
|
||||||
|
| 功能 | 个人中介 | 实体中介 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| 导入数据 | `POST /importPersonData` | `POST /importEntityData` |
|
||||||
|
| 查询状态 | `GET /importPersonStatus/{taskId}` | `GET /importEntityStatus/{taskId}` |
|
||||||
|
| 查询失败 | `GET /importPersonFailures/{taskId}` | `GET /importEntityFailures/{taskId}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 前端改造
|
||||||
|
|
||||||
|
#### 文件1: API接口定义
|
||||||
|
|
||||||
|
**路径**: `ruoyi-ui/src/api/ccdiIntermediary.js`
|
||||||
|
|
||||||
|
**需要添加的方法**:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 查询个人中介导入状态
|
||||||
|
export function getPersonImportStatus(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/ccdi/intermediary/importPersonStatus/${taskId}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询个人中介导入失败记录
|
||||||
|
export function getPersonImportFailures(taskId, pageNum, pageSize) {
|
||||||
|
return request({
|
||||||
|
url: `/ccdi/intermediary/importPersonFailures/${taskId}`,
|
||||||
|
method: 'get',
|
||||||
|
params: { pageNum, pageSize }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询实体中介导入状态
|
||||||
|
export function getEntityImportStatus(taskId) {
|
||||||
|
return request({
|
||||||
|
url: `/ccdi/intermediary/importEntityStatus/${taskId}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询实体中介导入失败记录
|
||||||
|
export function getEntityImportFailures(taskId, pageNum, pageSize) {
|
||||||
|
return request({
|
||||||
|
url: `/ccdi/intermediary/importEntityFailures/${taskId}`,
|
||||||
|
method: 'get',
|
||||||
|
params: { pageNum, pageSize }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 文件2: ImportDialog.vue改造
|
||||||
|
|
||||||
|
**路径**: `ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue`
|
||||||
|
|
||||||
|
**需要添加的import**:
|
||||||
|
```javascript
|
||||||
|
import { getPersonImportStatus, getEntityImportStatus } from "@/api/ccdiIntermediary";
|
||||||
|
```
|
||||||
|
|
||||||
|
**data中添加的状态管理**:
|
||||||
|
```javascript
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// ...原有data
|
||||||
|
pollingTimer: null,
|
||||||
|
currentTaskId: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**修改handleFileSuccess方法**:
|
||||||
|
```javascript
|
||||||
|
handleFileSuccess(response) {
|
||||||
|
this.isUploading = false;
|
||||||
|
|
||||||
|
if (response.code === 200 && response.data && response.data.taskId) {
|
||||||
|
const taskId = response.data.taskId;
|
||||||
|
this.currentTaskId = taskId;
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
this.$notify({
|
||||||
|
title: '导入任务已提交',
|
||||||
|
message: '正在后台处理中,处理完成后将通知您',
|
||||||
|
type: 'info',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 关闭对话框
|
||||||
|
this.visible = false;
|
||||||
|
this.$refs.upload.clearFiles();
|
||||||
|
|
||||||
|
// 通知父组件刷新列表
|
||||||
|
this.$emit("success", taskId);
|
||||||
|
|
||||||
|
// 开始轮询
|
||||||
|
this.startImportStatusPolling(taskId);
|
||||||
|
} else {
|
||||||
|
this.$modal.msgError(response.msg || '导入失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**添加轮询方法**:
|
||||||
|
```javascript
|
||||||
|
methods: {
|
||||||
|
/** 开始轮询导入状态 */
|
||||||
|
startImportStatusPolling(taskId) {
|
||||||
|
let pollCount = 0;
|
||||||
|
const maxPolls = 150; // 最多5分钟
|
||||||
|
|
||||||
|
this.pollingTimer = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
pollCount++;
|
||||||
|
|
||||||
|
if (pollCount > maxPolls) {
|
||||||
|
clearInterval(this.pollingTimer);
|
||||||
|
this.$modal.msgWarning('导入任务处理超时,请联系管理员');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据导入类型调用不同的API
|
||||||
|
const apiMethod = this.formData.importType === 'person'
|
||||||
|
? getPersonImportStatus
|
||||||
|
: getEntityImportStatus;
|
||||||
|
|
||||||
|
const response = await apiMethod(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
|
||||||
|
});
|
||||||
|
} else if (statusResult.failureCount > 0) {
|
||||||
|
this.$notify({
|
||||||
|
title: '导入完成',
|
||||||
|
message: `成功${statusResult.successCount}条,失败${statusResult.failureCount}条`,
|
||||||
|
type: 'warning',
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知父组件更新失败记录状态
|
||||||
|
this.$emit("import-complete", {
|
||||||
|
taskId: statusResult.taskId,
|
||||||
|
hasFailures: statusResult.failureCount > 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组件销毁时清除定时器 */
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.pollingTimer) {
|
||||||
|
clearInterval(this.pollingTimer);
|
||||||
|
this.pollingTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 测试方案
|
||||||
|
|
||||||
|
### 4.1 功能测试用例
|
||||||
|
|
||||||
|
#### 测试用例1: 正常导入流程
|
||||||
|
|
||||||
|
**前置条件**:
|
||||||
|
- 准备包含10条个人中介数据的Excel文件
|
||||||
|
- 数据格式正确,所有必填字段都已填写
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 登录系统,进入中介管理页面
|
||||||
|
2. 点击"导入"按钮
|
||||||
|
3. 选择"个人中介"类型
|
||||||
|
4. 上传Excel文件,不勾选"更新已存在的数据"
|
||||||
|
5. 点击"开始导入"
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ 立即收到通知:"导入任务已提交,正在后台处理中"
|
||||||
|
- ✅ 导入对话框关闭
|
||||||
|
- ✅ 2-5秒后收到完成通知(根据数据量)
|
||||||
|
- ✅ 列表自动刷新,显示新导入的数据
|
||||||
|
- ✅ 如果全部成功,显示绿色通知:"全部成功!共导入10条数据"
|
||||||
|
|
||||||
|
#### 测试用例2: 数据验证失败
|
||||||
|
|
||||||
|
**前置条件**:
|
||||||
|
- 准备包含错误数据的Excel(如身份证号格式错误、姓名为空等)
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 重复测试用例1的步骤
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ 导入任务正常提交
|
||||||
|
- ✅ 完成后显示黄色通知:"成功X条,失败Y条"
|
||||||
|
- ✅ 页面出现"查看导入失败记录"按钮
|
||||||
|
- ✅ 点击按钮可以查看失败原因
|
||||||
|
- ✅ 失败记录包含:原数据行号、错误信息
|
||||||
|
|
||||||
|
#### 测试用例3: 更新模式
|
||||||
|
|
||||||
|
**前置条件**:
|
||||||
|
- 数据库中已存在某个证件号的中介记录
|
||||||
|
- Excel文件中包含相同证件号的数据,但其他字段不同
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 勾选"更新已存在的数据"
|
||||||
|
2. 上传Excel文件
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ 已存在的数据被更新
|
||||||
|
- ✅ 审计字段`updatedBy`正确记录当前用户
|
||||||
|
- ✅ `updateTime`更新为当前时间
|
||||||
|
|
||||||
|
#### 测试用例4: 实体中介导入
|
||||||
|
|
||||||
|
**前置条件**:
|
||||||
|
- 准备包含机构中介数据的Excel文件
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 选择"机构中介"类型
|
||||||
|
2. 上传Excel文件
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ 导入流程与个人中介一致
|
||||||
|
- ✅ Redis Key前缀为`import:intermediary-entity:`
|
||||||
|
- ✅ 数据正确插入`ccdi_enterprise_base_info`表
|
||||||
|
|
||||||
|
#### 测试用例5: 并发导入
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 打开两个浏览器标签页
|
||||||
|
2. 同时在不同标签页导入个人中介和实体中介
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ 两个导入任务互不影响
|
||||||
|
- ✅ 各自独立显示进度通知
|
||||||
|
- ✅ 都能正确完成
|
||||||
|
|
||||||
|
#### 测试用例6: 大数据量导入
|
||||||
|
|
||||||
|
**前置条件**:
|
||||||
|
- 准备包含1000条数据的Excel文件
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 上传大文件
|
||||||
|
2. 观察导入过程
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ 立即返回taskId,不阻塞
|
||||||
|
- ✅ 轮询查询能正确获取进度
|
||||||
|
- ✅ 最终完成并显示正确统计信息
|
||||||
|
|
||||||
|
### 4.2 性能测试
|
||||||
|
|
||||||
|
#### 性能指标
|
||||||
|
|
||||||
|
| 指标 | 目标值 |
|
||||||
|
|------|--------|
|
||||||
|
| 接口响应时间 | < 500ms (立即返回) |
|
||||||
|
| 轮询间隔 | 2秒 |
|
||||||
|
| 轮询超时 | 5分钟 (150次) |
|
||||||
|
| 单批导入大小 | 500条 |
|
||||||
|
| 支持最大文件 | 10MB |
|
||||||
|
| 并发导入任务 | 10个 |
|
||||||
|
|
||||||
|
#### 测试方法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用Apache Bench进行压力测试
|
||||||
|
ab -n 100 -c 10 -T "multipart/form-data; boundary=----WebKitFormBoundary" \
|
||||||
|
-p test_data.xlsx http://localhost:8080/ccdi/intermediary/importPersonData
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 部署与验证
|
||||||
|
|
||||||
|
### 5.1 部署步骤
|
||||||
|
|
||||||
|
1. **代码修改**
|
||||||
|
- 按照上述方案修改3个后端文件
|
||||||
|
- 修改2个前端文件
|
||||||
|
|
||||||
|
2. **编译打包**
|
||||||
|
```bash
|
||||||
|
# 后端
|
||||||
|
cd ruoyi-ccdi
|
||||||
|
mvn clean package
|
||||||
|
|
||||||
|
# 前端
|
||||||
|
cd ruoyi-ui
|
||||||
|
npm run build:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **重启服务**
|
||||||
|
```bash
|
||||||
|
# 停止现有服务
|
||||||
|
# 部署新的jar包
|
||||||
|
# 启动服务
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **验证部署**
|
||||||
|
- 访问Swagger文档: `http://localhost:8080/swagger-ui/index.html`
|
||||||
|
- 确认新的接口已正确注册
|
||||||
|
|
||||||
|
### 5.2 验证清单
|
||||||
|
|
||||||
|
- [ ] 个人中介导入接口返回taskId
|
||||||
|
- [ ] 实体中介导入接口返回taskId
|
||||||
|
- [ ] 轮询查询状态接口正常工作
|
||||||
|
- [ ] 失败记录查询接口返回正确数据
|
||||||
|
- [ ] 前端轮询机制正常
|
||||||
|
- [ ] 导入完成通知正确显示
|
||||||
|
- [ ] Redis状态正确设置和过期
|
||||||
|
- [ ] 审计字段正确记录操作人
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 风险与注意事项
|
||||||
|
|
||||||
|
### 6.1 潜在风险
|
||||||
|
|
||||||
|
| 风险项 | 影响 | 缓解措施 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| Redis服务故障 | 导入状态无法记录 | 确保Redis高可用,增加监控 |
|
||||||
|
| 异步任务执行失败 | 任务状态卡在PROCESSING | 增加超时机制和失败重试 |
|
||||||
|
| 并发量过大 | 系统资源耗尽 | 限制并发导入任务数 |
|
||||||
|
| 轮询频繁 | 服务器压力增大 | 合理设置轮询间隔(2秒) |
|
||||||
|
|
||||||
|
### 6.2 注意事项
|
||||||
|
|
||||||
|
1. **异步方法无法使用@Transactional**
|
||||||
|
- 异步Service中使用`@Transactional`会失效
|
||||||
|
- 需要在方法内部手动管理事务
|
||||||
|
|
||||||
|
2. **Redis数据过期**
|
||||||
|
- 7天后导入状态和失败记录会自动删除
|
||||||
|
- 用户需要及时查看失败记录
|
||||||
|
|
||||||
|
3. **userName参数**
|
||||||
|
- 中介实体需要记录`createdBy/updatedBy`
|
||||||
|
- 必须传递当前用户名给异步方法
|
||||||
|
|
||||||
|
4. **轮询超时处理**
|
||||||
|
- 最多轮询150次(5分钟)
|
||||||
|
- 超时后需要提示用户联系管理员
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 实施计划
|
||||||
|
|
||||||
|
### 7.1 任务分解
|
||||||
|
|
||||||
|
| 任务 | 负责人 | 预计时间 |
|
||||||
|
|------|--------|----------|
|
||||||
|
| 1. 后端Service层改造 | 后端开发 | 2小时 |
|
||||||
|
| 2. 后端Controller层改造 | 后端开发 | 1小时 |
|
||||||
|
| 3. 前端API接口定义 | 前端开发 | 0.5小时 |
|
||||||
|
| 4. 前端ImportDialog组件改造 | 前端开发 | 2小时 |
|
||||||
|
| 5. 单元测试 | 测试开发 | 2小时 |
|
||||||
|
| 6. 集成测试 | 测试开发 | 2小时 |
|
||||||
|
| 7. 文档更新 | 技术文档 | 1小时 |
|
||||||
|
|
||||||
|
**总计**: 约10.5小时
|
||||||
|
|
||||||
|
### 7.2 里程碑
|
||||||
|
|
||||||
|
- **T+0**: 完成设计文档
|
||||||
|
- **T+1天**: 完成后端代码改造和单元测试
|
||||||
|
- **T+2天**: 完成前端代码改造
|
||||||
|
- **T+3天**: 完成集成测试和部署
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 附录
|
||||||
|
|
||||||
|
### 8.1 相关文档
|
||||||
|
|
||||||
|
- [员工导入功能设计](../员工导入功能/)
|
||||||
|
- [MyBatis Plus批量操作文档](https://baomidou.com/pages/2976a3/)
|
||||||
|
- [Spring异步任务文档](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling)
|
||||||
|
|
||||||
|
### 8.2 参考代码
|
||||||
|
|
||||||
|
- **员工导入Controller**: `CcdiEmployeeController.java:136-191`
|
||||||
|
- **员工导入Service**: `CcdiEmployeeServiceImpl.java:186-208`
|
||||||
|
- **员工异步导入Service**: `CcdiEmployeeImportServiceImpl.java:43-109`
|
||||||
|
- **员工导入前端**: `ruoyi-ui/src/views/ccdiEmployee/index.vue`
|
||||||
|
|
||||||
|
### 8.3 数据字典
|
||||||
|
|
||||||
|
**导入状态枚举**:
|
||||||
|
|
||||||
|
| 状态值 | 说明 |
|
||||||
|
|--------|------|
|
||||||
|
| PROCESSING | 处理中 |
|
||||||
|
| SUCCESS | 全部成功 |
|
||||||
|
| PARTIAL_SUCCESS | 部分成功(有失败记录) |
|
||||||
|
|
||||||
|
**Redis Key设计**:
|
||||||
|
|
||||||
|
| 类型 | Key模式 | 过期时间 |
|
||||||
|
|------|---------|----------|
|
||||||
|
| 个人中介状态 | `import:intermediary:{taskId}` | 7天 |
|
||||||
|
| 个人中介失败 | `import:intermediary:{taskId}:failures` | 7天 |
|
||||||
|
| 实体中介状态 | `import:intermediary-entity:{taskId}` | 7天 |
|
||||||
|
| 实体中介失败 | `import:intermediary-entity:{taskId}:failures` | 7天 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档版本**: v1.0
|
||||||
|
**最后更新**: 2026-02-08
|
||||||
|
**文档状态**: 待审核
|
||||||
Binary file not shown.
Reference in New Issue
Block a user