1088 lines
34 KiB
Markdown
1088 lines
34 KiB
Markdown
|
|
# 中介库异步导入功能设计文档
|
||
|
|
|
||
|
|
**创建日期:** 2026-02-06
|
||
|
|
**设计目标:** 将中介库管理的文件导入功能改造为异步实现,完全复用员工信息/招聘信息异步导入的架构模式
|
||
|
|
**数据量预期:** 小批量(通常<500条)
|
||
|
|
**架构模式:** 拆分式设计(个人中介和实体中介分别实现)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 一、架构概述
|
||
|
|
|
||
|
|
### 1.1 核心架构
|
||
|
|
|
||
|
|
采用**拆分式设计**,为个人中介和实体中介分别创建独立的异步导入服务:
|
||
|
|
|
||
|
|
- **异步处理层**: 使用Spring `@Async`注解,通过现有的`importExecutor`线程池执行异步任务
|
||
|
|
- **状态存储层**: 使用Redis Hash存储导入状态,TTL为7天
|
||
|
|
- **失败记录层**: 使用Redis String存储失败记录,存储JSON数组
|
||
|
|
- **API层**: 为个人和实体分别提供三个接口(导入、状态查询、失败记录查询)
|
||
|
|
|
||
|
|
### 1.2 数据流程
|
||
|
|
|
||
|
|
```
|
||
|
|
前端上传Excel(个人/实体)
|
||
|
|
↓
|
||
|
|
Controller解析并立即返回taskId
|
||
|
|
↓
|
||
|
|
异步服务在后台处理:
|
||
|
|
1. 数据验证
|
||
|
|
2. 分类(新增/更新)
|
||
|
|
3. 批量操作
|
||
|
|
4. 保存结果到Redis
|
||
|
|
↓
|
||
|
|
前端每2秒轮询状态
|
||
|
|
↓
|
||
|
|
状态变为SUCCESS/PARTIAL_SUCCESS/FAILED
|
||
|
|
↓
|
||
|
|
如有失败,显示对应的"查看失败记录"按钮
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.3 Redis Key设计
|
||
|
|
|
||
|
|
**个人中介导入:**
|
||
|
|
- **状态Key**: `import:intermediary-person:{taskId}` (Hash结构)
|
||
|
|
- **失败记录Key**: `import:intermediary-person:{taskId}:failures` (String结构)
|
||
|
|
- **TTL**: 7天
|
||
|
|
|
||
|
|
**实体中介导入:**
|
||
|
|
- **状态Key**: `import:intermediary-entity:{taskId}` (Hash结构)
|
||
|
|
- **失败记录Key**: `import:intermediary-entity:{taskId}:failures` (String结构)
|
||
|
|
- **TTL**: 7天
|
||
|
|
|
||
|
|
### 1.4 状态枚举
|
||
|
|
|
||
|
|
| 状态值 | 说明 | 前端行为 |
|
||
|
|
|--------|------|----------|
|
||
|
|
| PROCESSING | 处理中 | 继续轮询 |
|
||
|
|
| SUCCESS | 全部成功 | 显示成功通知,刷新列表 |
|
||
|
|
| PARTIAL_SUCCESS | 部分成功 | 显示警告通知,显示失败按钮 |
|
||
|
|
| FAILED | 全部失败 | 显示错误通知,显示失败按钮 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 二、后端组件设计
|
||
|
|
|
||
|
|
### 2.1 VO类设计
|
||
|
|
|
||
|
|
#### 2.1.1 IntermediaryPersonImportFailureVO (个人中介失败记录)
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Data
|
||
|
|
@Schema(description = "个人中介导入失败记录")
|
||
|
|
public class IntermediaryPersonImportFailureVO {
|
||
|
|
|
||
|
|
@Schema(description = "姓名")
|
||
|
|
private String name;
|
||
|
|
|
||
|
|
@Schema(description = "证件号码")
|
||
|
|
private String personId;
|
||
|
|
|
||
|
|
@Schema(description = "人员类型")
|
||
|
|
private String personType;
|
||
|
|
|
||
|
|
@Schema(description = "性别")
|
||
|
|
private String gender;
|
||
|
|
|
||
|
|
@Schema(description = "手机号码")
|
||
|
|
private String mobile;
|
||
|
|
|
||
|
|
@Schema(description = "所在公司")
|
||
|
|
private String company;
|
||
|
|
|
||
|
|
@Schema(description = "错误信息")
|
||
|
|
private String errorMessage;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.1.2 IntermediaryEntityImportFailureVO (实体中介失败记录)
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Data
|
||
|
|
@Schema(description = "实体中介导入失败记录")
|
||
|
|
public class IntermediaryEntityImportFailureVO {
|
||
|
|
|
||
|
|
@Schema(description = "机构名称")
|
||
|
|
private String enterpriseName;
|
||
|
|
|
||
|
|
@Schema(description = "统一社会信用代码")
|
||
|
|
private String socialCreditCode;
|
||
|
|
|
||
|
|
@Schema(description = "主体类型")
|
||
|
|
private String enterpriseType;
|
||
|
|
|
||
|
|
@Schema(description = "企业性质")
|
||
|
|
private String enterpriseNature;
|
||
|
|
|
||
|
|
@Schema(description = "法定代表人")
|
||
|
|
private String legalRepresentative;
|
||
|
|
|
||
|
|
@Schema(description = "成立日期")
|
||
|
|
private Date establishDate;
|
||
|
|
|
||
|
|
@Schema(description = "错误信息")
|
||
|
|
private String errorMessage;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.1.3 复用VO类
|
||
|
|
|
||
|
|
- `ImportResultVO` - 导入结果VO(复用员工导入)
|
||
|
|
- `ImportStatusVO` - 导入状态VO(复用员工导入)
|
||
|
|
|
||
|
|
### 2.2 Service层设计
|
||
|
|
|
||
|
|
#### 2.2.1 个人中介导入Service接口
|
||
|
|
|
||
|
|
```java
|
||
|
|
public interface ICcdiIntermediaryPersonImportService {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 异步导入个人中介数据
|
||
|
|
*
|
||
|
|
* @param excelList Excel数据列表
|
||
|
|
* @param isUpdateSupport 是否更新已存在的数据
|
||
|
|
* @param taskId 任务ID
|
||
|
|
* @param userName 当前用户名(用于审计字段)
|
||
|
|
*/
|
||
|
|
void importPersonAsync(List<CcdiIntermediaryPersonExcel> excelList,
|
||
|
|
Boolean isUpdateSupport,
|
||
|
|
String taskId,
|
||
|
|
String userName);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 查询导入状态
|
||
|
|
*
|
||
|
|
* @param taskId 任务ID
|
||
|
|
* @return 导入状态信息
|
||
|
|
*/
|
||
|
|
ImportStatusVO getImportStatus(String taskId);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取导入失败记录
|
||
|
|
*
|
||
|
|
* @param taskId 任务ID
|
||
|
|
* @return 失败记录列表
|
||
|
|
*/
|
||
|
|
List<IntermediaryPersonImportFailureVO> getImportFailures(String taskId);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.2.2 实体中介导入Service接口
|
||
|
|
|
||
|
|
```java
|
||
|
|
public interface ICcdiIntermediaryEntityImportService {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 异步导入实体中介数据
|
||
|
|
*
|
||
|
|
* @param excelList Excel数据列表
|
||
|
|
* @param isUpdateSupport 是否更新已存在的数据
|
||
|
|
* @param taskId 任务ID
|
||
|
|
* @param userName 当前用户名(用于审计字段)
|
||
|
|
*/
|
||
|
|
void importEntityAsync(List<CcdiIntermediaryEntityExcel> excelList,
|
||
|
|
Boolean isUpdateSupport,
|
||
|
|
String taskId,
|
||
|
|
String userName);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 查询导入状态
|
||
|
|
*
|
||
|
|
* @param taskId 任务ID
|
||
|
|
* @return 导入状态信息
|
||
|
|
*/
|
||
|
|
ImportStatusVO getImportStatus(String taskId);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 获取导入失败记录
|
||
|
|
*
|
||
|
|
* @param taskId 任务ID
|
||
|
|
* @return 失败记录列表
|
||
|
|
*/
|
||
|
|
List<IntermediaryEntityImportFailureVO> getImportFailures(String taskId);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.2.3 实现类核心逻辑
|
||
|
|
|
||
|
|
**个人中介导入实现:**
|
||
|
|
```java
|
||
|
|
@Service
|
||
|
|
@EnableAsync
|
||
|
|
public class CcdiIntermediaryPersonImportServiceImpl
|
||
|
|
implements ICcdiIntermediaryPersonImportService {
|
||
|
|
|
||
|
|
@Resource
|
||
|
|
private CcdiBizIntermediaryMapper intermediaryMapper;
|
||
|
|
|
||
|
|
@Resource
|
||
|
|
private RedisTemplate<String, Object> redisTemplate;
|
||
|
|
|
||
|
|
@Override
|
||
|
|
@Async
|
||
|
|
@Transactional
|
||
|
|
public void importPersonAsync(List<CcdiIntermediaryPersonExcel> excelList,
|
||
|
|
Boolean isUpdateSupport,
|
||
|
|
String taskId,
|
||
|
|
String userName) {
|
||
|
|
List<CcdiBizIntermediary> newRecords = new ArrayList<>();
|
||
|
|
List<CcdiBizIntermediary> updateRecords = new ArrayList<>();
|
||
|
|
List<IntermediaryPersonImportFailureVO> failures = new ArrayList<>();
|
||
|
|
|
||
|
|
// 1. 批量查询已存在的证件号
|
||
|
|
Set<String> existingPersonIds = getExistingPersonIds(excelList);
|
||
|
|
|
||
|
|
// 2. 分类数据
|
||
|
|
for (CcdiIntermediaryPersonExcel excel : excelList) {
|
||
|
|
try {
|
||
|
|
// 验证必填字段
|
||
|
|
if (StringUtils.isEmpty(excel.getName())) {
|
||
|
|
throw new RuntimeException("姓名不能为空");
|
||
|
|
}
|
||
|
|
if (StringUtils.isEmpty(excel.getPersonId())) {
|
||
|
|
throw new RuntimeException("证件号码不能为空");
|
||
|
|
}
|
||
|
|
|
||
|
|
CcdiBizIntermediary person = new CcdiBizIntermediary();
|
||
|
|
BeanUtils.copyProperties(excel, person);
|
||
|
|
person.setPersonType("中介");
|
||
|
|
person.setDataSource("IMPORT");
|
||
|
|
|
||
|
|
if (existingPersonIds.contains(excel.getPersonId())) {
|
||
|
|
if (isUpdateSupport) {
|
||
|
|
person.setUpdateBy(userName);
|
||
|
|
updateRecords.add(person);
|
||
|
|
} else {
|
||
|
|
throw new RuntimeException("该证件号已存在");
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
person.setCreateBy(userName);
|
||
|
|
person.setUpdateBy(userName);
|
||
|
|
newRecords.add(person);
|
||
|
|
}
|
||
|
|
} catch (Exception e) {
|
||
|
|
IntermediaryPersonImportFailureVO failure = new IntermediaryPersonImportFailureVO();
|
||
|
|
BeanUtils.copyProperties(excel, failure);
|
||
|
|
failure.setErrorMessage(e.getMessage());
|
||
|
|
failures.add(failure);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. 批量插入
|
||
|
|
if (!newRecords.isEmpty()) {
|
||
|
|
intermediaryMapper.insertBatch(newRecords);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. 批量更新
|
||
|
|
if (!updateRecords.isEmpty()) {
|
||
|
|
intermediaryMapper.updateBatch(updateRecords);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 5. 保存失败记录到Redis
|
||
|
|
if (!failures.isEmpty()) {
|
||
|
|
String failuresKey = "import:intermediary-person:" + taskId + ":failures";
|
||
|
|
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 6. 更新最终状态
|
||
|
|
ImportResult result = new ImportResult();
|
||
|
|
result.setTotalCount(excelList.size());
|
||
|
|
result.setSuccessCount(newRecords.size() + updateRecords.size());
|
||
|
|
result.setFailureCount(failures.size());
|
||
|
|
|
||
|
|
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
|
||
|
|
updateImportStatus("intermediary-person", taskId, finalStatus, result);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**实体中介导入实现:**
|
||
|
|
- 与个人中介类似
|
||
|
|
- 使用`CcdiEnterpriseBaseInfo`实体
|
||
|
|
- 唯一键是`socialCreditCode`
|
||
|
|
- Redis Key使用`intermediary-entity`
|
||
|
|
|
||
|
|
### 2.3 Controller层设计
|
||
|
|
|
||
|
|
#### 2.3.1 修改个人中介导入接口
|
||
|
|
|
||
|
|
```java
|
||
|
|
@Resource
|
||
|
|
private ICcdiIntermediaryPersonImportService personImportService;
|
||
|
|
|
||
|
|
@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("至少需要一条数据");
|
||
|
|
}
|
||
|
|
|
||
|
|
// 生成任务ID
|
||
|
|
String taskId = UUID.randomUUID().toString();
|
||
|
|
|
||
|
|
// 获取当前用户名
|
||
|
|
String userName = getUsername();
|
||
|
|
|
||
|
|
// 初始化导入状态到Redis
|
||
|
|
initImportStatus("intermediary-person", taskId, list.size());
|
||
|
|
|
||
|
|
// 提交异步任务
|
||
|
|
personImportService.importPersonAsync(list, updateSupport, taskId, userName);
|
||
|
|
|
||
|
|
// 立即返回,不等待后台任务完成
|
||
|
|
ImportResultVO result = new ImportResultVO();
|
||
|
|
result.setTaskId(taskId);
|
||
|
|
result.setStatus("PROCESSING");
|
||
|
|
result.setMessage("导入任务已提交,正在后台处理");
|
||
|
|
|
||
|
|
return AjaxResult.success("导入任务已提交,正在后台处理", result);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.3.2 新增个人中介状态查询接口
|
||
|
|
|
||
|
|
```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());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.3.3 新增个人中介失败记录查询接口
|
||
|
|
|
||
|
|
```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());
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.3.4 实体中介相关接口
|
||
|
|
|
||
|
|
类似地,为实体中介添加三个接口:
|
||
|
|
- `/importEntityData` - 导入接口
|
||
|
|
- `/importEntityStatus/{taskId}` - 状态查询接口
|
||
|
|
- `/importEntityFailures/{taskId}` - 失败记录查询接口
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 三、前端实现设计
|
||
|
|
|
||
|
|
### 3.1 API定义
|
||
|
|
|
||
|
|
在 `ruoyi-ui/src/api/ccdiIntermediary.js` 中添加:
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// 查询个人中介导入状态
|
||
|
|
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 }
|
||
|
|
})
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 Vue组件修改
|
||
|
|
|
||
|
|
#### 3.2.1 新增data属性
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
data() {
|
||
|
|
return {
|
||
|
|
// ...现有data
|
||
|
|
|
||
|
|
// 个人中介导入相关
|
||
|
|
personPollingTimer: null,
|
||
|
|
personShowFailureButton: false,
|
||
|
|
personCurrentTaskId: null,
|
||
|
|
personFailureDialogVisible: false,
|
||
|
|
personFailureList: [],
|
||
|
|
personFailureLoading: false,
|
||
|
|
personFailureTotal: 0,
|
||
|
|
personFailureQueryParams: {
|
||
|
|
pageNum: 1,
|
||
|
|
pageSize: 10
|
||
|
|
},
|
||
|
|
|
||
|
|
// 实体中介导入相关
|
||
|
|
entityPollingTimer: null,
|
||
|
|
entityShowFailureButton: false,
|
||
|
|
entityCurrentTaskId: null,
|
||
|
|
entityFailureDialogVisible: false,
|
||
|
|
entityFailureList: [],
|
||
|
|
entityFailureLoading: false,
|
||
|
|
entityFailureTotal: 0,
|
||
|
|
entityFailureQueryParams: {
|
||
|
|
pageNum: 1,
|
||
|
|
pageSize: 10
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 3.2.2 修改handleFileSuccess方法
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
handleFileSuccess(response, file, fileList) {
|
||
|
|
this.upload.isUploading = false;
|
||
|
|
this.upload.open = false;
|
||
|
|
|
||
|
|
if (response.code === 200) {
|
||
|
|
const taskId = response.data.taskId;
|
||
|
|
const importType = this.upload.importType; // 'person' 或 'entity'
|
||
|
|
|
||
|
|
// 显示后台处理提示
|
||
|
|
const typeName = importType === 'person' ? '个人中介' : '实体中介';
|
||
|
|
this.$notify({
|
||
|
|
title: '导入任务已提交',
|
||
|
|
message: `${typeName}数据正在后台处理中,处理完成后将通知您`,
|
||
|
|
type: 'info',
|
||
|
|
duration: 3000
|
||
|
|
});
|
||
|
|
|
||
|
|
// 根据类型开始轮询
|
||
|
|
if (importType === 'person') {
|
||
|
|
this.startPersonImportPolling(taskId);
|
||
|
|
} else {
|
||
|
|
this.startEntityImportPolling(taskId);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
this.$modal.msgError(response.msg);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 3.2.3 轮询方法
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
methods: {
|
||
|
|
// 个人中介轮询
|
||
|
|
startPersonImportPolling(taskId) {
|
||
|
|
this.personPollingTimer = setInterval(async () => {
|
||
|
|
try {
|
||
|
|
const response = await getPersonImportStatus(taskId);
|
||
|
|
|
||
|
|
if (response.data && response.data.status !== 'PROCESSING') {
|
||
|
|
clearInterval(this.personPollingTimer);
|
||
|
|
this.handlePersonImportComplete(response.data);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
clearInterval(this.personPollingTimer);
|
||
|
|
this.$modal.msgError('查询导入状态失败: ' + error.message);
|
||
|
|
}
|
||
|
|
}, 2000);
|
||
|
|
},
|
||
|
|
|
||
|
|
// 实体中介轮询
|
||
|
|
startEntityImportPolling(taskId) {
|
||
|
|
this.entityPollingTimer = setInterval(async () => {
|
||
|
|
try {
|
||
|
|
const response = await getEntityImportStatus(taskId);
|
||
|
|
|
||
|
|
if (response.data && response.data.status !== 'PROCESSING') {
|
||
|
|
clearInterval(this.entityPollingTimer);
|
||
|
|
this.handleEntityImportComplete(response.data);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
clearInterval(this.entityPollingTimer);
|
||
|
|
this.$modal.msgError('查询导入状态失败: ' + error.message);
|
||
|
|
}
|
||
|
|
}, 2000);
|
||
|
|
},
|
||
|
|
|
||
|
|
// 个人中介导入完成处理
|
||
|
|
handlePersonImportComplete(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.personShowFailureButton = true;
|
||
|
|
this.personCurrentTaskId = statusResult.taskId;
|
||
|
|
this.getList();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
// 实体中介导入完成处理
|
||
|
|
handleEntityImportComplete(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.entityShowFailureButton = true;
|
||
|
|
this.entityCurrentTaskId = statusResult.taskId;
|
||
|
|
this.getList();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 3.2.4 生命周期销毁钩子
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
beforeDestroy() {
|
||
|
|
// 清除个人中介轮询定时器
|
||
|
|
if (this.personPollingTimer) {
|
||
|
|
clearInterval(this.personPollingTimer);
|
||
|
|
this.personPollingTimer = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 清除实体中介轮询定时器
|
||
|
|
if (this.entityPollingTimer) {
|
||
|
|
clearInterval(this.entityPollingTimer);
|
||
|
|
this.entityPollingTimer = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.3 UI组件设计
|
||
|
|
|
||
|
|
#### 3.3.1 查看失败记录按钮
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<!-- 个人中介导入失败记录按钮 -->
|
||
|
|
<el-col :span="1.5" v-if="personShowFailureButton">
|
||
|
|
<el-button
|
||
|
|
type="warning"
|
||
|
|
plain
|
||
|
|
icon="el-icon-warning"
|
||
|
|
size="mini"
|
||
|
|
@click="viewPersonImportFailures"
|
||
|
|
>查看个人中介导入失败记录</el-button>
|
||
|
|
</el-col>
|
||
|
|
|
||
|
|
<!-- 实体中介导入失败记录按钮 -->
|
||
|
|
<el-col :span="1.5" v-if="entityShowFailureButton">
|
||
|
|
<el-button
|
||
|
|
type="warning"
|
||
|
|
plain
|
||
|
|
icon="el-icon-warning"
|
||
|
|
size="mini"
|
||
|
|
@click="viewEntityImportFailures"
|
||
|
|
>查看实体中介导入失败记录</el-button>
|
||
|
|
</el-col>
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 3.3.2 个人中介失败记录对话框
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<el-dialog
|
||
|
|
title="个人中介导入失败记录"
|
||
|
|
:visible.sync="personFailureDialogVisible"
|
||
|
|
width="1200px"
|
||
|
|
append-to-body
|
||
|
|
>
|
||
|
|
<el-table :data="personFailureList" v-loading="personFailureLoading">
|
||
|
|
<el-table-column label="姓名" prop="name" align="center" width="120" />
|
||
|
|
<el-table-column label="证件号码" prop="personId" align="center" width="180" />
|
||
|
|
<el-table-column label="人员类型" prop="personType" align="center" width="120" />
|
||
|
|
<el-table-column label="性别" prop="gender" align="center" width="80" />
|
||
|
|
<el-table-column label="手机号码" prop="mobile" align="center" width="130" />
|
||
|
|
<el-table-column label="所在公司" prop="company" align="center" width="150" />
|
||
|
|
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
|
||
|
|
</el-table>
|
||
|
|
|
||
|
|
<pagination
|
||
|
|
v-show="personFailureTotal > 0"
|
||
|
|
:total="personFailureTotal"
|
||
|
|
:page.sync="personFailureQueryParams.pageNum"
|
||
|
|
:limit.sync="personFailureQueryParams.pageSize"
|
||
|
|
@pagination="getPersonFailureList"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<div slot="footer" class="dialog-footer">
|
||
|
|
<el-button @click="personFailureDialogVisible = false">关闭</el-button>
|
||
|
|
</div>
|
||
|
|
</el-dialog>
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 3.3.3 实体中介失败记录对话框
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<el-dialog
|
||
|
|
title="实体中介导入失败记录"
|
||
|
|
:visible.sync="entityFailureDialogVisible"
|
||
|
|
width="1200px"
|
||
|
|
append-to-body
|
||
|
|
>
|
||
|
|
<el-table :data="entityFailureList" v-loading="entityFailureLoading">
|
||
|
|
<el-table-column label="机构名称" prop="enterpriseName" align="center" width="200" />
|
||
|
|
<el-table-column label="统一社会信用代码" prop="socialCreditCode" align="center" width="180" />
|
||
|
|
<el-table-column label="主体类型" prop="enterpriseType" align="center" width="120" />
|
||
|
|
<el-table-column label="企业性质" prop="enterpriseNature" align="center" width="120" />
|
||
|
|
<el-table-column label="法定代表人" prop="legalRepresentative" align="center" width="120" />
|
||
|
|
<el-table-column label="成立日期" prop="establishDate" align="center" width="120" />
|
||
|
|
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
|
||
|
|
</el-table>
|
||
|
|
|
||
|
|
<pagination
|
||
|
|
v-show="entityFailureTotal > 0"
|
||
|
|
:total="entityFailureTotal"
|
||
|
|
:page.sync="entityFailureQueryParams.pageNum"
|
||
|
|
:limit.sync="entityFailureQueryParams.pageSize"
|
||
|
|
@pagination="getEntityFailureList"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<div slot="footer" class="dialog-footer">
|
||
|
|
<el-button @click="entityFailureDialogVisible = false">关闭</el-button>
|
||
|
|
</div>
|
||
|
|
</el-dialog>
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 3.3.4 查询失败记录方法
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
methods: {
|
||
|
|
// 查看个人中介导入失败记录
|
||
|
|
viewPersonImportFailures() {
|
||
|
|
this.personFailureDialogVisible = true;
|
||
|
|
this.getPersonFailureList();
|
||
|
|
},
|
||
|
|
|
||
|
|
// 获取个人中介失败记录列表
|
||
|
|
getPersonFailureList() {
|
||
|
|
this.personFailureLoading = true;
|
||
|
|
getPersonImportFailures(
|
||
|
|
this.personCurrentTaskId,
|
||
|
|
this.personFailureQueryParams.pageNum,
|
||
|
|
this.personFailureQueryParams.pageSize
|
||
|
|
).then(response => {
|
||
|
|
this.personFailureList = response.rows;
|
||
|
|
this.personFailureTotal = response.total;
|
||
|
|
this.personFailureLoading = false;
|
||
|
|
}).catch(error => {
|
||
|
|
this.personFailureLoading = false;
|
||
|
|
this.$modal.msgError('查询失败记录失败: ' + error.message);
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
// 查看实体中介导入失败记录
|
||
|
|
viewEntityImportFailures() {
|
||
|
|
this.entityFailureDialogVisible = true;
|
||
|
|
this.getEntityFailureList();
|
||
|
|
},
|
||
|
|
|
||
|
|
// 获取实体中介失败记录列表
|
||
|
|
getEntityFailureList() {
|
||
|
|
this.entityFailureLoading = true;
|
||
|
|
getEntityImportFailures(
|
||
|
|
this.entityCurrentTaskId,
|
||
|
|
this.entityFailureQueryParams.pageNum,
|
||
|
|
this.entityFailureQueryParams.pageSize
|
||
|
|
).then(response => {
|
||
|
|
this.entityFailureList = response.rows;
|
||
|
|
this.entityFailureTotal = response.total;
|
||
|
|
this.entityFailureLoading = false;
|
||
|
|
}).catch(error => {
|
||
|
|
this.entityFailureLoading = false;
|
||
|
|
this.$modal.msgError('查询失败记录失败: ' + error.message);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 四、数据验证与错误处理
|
||
|
|
|
||
|
|
### 4.1 个人中介数据验证规则
|
||
|
|
|
||
|
|
#### 4.1.1 必填字段验证
|
||
|
|
|
||
|
|
- 姓名 (`name`)
|
||
|
|
- 证件号码 (`personId`)
|
||
|
|
|
||
|
|
#### 4.1.2 唯一性验证
|
||
|
|
|
||
|
|
- 证件号码(`personId`)必须唯一
|
||
|
|
- 批量查询已存在的证件号:
|
||
|
|
```java
|
||
|
|
private Set<String> getExistingPersonIds(List<CcdiIntermediaryPersonExcel> excelList) {
|
||
|
|
List<String> personIds = excelList.stream()
|
||
|
|
.map(CcdiIntermediaryPersonExcel::getPersonId)
|
||
|
|
.filter(StringUtils::isNotEmpty)
|
||
|
|
.collect(Collectors.toList());
|
||
|
|
|
||
|
|
if (personIds.isEmpty()) {
|
||
|
|
return Collections.emptySet();
|
||
|
|
}
|
||
|
|
|
||
|
|
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
|
||
|
|
wrapper.in(CcdiBizIntermediary::getPersonId, personIds);
|
||
|
|
List<CcdiBizIntermediary> existingList = intermediaryMapper.selectList(wrapper);
|
||
|
|
|
||
|
|
return existingList.stream()
|
||
|
|
.map(CcdiBizIntermediary::getPersonId)
|
||
|
|
.collect(Collectors.toSet());
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.2 实体中介数据验证规则
|
||
|
|
|
||
|
|
#### 4.2.1 必填字段验证
|
||
|
|
|
||
|
|
- 机构名称 (`enterpriseName`)
|
||
|
|
|
||
|
|
#### 4.2.2 唯一性验证
|
||
|
|
|
||
|
|
- 统一社会信用代码(`socialCreditCode`)必须唯一(如果不为空)
|
||
|
|
- 批量查询已存在的统一社会信用代码:
|
||
|
|
```java
|
||
|
|
private Set<String> getExistingSocialCreditCodes(List<CcdiIntermediaryEntityExcel> excelList) {
|
||
|
|
List<String> codes = excelList.stream()
|
||
|
|
.map(CcdiIntermediaryEntityExcel::getSocialCreditCode)
|
||
|
|
.filter(StringUtils::isNotEmpty)
|
||
|
|
.collect(Collectors.toList());
|
||
|
|
|
||
|
|
if (codes.isEmpty()) {
|
||
|
|
return Collections.emptySet();
|
||
|
|
}
|
||
|
|
|
||
|
|
LambdaQueryWrapper<CcdiEnterpriseBaseInfo> wrapper = new LambdaQueryWrapper<>();
|
||
|
|
wrapper.in(CcdiEnterpriseBaseInfo::getSocialCreditCode, codes);
|
||
|
|
List<CcdiEnterpriseBaseInfo> existingList = enterpriseMapper.selectList(wrapper);
|
||
|
|
|
||
|
|
return existingList.stream()
|
||
|
|
.map(CcdiEnterpriseBaseInfo::getSocialCreditCode)
|
||
|
|
.collect(Collectors.toSet());
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.3 错误处理流程
|
||
|
|
|
||
|
|
#### 4.3.1 单条数据错误
|
||
|
|
|
||
|
|
```java
|
||
|
|
try {
|
||
|
|
// 验证和处理数据
|
||
|
|
} catch (Exception e) {
|
||
|
|
IntermediaryPersonImportFailureVO failure = new IntermediaryPersonImportFailureVO();
|
||
|
|
BeanUtils.copyProperties(excel, failure);
|
||
|
|
failure.setErrorMessage(e.getMessage());
|
||
|
|
failures.add(failure);
|
||
|
|
// 继续处理下一条数据
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 4.3.2 状态更新逻辑
|
||
|
|
|
||
|
|
```java
|
||
|
|
private void updateImportStatus(String taskType, String taskId, String status, ImportResult result) {
|
||
|
|
String key = "import:" + taskType + ":" + 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);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 五、文件清单
|
||
|
|
|
||
|
|
### 5.1 新增文件
|
||
|
|
|
||
|
|
| 文件路径 | 说明 |
|
||
|
|
|---------|------|
|
||
|
|
| `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/IntermediaryPersonImportFailureVO.java` | 个人中介导入失败记录VO |
|
||
|
|
| `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/IntermediaryEntityImportFailureVO.java` | 实体中介导入失败记录VO |
|
||
|
|
| `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryPersonImportService.java` | 个人中介异步导入Service接口 |
|
||
|
|
| `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryEntityImportService.java` | 实体中介异步导入Service接口 |
|
||
|
|
| `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryPersonImportServiceImpl.java` | 个人中介异步导入Service实现 |
|
||
|
|
| `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryEntityImportServiceImpl.java` | 实体中介异步导入Service实现 |
|
||
|
|
| `test/test_intermediary_import.py` | 测试脚本 |
|
||
|
|
|
||
|
|
### 5.2 修改文件
|
||
|
|
|
||
|
|
| 文件路径 | 修改内容 |
|
||
|
|
|---------|---------|
|
||
|
|
| `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java` | 修改导入接口,添加状态查询和失败记录查询接口(个人+实体共6个接口) |
|
||
|
|
| `ruoyi-ui/src/api/ccdiIntermediary.js` | 添加导入状态和失败记录查询API(4个新方法) |
|
||
|
|
| `ruoyi-ui/src/views/ccdiIntermediary/index.vue` | 添加轮询逻辑、失败记录UI(两套独立组件) |
|
||
|
|
| `doc/api/ccdi_intermediary_api.md` | 更新API文档(新增导入相关接口文档) |
|
||
|
|
|
||
|
|
### 5.3 复用组件
|
||
|
|
|
||
|
|
| 组件 | 说明 |
|
||
|
|
|------|------|
|
||
|
|
| `ImportResultVO` | 导入结果VO(复用员工导入) |
|
||
|
|
| `ImportStatusVO` | 导入状态VO(复用员工导入) |
|
||
|
|
| `AsyncConfig` | 异步配置(复用员工导入) |
|
||
|
|
| `importExecutor` | 导入任务线程池(复用员工导入) |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 六、实施步骤
|
||
|
|
|
||
|
|
### 6.1 后端实施步骤
|
||
|
|
|
||
|
|
#### 步骤1: 创建失败记录VO类
|
||
|
|
|
||
|
|
**文件:**
|
||
|
|
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/IntermediaryPersonImportFailureVO.java`
|
||
|
|
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/IntermediaryEntityImportFailureVO.java`
|
||
|
|
|
||
|
|
#### 步骤2: 创建Service接口
|
||
|
|
|
||
|
|
**文件:**
|
||
|
|
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryPersonImportService.java`
|
||
|
|
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/ICcdiIntermediaryEntityImportService.java`
|
||
|
|
|
||
|
|
#### 步骤3: 实现Service
|
||
|
|
|
||
|
|
**文件:**
|
||
|
|
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryPersonImportServiceImpl.java`
|
||
|
|
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryEntityImportServiceImpl.java`
|
||
|
|
|
||
|
|
**操作:**
|
||
|
|
- 实现`ICcdiIntermediaryPersonImportService`接口
|
||
|
|
- 实现`ICcdiIntermediaryEntityImportService`接口
|
||
|
|
- 添加`@EnableAsync`注解
|
||
|
|
- 注入Mapper和RedisTemplate
|
||
|
|
- 实现异步导入逻辑(包含userName审计字段)
|
||
|
|
- 实现状态查询逻辑
|
||
|
|
- 实现失败记录查询逻辑
|
||
|
|
|
||
|
|
#### 步骤4: 修改Controller
|
||
|
|
|
||
|
|
**文件:**
|
||
|
|
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java`
|
||
|
|
|
||
|
|
**操作:**
|
||
|
|
- 注入两个导入Service
|
||
|
|
- 修改`importPersonData()`方法(改为异步)
|
||
|
|
- 修改`importEntityData()`方法(改为异步)
|
||
|
|
- 添加`getPersonImportStatus()`方法
|
||
|
|
- 添加`getPersonImportFailures()`方法
|
||
|
|
- 添加`getEntityImportStatus()`方法
|
||
|
|
- 添加`getEntityImportFailures()`方法
|
||
|
|
- 添加Swagger注解
|
||
|
|
|
||
|
|
### 6.2 前端实施步骤
|
||
|
|
|
||
|
|
#### 步骤5: 修改API定义
|
||
|
|
|
||
|
|
**文件:**
|
||
|
|
- `ruoyi-ui/src/api/ccdiIntermediary.js`
|
||
|
|
|
||
|
|
**操作:**
|
||
|
|
- 添加`getPersonImportStatus()`方法
|
||
|
|
- 添加`getPersonImportFailures()`方法
|
||
|
|
- 添加`getEntityImportStatus()`方法
|
||
|
|
- 添加`getEntityImportFailures()`方法
|
||
|
|
|
||
|
|
#### 步骤6: 修改Vue组件
|
||
|
|
|
||
|
|
**文件:**
|
||
|
|
- `ruoyi-ui/src/views/ccdiIntermediary/index.vue`
|
||
|
|
|
||
|
|
**操作:**
|
||
|
|
- 添加data属性(个人+实体两套)
|
||
|
|
- 修改`handleFileSuccess()`方法
|
||
|
|
- 添加轮询方法(个人+实体)
|
||
|
|
- 添加完成处理方法(个人+实体)
|
||
|
|
- 添加失败记录查询方法(个人+实体)
|
||
|
|
- 添加`beforeDestroy()`生命周期钩子
|
||
|
|
- 添加两个"查看失败记录"按钮
|
||
|
|
- 添加两个失败记录对话框
|
||
|
|
|
||
|
|
### 6.3 测试与文档
|
||
|
|
|
||
|
|
#### 步骤7: 生成测试脚本
|
||
|
|
|
||
|
|
**文件:**
|
||
|
|
- `test/test_intermediary_import.py`
|
||
|
|
|
||
|
|
**操作:**
|
||
|
|
- 编写测试脚本
|
||
|
|
- 包含:登录、个人/实体导入、状态查询、失败记录查询等测试用例
|
||
|
|
|
||
|
|
#### 步骤8: 手动测试
|
||
|
|
|
||
|
|
**操作:**
|
||
|
|
- 测试个人中介导入(全部成功、部分失败)
|
||
|
|
- 测试实体中介导入(全部成功、部分失败)
|
||
|
|
- 测试轮询机制
|
||
|
|
- 测试失败记录UI
|
||
|
|
|
||
|
|
#### 步骤9: 更新API文档
|
||
|
|
|
||
|
|
**文件:**
|
||
|
|
- `doc/api/ccdi_intermediary_api.md`
|
||
|
|
|
||
|
|
**操作:**
|
||
|
|
- 添加导入相关接口文档
|
||
|
|
- 包含:请求参数、响应示例、错误码说明
|
||
|
|
|
||
|
|
#### 步骤10: 代码提交
|
||
|
|
|
||
|
|
**操作:**
|
||
|
|
```bash
|
||
|
|
git add .
|
||
|
|
git commit -m "feat: 实现中介库异步导入功能"
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 七、测试计划
|
||
|
|
|
||
|
|
### 7.1 功能测试
|
||
|
|
|
||
|
|
| 测试项 | 测试内容 | 预期结果 |
|
||
|
|
|--------|---------|---------|
|
||
|
|
| 个人中介-正常导入 | 导入100-500条有效个人中介数据 | 全部成功,状态为SUCCESS |
|
||
|
|
| 个人中介-重复导入不更新 | personId已存在,updateSupport=false | 导入失败,提示"该证件号已存在" |
|
||
|
|
| 个人中介-重复导入更新 | personId已存在,updateSupport=true | 更新已有数据,状态为SUCCESS |
|
||
|
|
| 个人中介-部分错误 | 混合有效数据和无效数据 | 部分成功,状态为PARTIAL_SUCCESS |
|
||
|
|
| 实体中介-正常导入 | 导入100-500条有效实体中介数据 | 全部成功,状态为SUCCESS |
|
||
|
|
| 实体中介-重复导入不更新 | socialCreditCode已存在,updateSupport=false | 导入失败,提示"该统一社会信用代码已存在" |
|
||
|
|
| 实体中介-重复导入更新 | socialCreditCode已存在,updateSupport=true | 更新已有数据,状态为SUCCESS |
|
||
|
|
| 实体中介-部分错误 | 混合有效数据和无效数据 | 部分成功,状态为PARTIAL_SUCCESS |
|
||
|
|
| 状态查询 | 调用getImportStatus接口 | 返回正确状态和进度 |
|
||
|
|
| 失败记录查询 | 调用getImportFailures接口 | 返回失败记录列表,支持分页 |
|
||
|
|
| 前端轮询 | 导入后观察轮询行为 | 每2秒查询一次,完成后停止 |
|
||
|
|
| 完成通知 | 导入完成后观察通知 | 显示正确的成功/警告通知 |
|
||
|
|
| 前端UI分离 | 个人和实体导入分别显示不同按钮 | 按钮文案清晰,不会混淆 |
|
||
|
|
| 失败记录UI-个人 | 点击"查看个人中介导入失败记录"按钮 | 显示个人中介对话框,正确展示失败数据 |
|
||
|
|
| 失败记录UI-实体 | 点击"查看实体中介导入失败记录"按钮 | 显示实体中介对话框,正确展示失败数据 |
|
||
|
|
|
||
|
|
### 7.2 性能测试
|
||
|
|
|
||
|
|
| 测试项 | 测试数据量 | 性能要求 |
|
||
|
|
|--------|-----------|---------|
|
||
|
|
| 导入接口响应时间 | 任意 | < 500ms(立即返回taskId) |
|
||
|
|
| 个人中介数据处理 | 500条 | < 5秒 |
|
||
|
|
| 个人中介数据处理 | 1000条 | < 10秒 |
|
||
|
|
| 实体中介数据处理 | 500条 | < 5秒 |
|
||
|
|
| 实体中介数据处理 | 1000条 | < 10秒 |
|
||
|
|
| Redis存储 | 任意 | 数据正确存储,TTL为7天 |
|
||
|
|
| 前端轮询 | 任意 | 不阻塞UI,不影响用户操作 |
|
||
|
|
|
||
|
|
### 7.3 异常测试
|
||
|
|
|
||
|
|
| 测试项 | 测试内容 | 预期结果 |
|
||
|
|
|--------|---------|---------|
|
||
|
|
| 空文件 | 上传空Excel文件 | 返回错误提示"至少需要一条数据" |
|
||
|
|
| 格式错误 | 上传非Excel文件 | 解析失败,返回错误提示 |
|
||
|
|
| 不存在的taskId | 查询导入状态时传入随机UUID | 返回错误提示"任务不存在或已过期" |
|
||
|
|
| 并发导入 | 同时上传3个Excel文件(个人+实体) | 生成3个不同的taskId,各自独立处理,互不影响 |
|
||
|
|
| 网络中断 | 导入过程中断开网络 | 异步任务继续执行,恢复后可查询状态 |
|
||
|
|
|
||
|
|
### 7.4 数据验证测试
|
||
|
|
|
||
|
|
#### 个人中介
|
||
|
|
|
||
|
|
| 测试项 | 测试内容 | 预期结果 |
|
||
|
|
|--------|---------|---------|
|
||
|
|
| 姓名缺失 | 缺少name字段 | 记录到失败列表,提示"姓名不能为空" |
|
||
|
|
| 证件号缺失 | 缺少personId字段 | 记录到失败列表,提示"证件号码不能为空" |
|
||
|
|
| 证件号重复 | personId已存在且updateSupport=false | 记录到失败列表,提示"该证件号已存在" |
|
||
|
|
|
||
|
|
#### 实体中介
|
||
|
|
|
||
|
|
| 测试项 | 测试内容 | 预期结果 |
|
||
|
|
|--------|---------|---------|
|
||
|
|
| 机构名称缺失 | 缺少enterpriseName字段 | 记录到失败列表,提示"机构名称不能为空" |
|
||
|
|
| 统一社会信用代码重复 | socialCreditCode已存在且updateSupport=false | 记录到失败列表,提示"该统一社会信用代码已存在" |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 八、参考文档
|
||
|
|
|
||
|
|
- 员工信息异步导入实施计划: `doc/plans/2026-02-06-employee-async-import.md`
|
||
|
|
- 员工信息异步导入设计文档: `doc/plans/2026-02-06-employee-async-import-design.md`
|
||
|
|
- 招聘信息异步导入设计文档: `doc/plans/2026-02-06-recruitment-async-import-design.md`
|
||
|
|
- 员工信息导入API文档: `doc/api/ccdi-employee-import-api.md`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**设计版本:** 1.0
|
||
|
|
**创建日期:** 2026-02-06
|
||
|
|
**设计人员:** Claude
|
||
|
|
**审核状态:** 待审核
|