完成招聘双Sheet导入改造
This commit is contained in:
@@ -114,16 +114,14 @@ public class CcdiStaffRecruitmentController extends BaseController {
|
|||||||
@Operation(summary = "下载导入模板")
|
@Operation(summary = "下载导入模板")
|
||||||
@PostMapping("/importTemplate")
|
@PostMapping("/importTemplate")
|
||||||
public void importTemplate(HttpServletResponse response) {
|
public void importTemplate(HttpServletResponse response) {
|
||||||
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffRecruitmentExcel.class, "员工招聘信息");
|
EasyExcelUtil.importTemplateWithDictDropdown(
|
||||||
}
|
response,
|
||||||
|
CcdiStaffRecruitmentExcel.class,
|
||||||
/**
|
"招聘信息",
|
||||||
* 下载历史工作经历导入模板
|
CcdiStaffRecruitmentWorkExcel.class,
|
||||||
*/
|
"历史工作经历",
|
||||||
@Operation(summary = "下载历史工作经历导入模板")
|
"招聘信息管理导入模板"
|
||||||
@PostMapping("/workImportTemplate")
|
);
|
||||||
public void workImportTemplate(HttpServletResponse response) {
|
|
||||||
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffRecruitmentWorkExcel.class, "历史工作经历");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,16 +133,25 @@ public class CcdiStaffRecruitmentController extends BaseController {
|
|||||||
@Log(title = "员工招聘信息", businessType = BusinessType.IMPORT)
|
@Log(title = "员工招聘信息", businessType = BusinessType.IMPORT)
|
||||||
@PostMapping("/importData")
|
@PostMapping("/importData")
|
||||||
public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception {
|
public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception {
|
||||||
List<CcdiStaffRecruitmentExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiStaffRecruitmentExcel.class);
|
List<CcdiStaffRecruitmentExcel> recruitmentList = EasyExcelUtil.importExcel(
|
||||||
|
file.getInputStream(),
|
||||||
|
CcdiStaffRecruitmentExcel.class,
|
||||||
|
"招聘信息"
|
||||||
|
);
|
||||||
|
List<CcdiStaffRecruitmentWorkExcel> workList = EasyExcelUtil.importExcel(
|
||||||
|
file.getInputStream(),
|
||||||
|
CcdiStaffRecruitmentWorkExcel.class,
|
||||||
|
"历史工作经历"
|
||||||
|
);
|
||||||
|
|
||||||
if (list == null || list.isEmpty()) {
|
boolean hasRecruitmentRows = recruitmentList != null && !recruitmentList.isEmpty();
|
||||||
|
boolean hasWorkRows = workList != null && !workList.isEmpty();
|
||||||
|
if (!hasRecruitmentRows && !hasWorkRows) {
|
||||||
return error("至少需要一条数据");
|
return error("至少需要一条数据");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交异步任务
|
String taskId = recruitmentService.importRecruitment(recruitmentList, workList);
|
||||||
String taskId = recruitmentService.importRecruitment(list);
|
|
||||||
|
|
||||||
// 立即返回,不等待后台任务完成
|
|
||||||
ImportResultVO result = new ImportResultVO();
|
ImportResultVO result = new ImportResultVO();
|
||||||
result.setTaskId(taskId);
|
result.setTaskId(taskId);
|
||||||
result.setStatus("PROCESSING");
|
result.setStatus("PROCESSING");
|
||||||
@@ -153,31 +160,6 @@ public class CcdiStaffRecruitmentController extends BaseController {
|
|||||||
return AjaxResult.success("导入任务已提交,正在后台处理", result);
|
return AjaxResult.success("导入任务已提交,正在后台处理", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 异步导入历史工作经历
|
|
||||||
*/
|
|
||||||
@Operation(summary = "异步导入历史工作经历")
|
|
||||||
@Parameter(name = "file", description = "导入文件", required = true)
|
|
||||||
@PreAuthorize("@ss.hasPermi('ccdi:staffRecruitment:import')")
|
|
||||||
@Log(title = "员工招聘历史工作经历", businessType = BusinessType.IMPORT)
|
|
||||||
@PostMapping("/importWorkData")
|
|
||||||
public AjaxResult importWorkData(@Parameter(description = "导入文件") MultipartFile file) throws Exception {
|
|
||||||
List<CcdiStaffRecruitmentWorkExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiStaffRecruitmentWorkExcel.class);
|
|
||||||
|
|
||||||
if (list == null || list.isEmpty()) {
|
|
||||||
return error("至少需要一条数据");
|
|
||||||
}
|
|
||||||
|
|
||||||
String taskId = recruitmentService.importRecruitmentWork(list);
|
|
||||||
|
|
||||||
ImportResultVO result = new ImportResultVO();
|
|
||||||
result.setTaskId(taskId);
|
|
||||||
result.setStatus("PROCESSING");
|
|
||||||
result.setMessage("历史工作经历导入任务已提交,正在后台处理");
|
|
||||||
|
|
||||||
return AjaxResult.success("历史工作经历导入任务已提交,正在后台处理", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询导入状态
|
* 查询导入状态
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import lombok.Data;
|
|||||||
@Schema(description = "招聘信息导入失败记录")
|
@Schema(description = "招聘信息导入失败记录")
|
||||||
public class RecruitmentImportFailureVO {
|
public class RecruitmentImportFailureVO {
|
||||||
|
|
||||||
|
@Schema(description = "失败Sheet")
|
||||||
|
private String sheetName;
|
||||||
|
|
||||||
|
@Schema(description = "失败行号")
|
||||||
|
private String sheetRowNum;
|
||||||
|
|
||||||
@Schema(description = "招聘项目编号")
|
@Schema(description = "招聘项目编号")
|
||||||
private String recruitId;
|
private String recruitId;
|
||||||
|
|
||||||
|
|||||||
@@ -22,18 +22,8 @@ public interface ICcdiStaffRecruitmentImportService {
|
|||||||
* @param taskId 任务ID
|
* @param taskId 任务ID
|
||||||
* @param userName 用户名
|
* @param userName 用户名
|
||||||
*/
|
*/
|
||||||
void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> excelList,
|
void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> recruitmentList,
|
||||||
String taskId,
|
List<CcdiStaffRecruitmentWorkExcel> workList,
|
||||||
String userName);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 异步导入招聘记录历史工作经历数据
|
|
||||||
*
|
|
||||||
* @param excelList Excel数据列表
|
|
||||||
* @param taskId 任务ID
|
|
||||||
* @param userName 用户名
|
|
||||||
*/
|
|
||||||
void importRecruitmentWorkAsync(List<CcdiStaffRecruitmentWorkExcel> excelList,
|
|
||||||
String taskId,
|
String taskId,
|
||||||
String userName);
|
String userName);
|
||||||
|
|
||||||
|
|||||||
@@ -81,13 +81,6 @@ public interface ICcdiStaffRecruitmentService {
|
|||||||
* @param excelList Excel实体列表
|
* @param excelList Excel实体列表
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
String importRecruitment(List<CcdiStaffRecruitmentExcel> excelList);
|
String importRecruitment(List<CcdiStaffRecruitmentExcel> recruitmentList,
|
||||||
|
List<CcdiStaffRecruitmentWorkExcel> workList);
|
||||||
/**
|
|
||||||
* 导入招聘记录历史工作经历数据(异步)
|
|
||||||
*
|
|
||||||
* @param excelList Excel实体列表
|
|
||||||
* @return 任务ID
|
|
||||||
*/
|
|
||||||
String importRecruitmentWork(List<CcdiStaffRecruitmentWorkExcel> excelList);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.ruoyi.info.collection.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.common.utils.IdCardUtil;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.info.collection.domain.CcdiStaffRecruitment;
|
import com.ruoyi.info.collection.domain.CcdiStaffRecruitment;
|
||||||
import com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork;
|
import com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork;
|
||||||
import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentAddDTO;
|
import com.ruoyi.info.collection.domain.dto.CcdiStaffRecruitmentAddDTO;
|
||||||
@@ -16,9 +18,19 @@ import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentMapper;
|
|||||||
import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentWorkMapper;
|
import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentWorkMapper;
|
||||||
import com.ruoyi.info.collection.service.ICcdiStaffRecruitmentImportService;
|
import com.ruoyi.info.collection.service.ICcdiStaffRecruitmentImportService;
|
||||||
import com.ruoyi.info.collection.utils.ImportLogUtils;
|
import com.ruoyi.info.collection.utils.ImportLogUtils;
|
||||||
import com.ruoyi.common.utils.IdCardUtil;
|
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
@@ -28,10 +40,6 @@ import org.springframework.scheduling.annotation.EnableAsync;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 招聘信息异步导入Service实现
|
* 招聘信息异步导入Service实现
|
||||||
*
|
*
|
||||||
@@ -44,6 +52,10 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(CcdiStaffRecruitmentImportServiceImpl.class);
|
private static final Logger log = LoggerFactory.getLogger(CcdiStaffRecruitmentImportServiceImpl.class);
|
||||||
|
|
||||||
|
private static final String MAIN_SHEET_NAME = "招聘信息";
|
||||||
|
private static final String WORK_SHEET_NAME = "历史工作经历";
|
||||||
|
private static final int EXCEL_DATA_START_ROW = 2;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private CcdiStaffRecruitmentMapper recruitmentMapper;
|
private CcdiStaffRecruitmentMapper recruitmentMapper;
|
||||||
|
|
||||||
@@ -56,181 +68,56 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
|
|||||||
@Override
|
@Override
|
||||||
@Async
|
@Async
|
||||||
@Transactional
|
@Transactional
|
||||||
public void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> excelList,
|
public void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> recruitmentList,
|
||||||
|
List<CcdiStaffRecruitmentWorkExcel> workList,
|
||||||
String taskId,
|
String taskId,
|
||||||
String userName) {
|
String userName) {
|
||||||
|
List<CcdiStaffRecruitmentExcel> safeRecruitmentList = recruitmentList == null
|
||||||
|
? Collections.emptyList()
|
||||||
|
: recruitmentList;
|
||||||
|
List<CcdiStaffRecruitmentWorkExcel> safeWorkList = workList == null
|
||||||
|
? Collections.emptyList()
|
||||||
|
: workList;
|
||||||
|
int totalCount = safeRecruitmentList.size() + safeWorkList.size();
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// 记录导入开始
|
ImportLogUtils.logImportStart(log, taskId, "招聘信息双Sheet", totalCount, userName);
|
||||||
ImportLogUtils.logImportStart(log, taskId, "招聘信息", excelList.size(), userName);
|
|
||||||
|
|
||||||
List<CcdiStaffRecruitment> newRecords = new ArrayList<>();
|
|
||||||
List<RecruitmentImportFailureVO> failures = new ArrayList<>();
|
List<RecruitmentImportFailureVO> failures = new ArrayList<>();
|
||||||
|
List<MainImportRow> indexedMainRows = buildMainImportRows(safeRecruitmentList);
|
||||||
|
List<WorkImportRow> indexedWorkRows = buildWorkImportRows(safeWorkList);
|
||||||
|
|
||||||
// 批量查询已存在的招聘记录编号
|
MainImportResult mainImportResult = importMainSheet(indexedMainRows, failures, userName, taskId);
|
||||||
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的招聘记录编号", excelList.size());
|
int workSuccessCount = importWorkSheet(
|
||||||
Set<String> existingRecruitIds = getExistingRecruitIds(excelList);
|
indexedWorkRows,
|
||||||
ImportLogUtils.logBatchQueryComplete(log, taskId, "招聘记录编号", existingRecruitIds.size());
|
mainImportResult.importedRecruitmentMap(),
|
||||||
|
failures,
|
||||||
|
userName,
|
||||||
|
taskId
|
||||||
|
);
|
||||||
|
|
||||||
// 用于检测Excel内部的重复ID
|
|
||||||
Set<String> excelProcessedIds = new HashSet<>();
|
|
||||||
|
|
||||||
// 分类数据
|
|
||||||
for (int i = 0; i < excelList.size(); i++) {
|
|
||||||
CcdiStaffRecruitmentExcel excel = excelList.get(i);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 转换为AddDTO进行验证
|
|
||||||
CcdiStaffRecruitmentAddDTO addDTO = new CcdiStaffRecruitmentAddDTO();
|
|
||||||
BeanUtils.copyProperties(excel, addDTO);
|
|
||||||
addDTO.setRecruitType(RecruitType.inferCode(addDTO.getRecruitName()));
|
|
||||||
|
|
||||||
// 验证数据
|
|
||||||
validateRecruitmentData(addDTO, existingRecruitIds);
|
|
||||||
|
|
||||||
CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment();
|
|
||||||
BeanUtils.copyProperties(excel, recruitment);
|
|
||||||
recruitment.setRecruitType(addDTO.getRecruitType());
|
|
||||||
|
|
||||||
if (existingRecruitIds.contains(excel.getRecruitId())) {
|
|
||||||
// 招聘记录编号在数据库中已存在,直接报错
|
|
||||||
throw new RuntimeException(String.format("招聘记录编号[%s]已存在,请勿重复导入", excel.getRecruitId()));
|
|
||||||
} else if (excelProcessedIds.contains(excel.getRecruitId())) {
|
|
||||||
// 招聘记录编号在Excel文件内部重复
|
|
||||||
throw new RuntimeException(String.format("招聘记录编号[%s]在导入文件中重复,已跳过此条记录", excel.getRecruitId()));
|
|
||||||
} else {
|
|
||||||
recruitment.setCreatedBy(userName);
|
|
||||||
recruitment.setUpdatedBy(userName);
|
|
||||||
newRecords.add(recruitment);
|
|
||||||
excelProcessedIds.add(excel.getRecruitId()); // 标记为已处理
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记录进度
|
|
||||||
ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(),
|
|
||||||
newRecords.size(), failures.size());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO();
|
|
||||||
BeanUtils.copyProperties(excel, failure);
|
|
||||||
failure.setErrorMessage(e.getMessage());
|
|
||||||
failures.add(failure);
|
|
||||||
|
|
||||||
// 记录验证失败日志
|
|
||||||
String keyData = String.format("招聘记录编号=%s, 项目名称=%s, 应聘人员=%s",
|
|
||||||
excel.getRecruitId(), excel.getRecruitName(), excel.getCandName());
|
|
||||||
ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(), keyData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量插入新数据
|
|
||||||
if (!newRecords.isEmpty()) {
|
|
||||||
ImportLogUtils.logBatchOperationStart(log, taskId, "插入",
|
|
||||||
(newRecords.size() + 499) / 500, 500);
|
|
||||||
saveBatch(newRecords, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存失败记录到Redis
|
|
||||||
if (!failures.isEmpty()) {
|
if (!failures.isEmpty()) {
|
||||||
try {
|
saveFailures(taskId, failures);
|
||||||
String failuresKey = "import:recruitment:" + taskId + ":failures";
|
|
||||||
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
|
|
||||||
ImportLogUtils.logRedisOperation(log, taskId, "保存失败记录", failures.size());
|
|
||||||
} catch (Exception e) {
|
|
||||||
ImportLogUtils.logRedisError(log, taskId, "保存失败记录", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportResult result = new ImportResult();
|
ImportResult result = new ImportResult();
|
||||||
result.setTotalCount(excelList.size());
|
result.setTotalCount(totalCount);
|
||||||
result.setSuccessCount(newRecords.size());
|
result.setSuccessCount(mainImportResult.successCount() + workSuccessCount);
|
||||||
result.setFailureCount(failures.size());
|
result.setFailureCount(Math.max(totalCount - result.getSuccessCount(), 0));
|
||||||
|
|
||||||
// 更新最终状态
|
String finalStatus = resolveFinalStatus(result);
|
||||||
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
|
|
||||||
updateImportStatus(taskId, finalStatus, result);
|
|
||||||
|
|
||||||
// 记录导入完成
|
|
||||||
long duration = System.currentTimeMillis() - startTime;
|
|
||||||
ImportLogUtils.logImportComplete(log, taskId, "招聘信息",
|
|
||||||
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Async
|
|
||||||
@Transactional
|
|
||||||
public void importRecruitmentWorkAsync(List<CcdiStaffRecruitmentWorkExcel> excelList,
|
|
||||||
String taskId,
|
|
||||||
String userName) {
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
ImportLogUtils.logImportStart(log, taskId, "招聘历史工作经历", excelList.size(), userName);
|
|
||||||
|
|
||||||
List<RecruitmentImportFailureVO> failures = new ArrayList<>();
|
|
||||||
List<CcdiStaffRecruitmentWork> validRecords = new ArrayList<>();
|
|
||||||
Set<String> failedRecruitIds = new HashSet<>();
|
|
||||||
Set<String> processedRecruitSortKeys = new HashSet<>();
|
|
||||||
|
|
||||||
Map<String, CcdiStaffRecruitment> recruitmentMap = getRecruitmentMap(excelList);
|
|
||||||
|
|
||||||
for (int i = 0; i < excelList.size(); i++) {
|
|
||||||
CcdiStaffRecruitmentWorkExcel excel = excelList.get(i);
|
|
||||||
try {
|
|
||||||
CcdiStaffRecruitment recruitment = recruitmentMap.get(trim(excel.getRecruitId()));
|
|
||||||
validateRecruitmentWorkData(excel, recruitment, processedRecruitSortKeys);
|
|
||||||
|
|
||||||
CcdiStaffRecruitmentWork work = new CcdiStaffRecruitmentWork();
|
|
||||||
BeanUtils.copyProperties(excel, work);
|
|
||||||
work.setRecruitId(trim(excel.getRecruitId()));
|
|
||||||
work.setCreatedBy(userName);
|
|
||||||
work.setUpdatedBy(userName);
|
|
||||||
validRecords.add(work);
|
|
||||||
|
|
||||||
ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(),
|
|
||||||
validRecords.size(), failures.size());
|
|
||||||
} catch (Exception e) {
|
|
||||||
failedRecruitIds.add(trim(excel.getRecruitId()));
|
|
||||||
failures.add(buildWorkFailure(excel, e.getMessage()));
|
|
||||||
String keyData = String.format("招聘记录编号=%s, 候选人=%s, 工作单位=%s",
|
|
||||||
excel.getRecruitId(), excel.getCandName(), excel.getCompanyName());
|
|
||||||
ImportLogUtils.logValidationError(log, taskId, i + 1, e.getMessage(), keyData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<CcdiStaffRecruitmentWork> importRecords = validRecords.stream()
|
|
||||||
.filter(work -> !failedRecruitIds.contains(work.getRecruitId()))
|
|
||||||
.toList();
|
|
||||||
appendSkippedFailures(validRecords, failedRecruitIds, failures);
|
|
||||||
|
|
||||||
if (!importRecords.isEmpty()) {
|
|
||||||
Set<String> importRecruitIds = importRecords.stream()
|
|
||||||
.map(CcdiStaffRecruitmentWork::getRecruitId)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
LambdaQueryWrapper<CcdiStaffRecruitmentWork> deleteWrapper = new LambdaQueryWrapper<>();
|
|
||||||
deleteWrapper.in(CcdiStaffRecruitmentWork::getRecruitId, importRecruitIds);
|
|
||||||
recruitmentWorkMapper.delete(deleteWrapper);
|
|
||||||
|
|
||||||
importRecords.forEach(recruitmentWorkMapper::insert);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!failures.isEmpty()) {
|
|
||||||
try {
|
|
||||||
String failuresKey = "import:recruitment:" + taskId + ":failures";
|
|
||||||
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
|
|
||||||
ImportLogUtils.logRedisOperation(log, taskId, "保存失败记录", failures.size());
|
|
||||||
} catch (Exception e) {
|
|
||||||
ImportLogUtils.logRedisError(log, taskId, "保存失败记录", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportResult result = new ImportResult();
|
|
||||||
result.setTotalCount(excelList.size());
|
|
||||||
result.setSuccessCount(importRecords.size());
|
|
||||||
result.setFailureCount(failures.size());
|
|
||||||
String finalStatus = result.getFailureCount() == 0 ? "SUCCESS" : "PARTIAL_SUCCESS";
|
|
||||||
updateImportStatus(taskId, finalStatus, result);
|
updateImportStatus(taskId, finalStatus, result);
|
||||||
|
|
||||||
long duration = System.currentTimeMillis() - startTime;
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
ImportLogUtils.logImportComplete(log, taskId, "招聘历史工作经历",
|
ImportLogUtils.logImportComplete(
|
||||||
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
|
log,
|
||||||
|
taskId,
|
||||||
|
"招聘信息双Sheet",
|
||||||
|
totalCount,
|
||||||
|
result.getSuccessCount(),
|
||||||
|
result.getFailureCount(),
|
||||||
|
duration
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -270,14 +157,188 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
|
|||||||
return JSON.parseArray(JSON.toJSONString(failuresObj), RecruitmentImportFailureVO.class);
|
return JSON.parseArray(JSON.toJSONString(failuresObj), RecruitmentImportFailureVO.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private MainImportResult importMainSheet(List<MainImportRow> mainRows,
|
||||||
* 批量查询已存在的招聘记录编号
|
List<RecruitmentImportFailureVO> failures,
|
||||||
*/
|
String userName,
|
||||||
private Set<String> getExistingRecruitIds(List<CcdiStaffRecruitmentExcel> excelList) {
|
String taskId) {
|
||||||
List<String> recruitIds = excelList.stream()
|
if (mainRows.isEmpty()) {
|
||||||
.map(CcdiStaffRecruitmentExcel::getRecruitId)
|
return new MainImportResult(Collections.emptyMap(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> existingRecruitIds = getExistingRecruitIds(
|
||||||
|
mainRows.stream().map(MainImportRow::data).toList()
|
||||||
|
);
|
||||||
|
Set<String> processedRecruitIds = new HashSet<>();
|
||||||
|
List<CcdiStaffRecruitment> newRecords = new ArrayList<>();
|
||||||
|
Map<String, CcdiStaffRecruitment> importedRecruitmentMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
for (int index = 0; index < mainRows.size(); index++) {
|
||||||
|
MainImportRow mainRow = mainRows.get(index);
|
||||||
|
CcdiStaffRecruitmentExcel excel = mainRow.data();
|
||||||
|
try {
|
||||||
|
CcdiStaffRecruitmentAddDTO addDTO = new CcdiStaffRecruitmentAddDTO();
|
||||||
|
BeanUtils.copyProperties(excel, addDTO);
|
||||||
|
addDTO.setRecruitType(RecruitType.inferCode(addDTO.getRecruitName()));
|
||||||
|
|
||||||
|
validateRecruitmentData(addDTO, mainRow.sheetRowNum());
|
||||||
|
|
||||||
|
String recruitId = trim(excel.getRecruitId());
|
||||||
|
if (existingRecruitIds.contains(recruitId)) {
|
||||||
|
throw buildValidationException(
|
||||||
|
MAIN_SHEET_NAME,
|
||||||
|
List.of(mainRow.sheetRowNum()),
|
||||||
|
String.format("招聘记录编号[%s]已存在,请勿重复导入", recruitId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!processedRecruitIds.add(recruitId)) {
|
||||||
|
throw buildValidationException(
|
||||||
|
MAIN_SHEET_NAME,
|
||||||
|
List.of(mainRow.sheetRowNum()),
|
||||||
|
String.format("招聘记录编号[%s]在导入文件中重复,已跳过此条记录", recruitId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment();
|
||||||
|
BeanUtils.copyProperties(excel, recruitment);
|
||||||
|
recruitment.setRecruitId(recruitId);
|
||||||
|
recruitment.setRecruitType(addDTO.getRecruitType());
|
||||||
|
recruitment.setCreatedBy(userName);
|
||||||
|
recruitment.setUpdatedBy(userName);
|
||||||
|
newRecords.add(recruitment);
|
||||||
|
importedRecruitmentMap.put(recruitId, recruitment);
|
||||||
|
|
||||||
|
ImportLogUtils.logProgress(log, taskId, index + 1, mainRows.size(), newRecords.size(), failures.size());
|
||||||
|
} catch (Exception exception) {
|
||||||
|
FailureMeta failureMeta = resolveFailureMeta(exception, List.of(mainRow.sheetRowNum()), MAIN_SHEET_NAME);
|
||||||
|
failures.add(buildFailure(excel, failureMeta.sheetName(), failureMeta.sheetRowNum(), exception.getMessage()));
|
||||||
|
ImportLogUtils.logValidationError(
|
||||||
|
log,
|
||||||
|
taskId,
|
||||||
|
index + 1,
|
||||||
|
exception.getMessage(),
|
||||||
|
String.format("招聘记录编号=%s, 项目名称=%s, 应聘人员=%s", excel.getRecruitId(), excel.getRecruitName(), excel.getCandName())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newRecords.isEmpty()) {
|
||||||
|
ImportLogUtils.logBatchOperationStart(log, taskId, "插入招聘信息", (newRecords.size() + 499) / 500, 500);
|
||||||
|
saveBatch(newRecords, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MainImportResult(importedRecruitmentMap, newRecords.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int importWorkSheet(List<WorkImportRow> workRows,
|
||||||
|
Map<String, CcdiStaffRecruitment> importedRecruitmentMap,
|
||||||
|
List<RecruitmentImportFailureVO> failures,
|
||||||
|
String userName,
|
||||||
|
String taskId) {
|
||||||
|
if (workRows.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, CcdiStaffRecruitment> existingRecruitmentMap =
|
||||||
|
getExistingRecruitmentMap(workRows, importedRecruitmentMap);
|
||||||
|
Map<String, List<WorkImportRow>> groupedRows = groupWorkRows(workRows);
|
||||||
|
int successCount = 0;
|
||||||
|
int processedGroups = 0;
|
||||||
|
|
||||||
|
for (List<WorkImportRow> recruitWorkRows : groupedRows.values()) {
|
||||||
|
processedGroups++;
|
||||||
|
WorkImportRow firstRow = recruitWorkRows.get(0);
|
||||||
|
String recruitId = trim(firstRow.data().getRecruitId());
|
||||||
|
CcdiStaffRecruitment recruitment = importedRecruitmentMap.get(recruitId);
|
||||||
|
if (recruitment == null) {
|
||||||
|
recruitment = existingRecruitmentMap.get(recruitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
validateWorkGroup(recruitWorkRows, recruitment);
|
||||||
|
|
||||||
|
if (StringUtils.isNotEmpty(recruitId) && hasExistingWorkHistory(recruitId)) {
|
||||||
|
throw buildValidationException(
|
||||||
|
WORK_SHEET_NAME,
|
||||||
|
extractWorkRowNums(recruitWorkRows),
|
||||||
|
String.format("招聘记录编号[%s]已存在历史工作经历,不允许重复导入", recruitId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CcdiStaffRecruitmentWork> entities = buildWorkEntities(recruitWorkRows, userName);
|
||||||
|
entities.forEach(entity -> recruitmentWorkMapper.insert(entity));
|
||||||
|
successCount += recruitWorkRows.size();
|
||||||
|
|
||||||
|
ImportLogUtils.logProgress(log, taskId, processedGroups, groupedRows.size(), successCount, failures.size());
|
||||||
|
} catch (Exception exception) {
|
||||||
|
FailureMeta failureMeta = resolveFailureMeta(exception, extractWorkRowNums(recruitWorkRows), WORK_SHEET_NAME);
|
||||||
|
failures.add(buildFailure(firstRow.data(), failureMeta.sheetName(), failureMeta.sheetRowNum(), exception.getMessage()));
|
||||||
|
ImportLogUtils.logValidationError(
|
||||||
|
log,
|
||||||
|
taskId,
|
||||||
|
processedGroups,
|
||||||
|
exception.getMessage(),
|
||||||
|
String.format(
|
||||||
|
"招聘记录编号=%s, 候选人=%s, 工作单位=%s",
|
||||||
|
firstRow.data().getRecruitId(),
|
||||||
|
firstRow.data().getCandName(),
|
||||||
|
firstRow.data().getCompanyName()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return successCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, List<WorkImportRow>> groupWorkRows(List<WorkImportRow> workRows) {
|
||||||
|
Map<String, List<WorkImportRow>> groupedRows = new LinkedHashMap<>();
|
||||||
|
for (WorkImportRow workRow : workRows) {
|
||||||
|
groupedRows.computeIfAbsent(buildWorkGroupKey(workRow), key -> new ArrayList<>()).add(workRow);
|
||||||
|
}
|
||||||
|
return groupedRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildWorkGroupKey(WorkImportRow workRow) {
|
||||||
|
String recruitId = trim(workRow.data().getRecruitId());
|
||||||
|
if (StringUtils.isNotEmpty(recruitId)) {
|
||||||
|
return recruitId;
|
||||||
|
}
|
||||||
|
return "__ROW__" + workRow.sheetRowNum();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, CcdiStaffRecruitment> getExistingRecruitmentMap(List<WorkImportRow> workRows,
|
||||||
|
Map<String, CcdiStaffRecruitment> importedRecruitmentMap) {
|
||||||
|
LinkedHashSet<String> recruitIds = workRows.stream()
|
||||||
|
.map(row -> trim(row.data().getRecruitId()))
|
||||||
.filter(StringUtils::isNotEmpty)
|
.filter(StringUtils::isNotEmpty)
|
||||||
.collect(Collectors.toList());
|
.filter(recruitId -> !importedRecruitmentMap.containsKey(recruitId))
|
||||||
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
if (recruitIds.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
List<CcdiStaffRecruitment> recruitments = recruitmentMapper.selectBatchIds(recruitIds);
|
||||||
|
return recruitments.stream().collect(Collectors.toMap(CcdiStaffRecruitment::getRecruitId, item -> item));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CcdiStaffRecruitmentWork> buildWorkEntities(List<WorkImportRow> workRows, String userName) {
|
||||||
|
List<CcdiStaffRecruitmentWork> entities = new ArrayList<>();
|
||||||
|
for (WorkImportRow workRow : workRows) {
|
||||||
|
CcdiStaffRecruitmentWork entity = new CcdiStaffRecruitmentWork();
|
||||||
|
BeanUtils.copyProperties(workRow.data(), entity);
|
||||||
|
entity.setRecruitId(trim(workRow.data().getRecruitId()));
|
||||||
|
entity.setCreatedBy(userName);
|
||||||
|
entity.setUpdatedBy(userName);
|
||||||
|
entities.add(entity);
|
||||||
|
}
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> getExistingRecruitIds(List<CcdiStaffRecruitmentExcel> recruitmentList) {
|
||||||
|
List<String> recruitIds = recruitmentList.stream()
|
||||||
|
.map(CcdiStaffRecruitmentExcel::getRecruitId)
|
||||||
|
.map(this::trim)
|
||||||
|
.filter(StringUtils::isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (recruitIds.isEmpty()) {
|
if (recruitIds.isEmpty()) {
|
||||||
return Collections.emptySet();
|
return Collections.emptySet();
|
||||||
@@ -292,144 +353,134 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
|
|||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private boolean hasExistingWorkHistory(String recruitId) {
|
||||||
* 验证招聘信息数据
|
LambdaQueryWrapper<CcdiStaffRecruitmentWork> wrapper = new LambdaQueryWrapper<>();
|
||||||
*/
|
wrapper.eq(CcdiStaffRecruitmentWork::getRecruitId, recruitId);
|
||||||
private void validateRecruitmentData(CcdiStaffRecruitmentAddDTO addDTO,
|
return recruitmentWorkMapper.selectCount(wrapper) > 0;
|
||||||
Set<String> existingRecruitIds) {
|
}
|
||||||
// 验证必填字段
|
|
||||||
|
private void validateRecruitmentData(CcdiStaffRecruitmentAddDTO addDTO, int sheetRowNum) {
|
||||||
if (StringUtils.isEmpty(addDTO.getRecruitId())) {
|
if (StringUtils.isEmpty(addDTO.getRecruitId())) {
|
||||||
throw new RuntimeException("招聘记录编号不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(addDTO.getRecruitName())) {
|
if (StringUtils.isEmpty(addDTO.getRecruitName())) {
|
||||||
throw new RuntimeException("招聘项目名称不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘项目名称不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(addDTO.getPosName())) {
|
if (StringUtils.isEmpty(addDTO.getPosName())) {
|
||||||
throw new RuntimeException("职位名称不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "职位名称不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(addDTO.getPosCategory())) {
|
if (StringUtils.isEmpty(addDTO.getPosCategory())) {
|
||||||
throw new RuntimeException("职位类别不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "职位类别不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(addDTO.getPosDesc())) {
|
if (StringUtils.isEmpty(addDTO.getPosDesc())) {
|
||||||
throw new RuntimeException("职位描述不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "职位描述不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(addDTO.getCandName())) {
|
if (StringUtils.isEmpty(addDTO.getCandName())) {
|
||||||
throw new RuntimeException("应聘人员姓名不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员姓名不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(addDTO.getCandEdu())) {
|
if (StringUtils.isEmpty(addDTO.getCandEdu())) {
|
||||||
throw new RuntimeException("应聘人员学历不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员学历不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(addDTO.getCandId())) {
|
if (StringUtils.isEmpty(addDTO.getCandId())) {
|
||||||
throw new RuntimeException("证件号码不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "证件号码不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(addDTO.getCandSchool())) {
|
if (StringUtils.isEmpty(addDTO.getCandSchool())) {
|
||||||
throw new RuntimeException("应聘人员毕业院校不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员毕业院校不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(addDTO.getCandMajor())) {
|
if (StringUtils.isEmpty(addDTO.getCandMajor())) {
|
||||||
throw new RuntimeException("应聘人员专业不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员专业不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(addDTO.getCandGrad())) {
|
if (StringUtils.isEmpty(addDTO.getCandGrad())) {
|
||||||
throw new RuntimeException("应聘人员毕业年月不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员毕业年月不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(addDTO.getAdmitStatus())) {
|
if (StringUtils.isEmpty(addDTO.getAdmitStatus())) {
|
||||||
throw new RuntimeException("录用情况不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "录用情况不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(addDTO.getRecruitType())) {
|
if (StringUtils.isEmpty(addDTO.getRecruitType())) {
|
||||||
throw new RuntimeException("招聘类型不能为空");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘类型不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证证件号码格式
|
|
||||||
String idCardError = IdCardUtil.getErrorMessage(addDTO.getCandId());
|
String idCardError = IdCardUtil.getErrorMessage(addDTO.getCandId());
|
||||||
if (idCardError != null) {
|
if (idCardError != null) {
|
||||||
throw new RuntimeException("证件号码" + idCardError);
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "证件号码" + idCardError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证毕业年月格式(YYYYMM)
|
|
||||||
if (!addDTO.getCandGrad().matches("^((19|20)\\d{2})(0[1-9]|1[0-2])$")) {
|
if (!addDTO.getCandGrad().matches("^((19|20)\\d{2})(0[1-9]|1[0-2])$")) {
|
||||||
throw new RuntimeException("毕业年月格式不正确,应为YYYYMM");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "毕业年月格式不正确,应为YYYYMM");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证录用状态
|
|
||||||
if (AdmitStatus.getDescByCode(addDTO.getAdmitStatus()) == null) {
|
if (AdmitStatus.getDescByCode(addDTO.getAdmitStatus()) == null) {
|
||||||
throw new RuntimeException("录用情况只能填写'录用'、'未录用'或'放弃'");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "录用情况只能填写'录用'、'未录用'或'放弃'");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RecruitType.getDescByCode(addDTO.getRecruitType()) == null) {
|
if (RecruitType.getDescByCode(addDTO.getRecruitType()) == null) {
|
||||||
throw new RuntimeException("招聘类型只能填写'SOCIAL'或'CAMPUS'");
|
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘类型只能填写'SOCIAL'或'CAMPUS'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, CcdiStaffRecruitment> getRecruitmentMap(List<CcdiStaffRecruitmentWorkExcel> excelList) {
|
private void validateWorkGroup(List<WorkImportRow> workRows, CcdiStaffRecruitment recruitment) {
|
||||||
List<String> recruitIds = excelList.stream()
|
Set<Integer> processedSortOrders = new HashSet<>();
|
||||||
.map(CcdiStaffRecruitmentWorkExcel::getRecruitId)
|
for (WorkImportRow workRow : workRows) {
|
||||||
.map(this::trim)
|
validateRecruitmentWorkData(workRow.data(), recruitment, processedSortOrders, workRow.sheetRowNum());
|
||||||
.filter(StringUtils::isNotEmpty)
|
|
||||||
.distinct()
|
|
||||||
.toList();
|
|
||||||
if (recruitIds.isEmpty()) {
|
|
||||||
return Collections.emptyMap();
|
|
||||||
}
|
}
|
||||||
List<CcdiStaffRecruitment> recruitments = recruitmentMapper.selectBatchIds(recruitIds);
|
|
||||||
return recruitments.stream()
|
|
||||||
.collect(Collectors.toMap(CcdiStaffRecruitment::getRecruitId, item -> item));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateRecruitmentWorkData(CcdiStaffRecruitmentWorkExcel excel,
|
private void validateRecruitmentWorkData(CcdiStaffRecruitmentWorkExcel excel,
|
||||||
CcdiStaffRecruitment recruitment,
|
CcdiStaffRecruitment recruitment,
|
||||||
Set<String> processedRecruitSortKeys) {
|
Set<Integer> processedSortOrders,
|
||||||
|
int sheetRowNum) {
|
||||||
if (StringUtils.isEmpty(trim(excel.getRecruitId()))) {
|
if (StringUtils.isEmpty(trim(excel.getRecruitId()))) {
|
||||||
throw new RuntimeException("招聘记录编号不能为空");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(trim(excel.getCandName()))) {
|
if (StringUtils.isEmpty(trim(excel.getCandName()))) {
|
||||||
throw new RuntimeException("候选人姓名不能为空");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "候选人姓名不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(trim(excel.getRecruitName()))) {
|
if (StringUtils.isEmpty(trim(excel.getRecruitName()))) {
|
||||||
throw new RuntimeException("招聘项目名称不能为空");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘项目名称不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(trim(excel.getPosName()))) {
|
if (StringUtils.isEmpty(trim(excel.getPosName()))) {
|
||||||
throw new RuntimeException("职位名称不能为空");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "职位名称不能为空");
|
||||||
}
|
}
|
||||||
if (excel.getSortOrder() == null || excel.getSortOrder() <= 0) {
|
if (excel.getSortOrder() == null || excel.getSortOrder() <= 0) {
|
||||||
throw new RuntimeException("排序号不能为空且必须大于0");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "排序号不能为空且必须大于0");
|
||||||
|
}
|
||||||
|
if (!processedSortOrders.add(excel.getSortOrder())) {
|
||||||
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "同一招聘记录编号下排序号重复");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(trim(excel.getCompanyName()))) {
|
if (StringUtils.isEmpty(trim(excel.getCompanyName()))) {
|
||||||
throw new RuntimeException("工作单位不能为空");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "工作单位不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(trim(excel.getPositionName()))) {
|
if (StringUtils.isEmpty(trim(excel.getPositionName()))) {
|
||||||
throw new RuntimeException("岗位不能为空");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "岗位不能为空");
|
||||||
}
|
}
|
||||||
if (StringUtils.isEmpty(trim(excel.getJobStartMonth()))) {
|
if (StringUtils.isEmpty(trim(excel.getJobStartMonth()))) {
|
||||||
throw new RuntimeException("入职年月不能为空");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "入职年月不能为空");
|
||||||
}
|
}
|
||||||
validateMonth(excel.getJobStartMonth(), "入职年月");
|
validateMonth(excel.getJobStartMonth(), "入职年月", sheetRowNum);
|
||||||
if (StringUtils.isNotEmpty(trim(excel.getJobEndMonth()))) {
|
if (StringUtils.isNotEmpty(trim(excel.getJobEndMonth()))) {
|
||||||
validateMonth(excel.getJobEndMonth(), "离职年月");
|
validateMonth(excel.getJobEndMonth(), "离职年月", sheetRowNum);
|
||||||
}
|
}
|
||||||
if (recruitment == null) {
|
if (recruitment == null) {
|
||||||
throw new RuntimeException("招聘记录编号不存在,请先维护招聘主信息");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号不存在,请先维护招聘主信息");
|
||||||
}
|
}
|
||||||
if (!"SOCIAL".equals(recruitment.getRecruitType())) {
|
if (!"SOCIAL".equals(recruitment.getRecruitType())) {
|
||||||
throw new RuntimeException("该招聘记录不是社招,不允许导入历史工作经历");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "该招聘记录不是社招,不允许导入历史工作经历");
|
||||||
}
|
}
|
||||||
if (!sameText(excel.getCandName(), recruitment.getCandName())) {
|
if (!sameText(excel.getCandName(), recruitment.getCandName())) {
|
||||||
throw new RuntimeException("招聘记录编号与候选人姓名不匹配");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号与候选人姓名不匹配");
|
||||||
}
|
}
|
||||||
if (!sameText(excel.getRecruitName(), recruitment.getRecruitName())) {
|
if (!sameText(excel.getRecruitName(), recruitment.getRecruitName())) {
|
||||||
throw new RuntimeException("招聘记录编号与招聘项目名称不匹配");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号与招聘项目名称不匹配");
|
||||||
}
|
}
|
||||||
if (!sameText(excel.getPosName(), recruitment.getPosName())) {
|
if (!sameText(excel.getPosName(), recruitment.getPosName())) {
|
||||||
throw new RuntimeException("招聘记录编号与职位名称不匹配");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号与职位名称不匹配");
|
||||||
}
|
|
||||||
String duplicateKey = trim(excel.getRecruitId()) + "#" + excel.getSortOrder();
|
|
||||||
if (!processedRecruitSortKeys.add(duplicateKey)) {
|
|
||||||
throw new RuntimeException("同一招聘记录编号下排序号重复");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateMonth(String value, String fieldName) {
|
private void validateMonth(String value, String fieldName, int sheetRowNum) {
|
||||||
String month = trim(value);
|
String month = trim(value);
|
||||||
if (!month.matches("^((19|20)\\d{2})-(0[1-9]|1[0-2])$")) {
|
if (!month.matches("^((19|20)\\d{2})-(0[1-9]|1[0-2])$")) {
|
||||||
throw new RuntimeException(fieldName + "格式不正确,应为YYYY-MM");
|
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), fieldName + "格式不正确,应为YYYY-MM");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,32 +492,50 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
|
|||||||
return value == null ? null : value.trim();
|
return value == null ? null : value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private RecruitmentImportFailureVO buildWorkFailure(CcdiStaffRecruitmentWorkExcel excel, String errorMessage) {
|
private void saveFailures(String taskId, List<RecruitmentImportFailureVO> failures) {
|
||||||
|
try {
|
||||||
|
String failuresKey = "import:recruitment:" + taskId + ":failures";
|
||||||
|
redisTemplate.opsForValue().set(failuresKey, failures, 7, TimeUnit.DAYS);
|
||||||
|
ImportLogUtils.logRedisOperation(log, taskId, "保存失败记录", failures.size());
|
||||||
|
} catch (Exception exception) {
|
||||||
|
ImportLogUtils.logRedisError(log, taskId, "保存失败记录", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RecruitmentImportFailureVO buildFailure(CcdiStaffRecruitmentExcel excel,
|
||||||
|
String sheetName,
|
||||||
|
String sheetRowNum,
|
||||||
|
String errorMessage) {
|
||||||
RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO();
|
RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO();
|
||||||
BeanUtils.copyProperties(excel, failure);
|
BeanUtils.copyProperties(excel, failure);
|
||||||
|
failure.setSheetName(sheetName);
|
||||||
|
failure.setSheetRowNum(sheetRowNum);
|
||||||
failure.setErrorMessage(errorMessage);
|
failure.setErrorMessage(errorMessage);
|
||||||
return failure;
|
return failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void appendSkippedFailures(List<CcdiStaffRecruitmentWork> validRecords,
|
private RecruitmentImportFailureVO buildFailure(CcdiStaffRecruitmentWorkExcel excel,
|
||||||
Set<String> failedRecruitIds,
|
String sheetName,
|
||||||
List<RecruitmentImportFailureVO> failures) {
|
String sheetRowNum,
|
||||||
Set<String> appendedRecruitIds = new HashSet<>();
|
String errorMessage) {
|
||||||
for (CcdiStaffRecruitmentWork work : validRecords) {
|
|
||||||
if (failedRecruitIds.contains(work.getRecruitId()) && appendedRecruitIds.add(work.getRecruitId())) {
|
|
||||||
RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO();
|
RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO();
|
||||||
failure.setRecruitId(work.getRecruitId());
|
BeanUtils.copyProperties(excel, failure);
|
||||||
failure.setCompanyName(work.getCompanyName());
|
failure.setSheetName(sheetName);
|
||||||
failure.setPositionName(work.getPositionName());
|
failure.setSheetRowNum(sheetRowNum);
|
||||||
failure.setErrorMessage("同一招聘记录编号存在失败行,已跳过该编号下全部工作经历,避免覆盖旧数据");
|
failure.setErrorMessage(errorMessage);
|
||||||
failures.add(failure);
|
return failure;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private String resolveFinalStatus(ImportResult result) {
|
||||||
|
if (result.getFailureCount() == 0) {
|
||||||
|
return "SUCCESS";
|
||||||
|
}
|
||||||
|
if (result.getSuccessCount() == 0) {
|
||||||
|
return "FAILED";
|
||||||
|
}
|
||||||
|
return "PARTIAL_SUCCESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新导入状态
|
|
||||||
*/
|
|
||||||
private void updateImportStatus(String taskId, String status, ImportResult result) {
|
private void updateImportStatus(String taskId, String status, ImportResult result) {
|
||||||
String key = "import:recruitment:" + taskId;
|
String key = "import:recruitment:" + taskId;
|
||||||
Map<String, Object> statusData = new HashMap<>();
|
Map<String, Object> statusData = new HashMap<>();
|
||||||
@@ -486,35 +555,100 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
|
|||||||
redisTemplate.opsForHash().putAll(key, statusData);
|
redisTemplate.opsForHash().putAll(key, statusData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量保存
|
|
||||||
*/
|
|
||||||
private void saveBatch(List<CcdiStaffRecruitment> list, int batchSize) {
|
private void saveBatch(List<CcdiStaffRecruitment> list, int batchSize) {
|
||||||
// 使用真正的批量插入,分批次执行以提高性能
|
|
||||||
for (int i = 0; i < list.size(); i += batchSize) {
|
for (int i = 0; i < list.size(); i += batchSize) {
|
||||||
int end = Math.min(i + batchSize, list.size());
|
int end = Math.min(i + batchSize, list.size());
|
||||||
List<CcdiStaffRecruitment> subList = list.subList(i, end);
|
List<CcdiStaffRecruitment> subList = list.subList(i, end);
|
||||||
|
|
||||||
// 过滤掉已存在的记录,防止主键冲突
|
|
||||||
List<String> recruitIds = subList.stream()
|
List<String> recruitIds = subList.stream()
|
||||||
.map(CcdiStaffRecruitment::getRecruitId)
|
.map(CcdiStaffRecruitment::getRecruitId)
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
|
if (recruitIds.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!recruitIds.isEmpty()) {
|
|
||||||
List<CcdiStaffRecruitment> existingRecords = recruitmentMapper.selectBatchIds(recruitIds);
|
List<CcdiStaffRecruitment> existingRecords = recruitmentMapper.selectBatchIds(recruitIds);
|
||||||
Set<String> existingIds = existingRecords.stream()
|
Set<String> existingIds = existingRecords.stream()
|
||||||
.map(CcdiStaffRecruitment::getRecruitId)
|
.map(CcdiStaffRecruitment::getRecruitId)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
// 只插入不存在的记录
|
|
||||||
List<CcdiStaffRecruitment> toInsert = subList.stream()
|
List<CcdiStaffRecruitment> toInsert = subList.stream()
|
||||||
.filter(r -> !existingIds.contains(r.getRecruitId()))
|
.filter(record -> !existingIds.contains(record.getRecruitId()))
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
|
|
||||||
if (!toInsert.isEmpty()) {
|
if (!toInsert.isEmpty()) {
|
||||||
recruitmentMapper.insertBatch(toInsert);
|
recruitmentMapper.insertBatch(toInsert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<MainImportRow> buildMainImportRows(List<CcdiStaffRecruitmentExcel> recruitmentList) {
|
||||||
|
List<MainImportRow> rows = new ArrayList<>();
|
||||||
|
for (int i = 0; i < recruitmentList.size(); i++) {
|
||||||
|
rows.add(new MainImportRow(recruitmentList.get(i), i + EXCEL_DATA_START_ROW));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<WorkImportRow> buildWorkImportRows(List<CcdiStaffRecruitmentWorkExcel> workList) {
|
||||||
|
List<WorkImportRow> rows = new ArrayList<>();
|
||||||
|
for (int i = 0; i < workList.size(); i++) {
|
||||||
|
rows.add(new WorkImportRow(workList.get(i), i + EXCEL_DATA_START_ROW));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Integer> extractWorkRowNums(List<WorkImportRow> rows) {
|
||||||
|
return rows.stream().map(WorkImportRow::sheetRowNum).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private FailureMeta resolveFailureMeta(Exception exception, List<Integer> rowNums, String defaultSheetName) {
|
||||||
|
if (exception instanceof ImportValidationException validationException) {
|
||||||
|
return new FailureMeta(validationException.getSheetName(), validationException.getSheetRowNum());
|
||||||
|
}
|
||||||
|
return new FailureMeta(defaultSheetName, formatSheetRowNum(rowNums));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImportValidationException buildValidationException(String sheetName, List<Integer> rowNums, String message) {
|
||||||
|
return new ImportValidationException(sheetName, formatSheetRowNum(rowNums), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatSheetRowNum(List<Integer> rowNums) {
|
||||||
|
if (rowNums == null || rowNums.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return rowNums.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
.map(String::valueOf)
|
||||||
|
.collect(Collectors.joining("、"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private record MainImportRow(CcdiStaffRecruitmentExcel data, int sheetRowNum) {}
|
||||||
|
|
||||||
|
private record WorkImportRow(CcdiStaffRecruitmentWorkExcel data, int sheetRowNum) {}
|
||||||
|
|
||||||
|
private record MainImportResult(Map<String, CcdiStaffRecruitment> importedRecruitmentMap, int successCount) {}
|
||||||
|
|
||||||
|
private record FailureMeta(String sheetName, String sheetRowNum) {}
|
||||||
|
|
||||||
|
private static class ImportValidationException extends RuntimeException {
|
||||||
|
|
||||||
|
private final String sheetName;
|
||||||
|
private final String sheetRowNum;
|
||||||
|
|
||||||
|
private ImportValidationException(String sheetName, String sheetRowNum, String message) {
|
||||||
|
super(message);
|
||||||
|
this.sheetName = sheetName;
|
||||||
|
this.sheetRowNum = sheetRowNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSheetName() {
|
||||||
|
return sheetName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSheetRowNum() {
|
||||||
|
return sheetRowNum;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,24 +182,26 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public String importRecruitment(java.util.List<CcdiStaffRecruitmentExcel> excelList) {
|
public String importRecruitment(List<CcdiStaffRecruitmentExcel> recruitmentList,
|
||||||
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
|
List<CcdiStaffRecruitmentWorkExcel> workList) {
|
||||||
|
recruitmentList = recruitmentList == null ? List.of() : recruitmentList;
|
||||||
|
workList = workList == null ? List.of() : workList;
|
||||||
|
boolean noRecruitmentRows = StringUtils.isNull(recruitmentList) || recruitmentList.isEmpty();
|
||||||
|
boolean noWorkRows = StringUtils.isNull(workList) || workList.isEmpty();
|
||||||
|
if (noRecruitmentRows && noWorkRows) {
|
||||||
throw new RuntimeException("至少需要一条数据");
|
throw new RuntimeException("至少需要一条数据");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成任务ID
|
|
||||||
String taskId = UUID.randomUUID().toString();
|
String taskId = UUID.randomUUID().toString();
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// 获取当前用户名
|
|
||||||
String userName = SecurityUtils.getUsername();
|
String userName = SecurityUtils.getUsername();
|
||||||
|
int totalCount = recruitmentList.size() + workList.size();
|
||||||
|
|
||||||
// 初始化Redis状态
|
|
||||||
String statusKey = "import:recruitment:" + taskId;
|
String statusKey = "import:recruitment:" + taskId;
|
||||||
Map<String, Object> statusData = new HashMap<>();
|
Map<String, Object> statusData = new HashMap<>();
|
||||||
statusData.put("taskId", taskId);
|
statusData.put("taskId", taskId);
|
||||||
statusData.put("status", "PROCESSING");
|
statusData.put("status", "PROCESSING");
|
||||||
statusData.put("totalCount", excelList.size());
|
statusData.put("totalCount", totalCount);
|
||||||
statusData.put("successCount", 0);
|
statusData.put("successCount", 0);
|
||||||
statusData.put("failureCount", 0);
|
statusData.put("failureCount", 0);
|
||||||
statusData.put("progress", 0);
|
statusData.put("progress", 0);
|
||||||
@@ -209,44 +211,7 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
|
|||||||
redisTemplate.opsForHash().putAll(statusKey, statusData);
|
redisTemplate.opsForHash().putAll(statusKey, statusData);
|
||||||
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
|
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
|
||||||
|
|
||||||
// 调用异步导入服务
|
recruitmentImportService.importRecruitmentAsync(recruitmentList, workList, taskId, userName);
|
||||||
recruitmentImportService.importRecruitmentAsync(excelList, taskId, userName);
|
|
||||||
|
|
||||||
return taskId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导入招聘记录历史工作经历数据(异步)
|
|
||||||
*
|
|
||||||
* @param excelList Excel实体列表
|
|
||||||
* @return 任务ID
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public String importRecruitmentWork(List<CcdiStaffRecruitmentWorkExcel> excelList) {
|
|
||||||
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
|
|
||||||
throw new RuntimeException("至少需要一条数据");
|
|
||||||
}
|
|
||||||
|
|
||||||
String taskId = UUID.randomUUID().toString();
|
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
String userName = SecurityUtils.getUsername();
|
|
||||||
|
|
||||||
String statusKey = "import:recruitment:" + taskId;
|
|
||||||
Map<String, Object> statusData = new HashMap<>();
|
|
||||||
statusData.put("taskId", taskId);
|
|
||||||
statusData.put("status", "PROCESSING");
|
|
||||||
statusData.put("totalCount", excelList.size());
|
|
||||||
statusData.put("successCount", 0);
|
|
||||||
statusData.put("failureCount", 0);
|
|
||||||
statusData.put("progress", 0);
|
|
||||||
statusData.put("startTime", startTime);
|
|
||||||
statusData.put("message", "正在处理历史工作经历...");
|
|
||||||
|
|
||||||
redisTemplate.opsForHash().putAll(statusKey, statusData);
|
|
||||||
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
|
|
||||||
|
|
||||||
recruitmentImportService.importRecruitmentWorkAsync(excelList, taskId, userName);
|
|
||||||
|
|
||||||
return taskId;
|
return taskId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.ruoyi.info.collection.service;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class CcdiStaffRecruitmentDualImportContractTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExposeSingleDualSheetImportEntry() throws Exception {
|
||||||
|
String controller = Files.readString(
|
||||||
|
Path.of("src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java")
|
||||||
|
);
|
||||||
|
assertTrue(controller.contains("\"招聘信息\""));
|
||||||
|
assertTrue(controller.contains("\"历史工作经历\""));
|
||||||
|
assertFalse(controller.contains("workImportTemplate"));
|
||||||
|
assertFalse(controller.contains("importWorkData"));
|
||||||
|
|
||||||
|
String service = Files.readString(
|
||||||
|
Path.of("src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java")
|
||||||
|
);
|
||||||
|
assertTrue(service.contains("String importRecruitment("));
|
||||||
|
assertTrue(service.contains("List<CcdiStaffRecruitmentExcel> recruitmentList"));
|
||||||
|
assertTrue(service.contains("List<CcdiStaffRecruitmentWorkExcel> workList"));
|
||||||
|
assertFalse(service.contains("importRecruitmentWork("));
|
||||||
|
|
||||||
|
String importService = Files.readString(
|
||||||
|
Path.of("src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java")
|
||||||
|
);
|
||||||
|
assertTrue(importService.contains("void importRecruitmentAsync("));
|
||||||
|
assertTrue(importService.contains("List<CcdiStaffRecruitmentExcel> recruitmentList"));
|
||||||
|
assertTrue(importService.contains("List<CcdiStaffRecruitmentWorkExcel> workList"));
|
||||||
|
assertFalse(importService.contains("importRecruitmentWorkAsync("));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExposeFailureSheetFieldsAndSingleTaskInit() throws Exception {
|
||||||
|
assertHasField(
|
||||||
|
"com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO",
|
||||||
|
"sheetName"
|
||||||
|
);
|
||||||
|
assertHasField(
|
||||||
|
"com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO",
|
||||||
|
"sheetRowNum"
|
||||||
|
);
|
||||||
|
|
||||||
|
String serviceImpl = Files.readString(
|
||||||
|
Path.of("src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java")
|
||||||
|
);
|
||||||
|
assertTrue(serviceImpl.contains("recruitmentList.size() + workList.size()"));
|
||||||
|
assertFalse(serviceImpl.contains("importRecruitmentWork("));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertHasField(String className, String fieldName) throws Exception {
|
||||||
|
Class<?> clazz = Class.forName(className);
|
||||||
|
Field field = clazz.getDeclaredField(fieldName);
|
||||||
|
assertNotNull(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.ruoyi.info.collection.service;
|
||||||
|
|
||||||
|
import com.ruoyi.info.collection.domain.CcdiStaffRecruitment;
|
||||||
|
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel;
|
||||||
|
import com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO;
|
||||||
|
import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentMapper;
|
||||||
|
import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentWorkMapper;
|
||||||
|
import com.ruoyi.info.collection.service.impl.CcdiStaffRecruitmentImportServiceImpl;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.data.redis.core.HashOperations;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.ValueOperations;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CcdiStaffRecruitmentImportServiceImplTest {
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CcdiStaffRecruitmentImportServiceImpl service;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CcdiStaffRecruitmentMapper recruitmentMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CcdiStaffRecruitmentWorkMapper recruitmentWorkMapper;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ValueOperations<String, Object> valueOperations;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HashOperations<String, Object, Object> hashOperations;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFailWholeWorkGroupWhenExistingHistoryExists() {
|
||||||
|
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||||
|
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
|
||||||
|
when(recruitmentMapper.selectBatchIds(any())).thenReturn(List.of(buildRecruitment("RC001")));
|
||||||
|
when(recruitmentWorkMapper.selectCount(any())).thenReturn(1L);
|
||||||
|
|
||||||
|
CcdiStaffRecruitmentWorkExcel workRow = new CcdiStaffRecruitmentWorkExcel();
|
||||||
|
workRow.setRecruitId("RC001");
|
||||||
|
workRow.setCandName("张三");
|
||||||
|
workRow.setRecruitName("社会招聘项目");
|
||||||
|
workRow.setPosName("Java工程师");
|
||||||
|
workRow.setSortOrder(1);
|
||||||
|
workRow.setCompanyName("测试科技");
|
||||||
|
workRow.setPositionName("开发工程师");
|
||||||
|
workRow.setJobStartMonth("2022-01");
|
||||||
|
|
||||||
|
service.importRecruitmentAsync(Collections.emptyList(), List.of(workRow), "task-1", "admin");
|
||||||
|
|
||||||
|
verify(recruitmentWorkMapper, never()).delete(any());
|
||||||
|
verify(recruitmentWorkMapper, never()).insert(any(com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork.class));
|
||||||
|
|
||||||
|
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
|
||||||
|
verify(valueOperations).set(eq("import:recruitment:task-1:failures"), failureCaptor.capture(), anyLong(), any());
|
||||||
|
Object rawFailures = failureCaptor.getValue();
|
||||||
|
assertNotNull(rawFailures);
|
||||||
|
assertInstanceOf(List.class, rawFailures);
|
||||||
|
List<?> failures = (List<?>) rawFailures;
|
||||||
|
assertFalse(failures.isEmpty());
|
||||||
|
RecruitmentImportFailureVO failure = (RecruitmentImportFailureVO) failures.get(0);
|
||||||
|
assertEquals("历史工作经历", failure.getSheetName());
|
||||||
|
assertEquals("2", failure.getSheetRowNum());
|
||||||
|
assertEquals("招聘记录编号[RC001]已存在历史工作经历,不允许重复导入", failure.getErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiStaffRecruitment buildRecruitment(String recruitId) {
|
||||||
|
CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment();
|
||||||
|
recruitment.setRecruitId(recruitId);
|
||||||
|
recruitment.setRecruitType("SOCIAL");
|
||||||
|
recruitment.setCandName("张三");
|
||||||
|
recruitment.setRecruitName("社会招聘项目");
|
||||||
|
recruitment.setPosName("Java工程师");
|
||||||
|
return recruitment;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import com.ruoyi.common.core.domain.entity.SysDictData;
|
|||||||
import com.ruoyi.common.utils.DictUtils;
|
import com.ruoyi.common.utils.DictUtils;
|
||||||
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
|
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
|
||||||
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
|
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
|
||||||
|
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel;
|
||||||
|
import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel;
|
||||||
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
|
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
|
||||||
import org.apache.poi.ss.usermodel.CellStyle;
|
import org.apache.poi.ss.usermodel.CellStyle;
|
||||||
import org.apache.poi.ss.usermodel.DataValidation;
|
import org.apache.poi.ss.usermodel.DataValidation;
|
||||||
@@ -98,6 +100,35 @@ class EasyExcelUtilTemplateTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void importTemplateWithDictDropdown_shouldCreateRecruitmentDualSheetTemplate() throws Exception {
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
|
||||||
|
try (MockedStatic<DictUtils> mocked = mockStatic(DictUtils.class)) {
|
||||||
|
mocked.when(() -> DictUtils.getDictCache("ccdi_admit_status"))
|
||||||
|
.thenReturn(List.of(
|
||||||
|
buildDictData("录用"),
|
||||||
|
buildDictData("未录用"),
|
||||||
|
buildDictData("放弃")
|
||||||
|
));
|
||||||
|
|
||||||
|
EasyExcelUtil.importTemplateWithDictDropdown(
|
||||||
|
response,
|
||||||
|
CcdiStaffRecruitmentExcel.class,
|
||||||
|
"招聘信息",
|
||||||
|
CcdiStaffRecruitmentWorkExcel.class,
|
||||||
|
"历史工作经历",
|
||||||
|
"招聘信息管理导入模板"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
|
||||||
|
assertEquals(2, workbook.getNumberOfSheets(), "招聘导入模板应输出双Sheet");
|
||||||
|
assertEquals("招聘信息", workbook.getSheetAt(0).getSheetName());
|
||||||
|
assertEquals("历史工作经历", workbook.getSheetAt(1).getSheetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void assertTextColumn(Sheet sheet, int columnIndex) {
|
private void assertTextColumn(Sheet sheet, int columnIndex) {
|
||||||
CellStyle style = sheet.getColumnStyle(columnIndex);
|
CellStyle style = sheet.getColumnStyle(columnIndex);
|
||||||
assertNotNull(style, "文本列应设置默认样式");
|
assertNotNull(style, "文本列应设置默认样式");
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# 招聘信息管理双 Sheet 导入实施记录
|
||||||
|
|
||||||
|
## 文档信息
|
||||||
|
|
||||||
|
- 保存路径:`docs/reports/implementation/2026-04-23-staff-recruitment-dual-sheet-import-implementation.md`
|
||||||
|
- 实施日期:2026-04-23
|
||||||
|
- 关联范围:招聘信息管理前后端
|
||||||
|
|
||||||
|
## 本次修改内容
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
|
||||||
|
1. 将招聘导入控制器收口为单一模板下载接口与单一导入入口:
|
||||||
|
- 模板改为 `招聘信息` + `历史工作经历` 双 Sheet;
|
||||||
|
- 移除独立 `workImportTemplate`、`importWorkData` 入口。
|
||||||
|
2. 调整 `ICcdiStaffRecruitmentService`、`ICcdiStaffRecruitmentImportService` 签名,统一为双 Sheet 单任务提交。
|
||||||
|
3. 在 `CcdiStaffRecruitmentServiceImpl` 中统一初始化 Redis 任务状态,任务总数按 `recruitmentList.size() + workList.size()` 统计。
|
||||||
|
4. 重写 `CcdiStaffRecruitmentImportServiceImpl` 导入编排:
|
||||||
|
- 主 Sheet 先校验并落库;
|
||||||
|
- 工作经历 Sheet 按 `recruitId` 分组校验;
|
||||||
|
- 工作经历既支持匹配“本次主 Sheet 成功数据”,也支持匹配数据库已有招聘主信息;
|
||||||
|
- 若数据库已存在该招聘记录的历史工作经历,则整组失败,不做覆盖。
|
||||||
|
5. 为 `RecruitmentImportFailureVO` 补充 `sheetName`、`sheetRowNum` 字段,失败记录可直接定位到具体 Sheet 和 Excel 行号。
|
||||||
|
6. 新增/补充招聘导入回归测试:
|
||||||
|
- `CcdiStaffRecruitmentDualImportContractTest`
|
||||||
|
- `CcdiStaffRecruitmentImportServiceImplTest`
|
||||||
|
- `EasyExcelUtilTemplateTest`
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
|
||||||
|
1. 招聘信息管理页工具栏只保留一个“导入”按钮,删除独立“导入工作经历”入口。
|
||||||
|
2. 上传弹窗文案统一为双 Sheet 模式,模板说明明确为“招聘信息 + 历史工作经历”。
|
||||||
|
3. 页面本地状态收口为单任务轮询:
|
||||||
|
- 只保存一个 `currentTaskId`;
|
||||||
|
- 删除按导入类型分流的状态与提示文案。
|
||||||
|
4. 失败弹窗统一展示:
|
||||||
|
- `失败Sheet`
|
||||||
|
- `失败行号`
|
||||||
|
- `失败原因`
|
||||||
|
- 以及招聘编号/项目/岗位/候选人/工作单位等辅助字段。
|
||||||
|
5. 新增前端静态契约测试:
|
||||||
|
- `staff-recruitment-import-toolbar.test.js`
|
||||||
|
- `staff-recruitment-import-state.test.js`
|
||||||
|
- `staff-recruitment-import-failure-dialog.test.js`
|
||||||
|
|
||||||
|
## 影响范围
|
||||||
|
|
||||||
|
- 后端:招聘导入模板下载、导入提交、异步导入编排、失败记录返回。
|
||||||
|
- 前端:招聘信息管理页导入入口、上传弹窗、导入轮询、失败弹窗。
|
||||||
|
- 测试:招聘导入后端定向测试、前端静态契约测试、真实页面 Playwright 验证。
|
||||||
|
- 文档:新增本实施记录。
|
||||||
|
|
||||||
|
## 验证情况
|
||||||
|
|
||||||
|
1. 后端定向测试通过:
|
||||||
|
- 命令:
|
||||||
|
`mvn -pl ccdi-info-collection -am -Dtest=CcdiStaffRecruitmentDualImportContractTest,CcdiStaffRecruitmentImportServiceImplTest,EasyExcelUtilTemplateTest -Dsurefire.failIfNoSpecifiedTests=false test`
|
||||||
|
- 结果:7 个测试全部通过。
|
||||||
|
2. 后端编译通过:
|
||||||
|
- 命令:
|
||||||
|
`mvn -pl ccdi-info-collection,ruoyi-admin -am -DskipTests compile`
|
||||||
|
- 结果:`BUILD SUCCESS`。
|
||||||
|
3. 前端静态契约测试通过:
|
||||||
|
- 命令:
|
||||||
|
`source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && node ruoyi-ui/tests/unit/staff-recruitment-import-toolbar.test.js && node ruoyi-ui/tests/unit/staff-recruitment-import-state.test.js && node ruoyi-ui/tests/unit/staff-recruitment-import-failure-dialog.test.js`
|
||||||
|
- 结果:3 个脚本全部通过。
|
||||||
|
4. 前端生产构建通过:
|
||||||
|
- 命令:
|
||||||
|
`source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run build:prod`
|
||||||
|
- 结果:构建成功,仅存在原有体积告警,无新增构建错误。
|
||||||
|
5. 真实页面 Playwright 验证通过:
|
||||||
|
- 从真实登录页进入 `/maintain/staffRecruitment`;
|
||||||
|
- 在真实导入弹窗中下载双 Sheet 模板;
|
||||||
|
- 使用真实模板生成并上传 `只导招聘信息 Sheet` 样本,成功新增 `RC202604230901`;
|
||||||
|
- 使用真实模板生成并上传 `只导历史工作经历 Sheet` 样本,`RC202604230901` 从 `0段` 变为 `1段`;
|
||||||
|
- 使用真实模板生成并上传 `双 Sheet 同时导入` 样本,成功新增 `RC202604230902` 且直接显示 `1段`;
|
||||||
|
- 再次上传 `RC202604230902` 的工作经历样本,页面出现失败按钮;
|
||||||
|
- 打开失败弹窗后,确认展示了 `失败Sheet / 失败行号 / 失败原因`,并看到错误文案:
|
||||||
|
`招聘记录编号[RC202604230902]已存在历史工作经历,不允许重复导入`。
|
||||||
|
6. 测试数据与缓存清理完成:
|
||||||
|
- 通过后端删除接口清理 `RC202604230901`、`RC202604230902`;
|
||||||
|
- 再次查询两条测试招聘名称,返回 `total=0`;
|
||||||
|
- 浏览器侧 `localStorage.staff_recruitment_import_last_task` 已清空;
|
||||||
|
- 已关闭本轮 Playwright 浏览器、前端 `8080` dev server 和后端 `62318` 进程。
|
||||||
@@ -99,25 +99,6 @@
|
|||||||
v-hasPermi="['ccdi:staffRecruitment:import']"
|
v-hasPermi="['ccdi:staffRecruitment:import']"
|
||||||
>导入</el-button>
|
>导入</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="1.5">
|
|
||||||
<el-button
|
|
||||||
v-if="isPreviewMode()"
|
|
||||||
type="info"
|
|
||||||
plain
|
|
||||||
icon="el-icon-upload"
|
|
||||||
size="mini"
|
|
||||||
@click="handleWorkImport"
|
|
||||||
>导入工作经历</el-button>
|
|
||||||
<el-button
|
|
||||||
v-else
|
|
||||||
type="info"
|
|
||||||
plain
|
|
||||||
icon="el-icon-upload"
|
|
||||||
size="mini"
|
|
||||||
@click="handleWorkImport"
|
|
||||||
v-hasPermi="['ccdi:staffRecruitment:import']"
|
|
||||||
>导入工作经历</el-button>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="1.5" v-if="showFailureButton">
|
<el-col :span="1.5" v-if="showFailureButton">
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
:content="getLastImportTooltip()"
|
:content="getLastImportTooltip()"
|
||||||
@@ -570,33 +551,22 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<el-table :data="failureList" v-loading="failureLoading">
|
<el-table :data="failureList" v-loading="failureLoading">
|
||||||
|
<el-table-column label="失败Sheet" prop="sheetName" align="center" width="140">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.sheetName || "-" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="失败行号" prop="sheetRowNum" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.sheetRowNum ? `第${scope.row.sheetRowNum}行` : "-" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="招聘记录编号" prop="recruitId" align="center" width="150" />
|
<el-table-column label="招聘记录编号" prop="recruitId" align="center" width="150" />
|
||||||
<el-table-column label="招聘项目名称" prop="recruitName" align="center" :show-overflow-tooltip="true"/>
|
<el-table-column label="招聘项目名称" prop="recruitName" align="center" :show-overflow-tooltip="true"/>
|
||||||
<el-table-column label="职位名称" prop="posName" align="center" :show-overflow-tooltip="true"/>
|
<el-table-column label="职位名称" prop="posName" align="center" :show-overflow-tooltip="true"/>
|
||||||
<el-table-column label="候选人姓名" prop="candName" align="center" width="120"/>
|
<el-table-column label="候选人姓名" prop="candName" align="center" width="120"/>
|
||||||
<el-table-column
|
<el-table-column label="工作单位" prop="companyName" align="center" min-width="180" :show-overflow-tooltip="true" />
|
||||||
v-if="currentImportType !== 'work'"
|
<el-table-column label="岗位" prop="positionName" align="center" min-width="140" :show-overflow-tooltip="true" />
|
||||||
label="证件号码"
|
|
||||||
prop="candId"
|
|
||||||
align="center"
|
|
||||||
width="180"
|
|
||||||
/>
|
|
||||||
<el-table-column
|
|
||||||
v-if="currentImportType === 'work'"
|
|
||||||
label="工作单位"
|
|
||||||
prop="companyName"
|
|
||||||
align="center"
|
|
||||||
min-width="180"
|
|
||||||
:show-overflow-tooltip="true"
|
|
||||||
/>
|
|
||||||
<el-table-column
|
|
||||||
v-if="currentImportType === 'work'"
|
|
||||||
label="岗位"
|
|
||||||
prop="positionName"
|
|
||||||
align="center"
|
|
||||||
min-width="140"
|
|
||||||
:show-overflow-tooltip="true"
|
|
||||||
/>
|
|
||||||
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200"
|
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200"
|
||||||
:show-overflow-tooltip="true" />
|
:show-overflow-tooltip="true" />
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -845,11 +815,9 @@ export default {
|
|||||||
// 是否显示弹出层
|
// 是否显示弹出层
|
||||||
open: false,
|
open: false,
|
||||||
// 弹出层标题
|
// 弹出层标题
|
||||||
title: "",
|
title: "招聘信息数据导入",
|
||||||
// 导入类型
|
|
||||||
importType: "recruitment",
|
|
||||||
// 弹窗提示
|
// 弹窗提示
|
||||||
tip: "仅允许导入\"xls\"或\"xlsx\"格式文件。",
|
tip: "仅允许导入\"xls\"或\"xlsx\"格式文件。模板包含“招聘信息”和“历史工作经历”两个 Sheet。",
|
||||||
// 是否禁用上传
|
// 是否禁用上传
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
// 设置上传的请求头部
|
// 设置上传的请求头部
|
||||||
@@ -866,8 +834,6 @@ export default {
|
|||||||
showFailureButton: false,
|
showFailureButton: false,
|
||||||
// 当前导入任务ID
|
// 当前导入任务ID
|
||||||
currentTaskId: null,
|
currentTaskId: null,
|
||||||
// 当前导入类型
|
|
||||||
currentImportType: "recruitment",
|
|
||||||
// 失败记录对话框
|
// 失败记录对话框
|
||||||
failureDialogVisible: false,
|
failureDialogVisible: false,
|
||||||
failureList: [],
|
failureList: [],
|
||||||
@@ -886,7 +852,7 @@ export default {
|
|||||||
lastImportInfo() {
|
lastImportInfo() {
|
||||||
const savedTask = this.getImportTaskFromStorage();
|
const savedTask = this.getImportTaskFromStorage();
|
||||||
if (savedTask && savedTask.totalCount) {
|
if (savedTask && savedTask.totalCount) {
|
||||||
return `导入类型: ${this.getImportTypeLabel(savedTask.importType || 'recruitment')} | 导入时间: ${this.parseTime(savedTask.saveTime)} | 总数: ${savedTask.totalCount}条 | 成功: ${savedTask.successCount}条 | 失败: ${savedTask.failureCount}条`;
|
return `导入时间: ${this.parseTime(savedTask.saveTime)} | 总数: ${savedTask.totalCount}条 | 成功: ${savedTask.successCount}条 | 失败: ${savedTask.failureCount}条`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -1163,9 +1129,7 @@ export default {
|
|||||||
this.handleDetail({ recruitId });
|
this.handleDetail({ recruitId });
|
||||||
} else if (mode === "edit") {
|
} else if (mode === "edit") {
|
||||||
this.handleUpdate({ recruitId });
|
this.handleUpdate({ recruitId });
|
||||||
} else if (mode === "workImport") {
|
} else if (mode === "workImport" || mode === "import") {
|
||||||
this.handleWorkImport();
|
|
||||||
} else if (mode === "import") {
|
|
||||||
this.handleImport();
|
this.handleImport();
|
||||||
} else if (mode === "add") {
|
} else if (mode === "add") {
|
||||||
this.handleAdd();
|
this.handleAdd();
|
||||||
@@ -1236,24 +1200,13 @@ export default {
|
|||||||
},
|
},
|
||||||
/** 导入按钮操作 */
|
/** 导入按钮操作 */
|
||||||
handleImport() {
|
handleImport() {
|
||||||
this.openImportDialog("recruitment");
|
this.openImportDialog();
|
||||||
},
|
|
||||||
/** 导入工作经历按钮操作 */
|
|
||||||
handleWorkImport() {
|
|
||||||
this.openImportDialog("work");
|
|
||||||
},
|
},
|
||||||
/** 打开导入弹窗 */
|
/** 打开导入弹窗 */
|
||||||
openImportDialog(importType) {
|
openImportDialog() {
|
||||||
const isWorkImport = importType === "work";
|
this.upload.title = "招聘信息数据导入";
|
||||||
this.upload.importType = importType;
|
this.upload.url = process.env.VUE_APP_BASE_API + "/ccdi/staffRecruitment/importData";
|
||||||
this.currentImportType = importType;
|
this.upload.tip = "仅允许导入\"xls\"或\"xlsx\"格式文件。模板包含“招聘信息”和“历史工作经历”两个 Sheet。";
|
||||||
this.upload.title = isWorkImport ? "历史工作经历数据导入" : "招聘信息数据导入";
|
|
||||||
this.upload.url = process.env.VUE_APP_BASE_API + (isWorkImport
|
|
||||||
? "/ccdi/staffRecruitment/importWorkData"
|
|
||||||
: "/ccdi/staffRecruitment/importData");
|
|
||||||
this.upload.tip = isWorkImport
|
|
||||||
? "仅允许导入\"xls\"或\"xlsx\"格式文件;招聘记录编号用于匹配,姓名/项目/职位用于校验。"
|
|
||||||
: "仅允许导入\"xls\"或\"xlsx\"格式文件。";
|
|
||||||
if (this.isPreviewMode()) {
|
if (this.isPreviewMode()) {
|
||||||
this.upload.open = true;
|
this.upload.open = true;
|
||||||
return;
|
return;
|
||||||
@@ -1262,11 +1215,7 @@ export default {
|
|||||||
},
|
},
|
||||||
/** 下载模板操作 */
|
/** 下载模板操作 */
|
||||||
importTemplate() {
|
importTemplate() {
|
||||||
if (this.upload.importType === "work") {
|
this.download('ccdi/staffRecruitment/importTemplate', {}, `招聘信息管理导入模板_${new Date().getTime()}.xlsx`);
|
||||||
this.download('ccdi/staffRecruitment/workImportTemplate', {}, `历史工作经历导入模板_${new Date().getTime()}.xlsx`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.download('ccdi/staffRecruitment/importTemplate', {}, `招聘信息导入模板_${new Date().getTime()}.xlsx`);
|
|
||||||
},
|
},
|
||||||
// 文件上传中处理
|
// 文件上传中处理
|
||||||
handleFileUploadProgress(event, file, fileList) {
|
handleFileUploadProgress(event, file, fileList) {
|
||||||
@@ -1301,19 +1250,17 @@ export default {
|
|||||||
taskId: taskId,
|
taskId: taskId,
|
||||||
status: 'PROCESSING',
|
status: 'PROCESSING',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
hasFailures: false,
|
hasFailures: false
|
||||||
importType: this.upload.importType
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 重置状态
|
// 重置状态
|
||||||
this.showFailureButton = false;
|
this.showFailureButton = false;
|
||||||
this.currentTaskId = taskId;
|
this.currentTaskId = taskId;
|
||||||
this.currentImportType = this.upload.importType;
|
|
||||||
|
|
||||||
// 显示后台处理提示
|
// 显示后台处理提示
|
||||||
this.$notify({
|
this.$notify({
|
||||||
title: '导入任务已提交',
|
title: '导入任务已提交',
|
||||||
message: `${this.getImportTypeLabel(this.upload.importType)}正在后台处理中,处理完成后将通知您`,
|
message: '招聘信息管理导入任务正在后台处理中,处理完成后将通知您',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
duration: 3000
|
duration: 3000
|
||||||
});
|
});
|
||||||
@@ -1361,15 +1308,14 @@ export default {
|
|||||||
hasFailures: statusResult.failureCount > 0,
|
hasFailures: statusResult.failureCount > 0,
|
||||||
totalCount: statusResult.totalCount,
|
totalCount: statusResult.totalCount,
|
||||||
successCount: statusResult.successCount,
|
successCount: statusResult.successCount,
|
||||||
failureCount: statusResult.failureCount,
|
failureCount: statusResult.failureCount
|
||||||
importType: this.currentImportType
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (statusResult.status === 'SUCCESS') {
|
if (statusResult.status === 'SUCCESS') {
|
||||||
// 全部成功
|
// 全部成功
|
||||||
this.$notify({
|
this.$notify({
|
||||||
title: '导入完成',
|
title: '导入完成',
|
||||||
message: `${this.getImportTypeLabel(this.currentImportType)}全部成功!共导入${statusResult.totalCount}条数据`,
|
message: `招聘信息管理全部成功!共导入${statusResult.totalCount}条数据`,
|
||||||
type: 'success',
|
type: 'success',
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
@@ -1379,7 +1325,7 @@ export default {
|
|||||||
// 部分失败
|
// 部分失败
|
||||||
this.$notify({
|
this.$notify({
|
||||||
title: '导入完成',
|
title: '导入完成',
|
||||||
message: `${this.getImportTypeLabel(this.currentImportType)}成功${statusResult.successCount}条,失败${statusResult.failureCount}条`,
|
message: `招聘信息管理成功${statusResult.successCount}条,失败${statusResult.failureCount}条`,
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
@@ -1448,7 +1394,6 @@ export default {
|
|||||||
// 如果有失败记录,恢复按钮显示
|
// 如果有失败记录,恢复按钮显示
|
||||||
if (savedTask.hasFailures && savedTask.taskId) {
|
if (savedTask.hasFailures && savedTask.taskId) {
|
||||||
this.currentTaskId = savedTask.taskId;
|
this.currentTaskId = savedTask.taskId;
|
||||||
this.currentImportType = savedTask.importType || "recruitment";
|
|
||||||
this.showFailureButton = true;
|
this.showFailureButton = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1458,7 +1403,7 @@ export default {
|
|||||||
if (savedTask && savedTask.saveTime) {
|
if (savedTask && savedTask.saveTime) {
|
||||||
const date = new Date(savedTask.saveTime);
|
const date = new Date(savedTask.saveTime);
|
||||||
const timeStr = this.parseTime(date, '{y}-{m}-{d} {h}:{i}');
|
const timeStr = this.parseTime(date, '{y}-{m}-{d} {h}:{i}');
|
||||||
return `上次${this.getImportTypeLabel(savedTask.importType || 'recruitment')}: ${timeStr}`;
|
return `上次导入: ${timeStr}`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
@@ -1538,7 +1483,7 @@ export default {
|
|||||||
// 提交上传文件
|
// 提交上传文件
|
||||||
submitFileForm() {
|
submitFileForm() {
|
||||||
if (this.isPreviewMode()) {
|
if (this.isPreviewMode()) {
|
||||||
this.$modal.msgSuccess(`预览模式:已模拟提交${this.getImportTypeLabel(this.upload.importType)}`);
|
this.$modal.msgSuccess("预览模式:已模拟提交招聘信息管理导入");
|
||||||
this.upload.open = false;
|
this.upload.open = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1548,10 +1493,6 @@ export default {
|
|||||||
handleImportDialogClose() {
|
handleImportDialogClose() {
|
||||||
this.upload.isUploading = false;
|
this.upload.isUploading = false;
|
||||||
this.$refs.upload.clearFiles();
|
this.$refs.upload.clearFiles();
|
||||||
},
|
|
||||||
/** 导入类型展示 */
|
|
||||||
getImportTypeLabel(importType) {
|
|
||||||
return importType === "work" ? "历史工作经历导入" : "招聘信息导入";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const assert = require("assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const componentPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../src/views/ccdiStaffRecruitment/index.vue"
|
||||||
|
);
|
||||||
|
const source = fs.readFileSync(componentPath, "utf8");
|
||||||
|
|
||||||
|
[
|
||||||
|
'label="失败Sheet"',
|
||||||
|
'label="失败行号"',
|
||||||
|
"scope.row.sheetName",
|
||||||
|
"scope.row.sheetRowNum",
|
||||||
|
"失败原因"
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(source.includes(token), `招聘失败弹窗缺少双Sheet定位列: ${token}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("staff-recruitment-import-failure-dialog test passed");
|
||||||
28
ruoyi-ui/tests/unit/staff-recruitment-import-state.test.js
Normal file
28
ruoyi-ui/tests/unit/staff-recruitment-import-state.test.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const assert = require("assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const componentPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../src/views/ccdiStaffRecruitment/index.vue"
|
||||||
|
);
|
||||||
|
const source = fs.readFileSync(componentPath, "utf8");
|
||||||
|
|
||||||
|
[
|
||||||
|
"模板包含“招聘信息”和“历史工作经历”两个 Sheet。",
|
||||||
|
"this.currentTaskId = taskId",
|
||||||
|
"this.showFailureButton = false",
|
||||||
|
"this.startImportStatusPolling(taskId)"
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(source.includes(token), `招聘导入状态未统一到单任务: ${token}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
"currentImportType",
|
||||||
|
"upload.importType",
|
||||||
|
"getImportTypeLabel"
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(!source.includes(token), `招聘导入状态不应再按类型拆分: ${token}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("staff-recruitment-import-state test passed");
|
||||||
34
ruoyi-ui/tests/unit/staff-recruitment-import-toolbar.test.js
Normal file
34
ruoyi-ui/tests/unit/staff-recruitment-import-toolbar.test.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const assert = require("assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const pagePath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../src/views/ccdiStaffRecruitment/index.vue"
|
||||||
|
);
|
||||||
|
const apiPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../src/api/ccdiStaffRecruitment.js"
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageSource = fs.readFileSync(pagePath, "utf8");
|
||||||
|
const apiSource = fs.readFileSync(apiPath, "utf8");
|
||||||
|
const source = `${pageSource}\n${apiSource}`;
|
||||||
|
|
||||||
|
[
|
||||||
|
"handleImport()",
|
||||||
|
'"/ccdi/staffRecruitment/importData"',
|
||||||
|
"招聘信息管理导入模板"
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(source.includes(token), `招聘导入入口缺少统一双Sheet能力: ${token}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
"handleWorkImport",
|
||||||
|
"importWorkData",
|
||||||
|
"workImportTemplate"
|
||||||
|
].forEach((token) => {
|
||||||
|
assert(!source.includes(token), `招聘页不应继续保留独立工作经历导入: ${token}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("staff-recruitment-import-toolbar test passed");
|
||||||
Reference in New Issue
Block a user