完成招聘双Sheet导入改造

This commit is contained in:
wkc
2026-04-23 10:27:08 +08:00
parent 110817abba
commit a2ba044ebe
14 changed files with 838 additions and 467 deletions

View File

@@ -114,16 +114,14 @@ public class CcdiStaffRecruitmentController extends BaseController {
@Operation(summary = "下载导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffRecruitmentExcel.class, "员工招聘信息");
}
/**
* 下载历史工作经历导入模板
*/
@Operation(summary = "下载历史工作经历导入模板")
@PostMapping("/workImportTemplate")
public void workImportTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffRecruitmentWorkExcel.class, "历史工作经历");
EasyExcelUtil.importTemplateWithDictDropdown(
response,
CcdiStaffRecruitmentExcel.class,
"招聘信息",
CcdiStaffRecruitmentWorkExcel.class,
"历史工作经历",
"招聘信息管理导入模板"
);
}
/**
@@ -135,16 +133,25 @@ public class CcdiStaffRecruitmentController extends BaseController {
@Log(title = "员工招聘信息", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
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("至少需要一条数据");
}
// 提交异步任务
String taskId = recruitmentService.importRecruitment(list);
String taskId = recruitmentService.importRecruitment(recruitmentList, workList);
// 立即返回,不等待后台任务完成
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");
@@ -153,31 +160,6 @@ public class CcdiStaffRecruitmentController extends BaseController {
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);
}
/**
* 查询导入状态
*/

View File

@@ -13,6 +13,12 @@ import lombok.Data;
@Schema(description = "招聘信息导入失败记录")
public class RecruitmentImportFailureVO {
@Schema(description = "失败Sheet")
private String sheetName;
@Schema(description = "失败行号")
private String sheetRowNum;
@Schema(description = "招聘项目编号")
private String recruitId;

View File

@@ -22,21 +22,11 @@ public interface ICcdiStaffRecruitmentImportService {
* @param taskId 任务ID
* @param userName 用户名
*/
void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> excelList,
void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> recruitmentList,
List<CcdiStaffRecruitmentWorkExcel> workList,
String taskId,
String userName);
/**
* 异步导入招聘记录历史工作经历数据
*
* @param excelList Excel数据列表
* @param taskId 任务ID
* @param userName 用户名
*/
void importRecruitmentWorkAsync(List<CcdiStaffRecruitmentWorkExcel> excelList,
String taskId,
String userName);
/**
* 查询导入状态
*

View File

@@ -81,13 +81,6 @@ public interface ICcdiStaffRecruitmentService {
* @param excelList Excel实体列表
* @return 结果
*/
String importRecruitment(List<CcdiStaffRecruitmentExcel> excelList);
/**
* 导入招聘记录历史工作经历数据(异步)
*
* @param excelList Excel实体列表
* @return 任务ID
*/
String importRecruitmentWork(List<CcdiStaffRecruitmentWorkExcel> excelList);
String importRecruitment(List<CcdiStaffRecruitmentExcel> recruitmentList,
List<CcdiStaffRecruitmentWorkExcel> workList);
}

View File

@@ -2,6 +2,8 @@ package com.ruoyi.info.collection.service.impl;
import com.alibaba.fastjson2.JSON;
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.CcdiStaffRecruitmentWork;
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.service.ICcdiStaffRecruitmentImportService;
import com.ruoyi.info.collection.utils.ImportLogUtils;
import com.ruoyi.common.utils.IdCardUtil;
import com.ruoyi.common.utils.StringUtils;
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.LoggerFactory;
import org.springframework.beans.BeanUtils;
@@ -28,10 +40,6 @@ import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 招聘信息异步导入Service实现
*
@@ -44,6 +52,10 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
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
private CcdiStaffRecruitmentMapper recruitmentMapper;
@@ -56,181 +68,56 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
@Override
@Async
@Transactional
public void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> excelList,
public void importRecruitmentAsync(List<CcdiStaffRecruitmentExcel> recruitmentList,
List<CcdiStaffRecruitmentWorkExcel> workList,
String taskId,
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();
// 记录导入开始
ImportLogUtils.logImportStart(log, taskId, "招聘信息", excelList.size(), userName);
ImportLogUtils.logImportStart(log, taskId, "招聘信息双Sheet", totalCount, userName);
List<CcdiStaffRecruitment> newRecords = new ArrayList<>();
List<RecruitmentImportFailureVO> failures = new ArrayList<>();
List<MainImportRow> indexedMainRows = buildMainImportRows(safeRecruitmentList);
List<WorkImportRow> indexedWorkRows = buildWorkImportRows(safeWorkList);
// 批量查询已存在的招聘记录编号
ImportLogUtils.logBatchQueryStart(log, taskId, "已存在的招聘记录编号", excelList.size());
Set<String> existingRecruitIds = getExistingRecruitIds(excelList);
ImportLogUtils.logBatchQueryComplete(log, taskId, "招聘记录编号", existingRecruitIds.size());
MainImportResult mainImportResult = importMainSheet(indexedMainRows, failures, userName, taskId);
int workSuccessCount = importWorkSheet(
indexedWorkRows,
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()) {
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);
}
saveFailures(taskId, failures);
}
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
result.setSuccessCount(newRecords.size());
result.setFailureCount(failures.size());
result.setTotalCount(totalCount);
result.setSuccessCount(mainImportResult.successCount() + workSuccessCount);
result.setFailureCount(Math.max(totalCount - result.getSuccessCount(), 0));
// 更新最终状态
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";
String finalStatus = resolveFinalStatus(result);
updateImportStatus(taskId, finalStatus, result);
long duration = System.currentTimeMillis() - startTime;
ImportLogUtils.logImportComplete(log, taskId, "招聘历史工作经历",
excelList.size(), result.getSuccessCount(), result.getFailureCount(), duration);
ImportLogUtils.logImportComplete(
log,
taskId,
"招聘信息双Sheet",
totalCount,
result.getSuccessCount(),
result.getFailureCount(),
duration
);
}
@Override
@@ -270,14 +157,188 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
return JSON.parseArray(JSON.toJSONString(failuresObj), RecruitmentImportFailureVO.class);
}
/**
* 批量查询已存在的招聘记录编号
*/
private Set<String> getExistingRecruitIds(List<CcdiStaffRecruitmentExcel> excelList) {
List<String> recruitIds = excelList.stream()
.map(CcdiStaffRecruitmentExcel::getRecruitId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
private MainImportResult importMainSheet(List<MainImportRow> mainRows,
List<RecruitmentImportFailureVO> failures,
String userName,
String taskId) {
if (mainRows.isEmpty()) {
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(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()) {
return Collections.emptySet();
@@ -288,148 +349,138 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
List<CcdiStaffRecruitment> existingRecruitments = recruitmentMapper.selectList(wrapper);
return existingRecruitments.stream()
.map(CcdiStaffRecruitment::getRecruitId)
.collect(Collectors.toSet());
.map(CcdiStaffRecruitment::getRecruitId)
.collect(Collectors.toSet());
}
/**
* 验证招聘信息数据
*/
private void validateRecruitmentData(CcdiStaffRecruitmentAddDTO addDTO,
Set<String> existingRecruitIds) {
// 验证必填字段
private boolean hasExistingWorkHistory(String recruitId) {
LambdaQueryWrapper<CcdiStaffRecruitmentWork> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CcdiStaffRecruitmentWork::getRecruitId, recruitId);
return recruitmentWorkMapper.selectCount(wrapper) > 0;
}
private void validateRecruitmentData(CcdiStaffRecruitmentAddDTO addDTO, int sheetRowNum) {
if (StringUtils.isEmpty(addDTO.getRecruitId())) {
throw new RuntimeException("招聘记录编号不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号不能为空");
}
if (StringUtils.isEmpty(addDTO.getRecruitName())) {
throw new RuntimeException("招聘项目名称不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘项目名称不能为空");
}
if (StringUtils.isEmpty(addDTO.getPosName())) {
throw new RuntimeException("职位名称不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "职位名称不能为空");
}
if (StringUtils.isEmpty(addDTO.getPosCategory())) {
throw new RuntimeException("职位类别不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "职位类别不能为空");
}
if (StringUtils.isEmpty(addDTO.getPosDesc())) {
throw new RuntimeException("职位描述不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "职位描述不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandName())) {
throw new RuntimeException("应聘人员姓名不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员姓名不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandEdu())) {
throw new RuntimeException("应聘人员学历不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员学历不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandId())) {
throw new RuntimeException("证件号码不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "证件号码不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandSchool())) {
throw new RuntimeException("应聘人员毕业院校不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员毕业院校不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandMajor())) {
throw new RuntimeException("应聘人员专业不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员专业不能为空");
}
if (StringUtils.isEmpty(addDTO.getCandGrad())) {
throw new RuntimeException("应聘人员毕业年月不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "应聘人员毕业年月不能为空");
}
if (StringUtils.isEmpty(addDTO.getAdmitStatus())) {
throw new RuntimeException("录用情况不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "录用情况不能为空");
}
if (StringUtils.isEmpty(addDTO.getRecruitType())) {
throw new RuntimeException("招聘类型不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "招聘类型不能为空");
}
// 验证证件号码格式
String idCardError = IdCardUtil.getErrorMessage(addDTO.getCandId());
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])$")) {
throw new RuntimeException("毕业年月格式不正确,应为YYYYMM");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "毕业年月格式不正确,应为YYYYMM");
}
// 验证录用状态
if (AdmitStatus.getDescByCode(addDTO.getAdmitStatus()) == null) {
throw new RuntimeException("录用情况只能填写'录用'、'未录用'或'放弃'");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "录用情况只能填写'录用'、'未录用'或'放弃'");
}
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) {
List<String> recruitIds = excelList.stream()
.map(CcdiStaffRecruitmentWorkExcel::getRecruitId)
.map(this::trim)
.filter(StringUtils::isNotEmpty)
.distinct()
.toList();
if (recruitIds.isEmpty()) {
return Collections.emptyMap();
private void validateWorkGroup(List<WorkImportRow> workRows, CcdiStaffRecruitment recruitment) {
Set<Integer> processedSortOrders = new HashSet<>();
for (WorkImportRow workRow : workRows) {
validateRecruitmentWorkData(workRow.data(), recruitment, processedSortOrders, workRow.sheetRowNum());
}
List<CcdiStaffRecruitment> recruitments = recruitmentMapper.selectBatchIds(recruitIds);
return recruitments.stream()
.collect(Collectors.toMap(CcdiStaffRecruitment::getRecruitId, item -> item));
}
private void validateRecruitmentWorkData(CcdiStaffRecruitmentWorkExcel excel,
CcdiStaffRecruitment recruitment,
Set<String> processedRecruitSortKeys) {
Set<Integer> processedSortOrders,
int sheetRowNum) {
if (StringUtils.isEmpty(trim(excel.getRecruitId()))) {
throw new RuntimeException("招聘记录编号不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号不能为空");
}
if (StringUtils.isEmpty(trim(excel.getCandName()))) {
throw new RuntimeException("候选人姓名不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "候选人姓名不能为空");
}
if (StringUtils.isEmpty(trim(excel.getRecruitName()))) {
throw new RuntimeException("招聘项目名称不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘项目名称不能为空");
}
if (StringUtils.isEmpty(trim(excel.getPosName()))) {
throw new RuntimeException("职位名称不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "职位名称不能为空");
}
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()))) {
throw new RuntimeException("工作单位不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "工作单位不能为空");
}
if (StringUtils.isEmpty(trim(excel.getPositionName()))) {
throw new RuntimeException("岗位不能为空");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "岗位不能为空");
}
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()))) {
validateMonth(excel.getJobEndMonth(), "离职年月");
validateMonth(excel.getJobEndMonth(), "离职年月", sheetRowNum);
}
if (recruitment == null) {
throw new RuntimeException("招聘记录编号不存在,请先维护招聘主信息");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号不存在,请先维护招聘主信息");
}
if (!"SOCIAL".equals(recruitment.getRecruitType())) {
throw new RuntimeException("该招聘记录不是社招,不允许导入历史工作经历");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "该招聘记录不是社招,不允许导入历史工作经历");
}
if (!sameText(excel.getCandName(), recruitment.getCandName())) {
throw new RuntimeException("招聘记录编号与候选人姓名不匹配");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号与候选人姓名不匹配");
}
if (!sameText(excel.getRecruitName(), recruitment.getRecruitName())) {
throw new RuntimeException("招聘记录编号与招聘项目名称不匹配");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号与招聘项目名称不匹配");
}
if (!sameText(excel.getPosName(), recruitment.getPosName())) {
throw new RuntimeException("招聘记录编号与职位名称不匹配");
}
String duplicateKey = trim(excel.getRecruitId()) + "#" + excel.getSortOrder();
if (!processedRecruitSortKeys.add(duplicateKey)) {
throw new RuntimeException("同一招聘记录编号下排序号重复");
throw buildValidationException(WORK_SHEET_NAME, List.of(sheetRowNum), "招聘记录编号与职位名称不匹配");
}
}
private void validateMonth(String value, String fieldName) {
private void validateMonth(String value, String fieldName, int sheetRowNum) {
String month = trim(value);
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();
}
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();
BeanUtils.copyProperties(excel, failure);
failure.setSheetName(sheetName);
failure.setSheetRowNum(sheetRowNum);
failure.setErrorMessage(errorMessage);
return failure;
}
private void appendSkippedFailures(List<CcdiStaffRecruitmentWork> validRecords,
Set<String> failedRecruitIds,
List<RecruitmentImportFailureVO> failures) {
Set<String> appendedRecruitIds = new HashSet<>();
for (CcdiStaffRecruitmentWork work : validRecords) {
if (failedRecruitIds.contains(work.getRecruitId()) && appendedRecruitIds.add(work.getRecruitId())) {
RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO();
failure.setRecruitId(work.getRecruitId());
failure.setCompanyName(work.getCompanyName());
failure.setPositionName(work.getPositionName());
failure.setErrorMessage("同一招聘记录编号存在失败行,已跳过该编号下全部工作经历,避免覆盖旧数据");
failures.add(failure);
}
}
private RecruitmentImportFailureVO buildFailure(CcdiStaffRecruitmentWorkExcel excel,
String sheetName,
String sheetRowNum,
String errorMessage) {
RecruitmentImportFailureVO failure = new RecruitmentImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setSheetName(sheetName);
failure.setSheetRowNum(sheetRowNum);
failure.setErrorMessage(errorMessage);
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) {
String key = "import:recruitment:" + taskId;
Map<String, Object> statusData = new HashMap<>();
@@ -486,35 +555,100 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm
redisTemplate.opsForHash().putAll(key, statusData);
}
/**
* 批量保存
*/
private void saveBatch(List<CcdiStaffRecruitment> list, int batchSize) {
// 使用真正的批量插入,分批次执行以提高性能
for (int i = 0; i < list.size(); i += batchSize) {
int end = Math.min(i + batchSize, list.size());
List<CcdiStaffRecruitment> subList = list.subList(i, end);
// 过滤掉已存在的记录,防止主键冲突
List<String> recruitIds = subList.stream()
.map(CcdiStaffRecruitment::getRecruitId)
.collect(Collectors.toList());
.map(CcdiStaffRecruitment::getRecruitId)
.toList();
if (recruitIds.isEmpty()) {
continue;
}
if (!recruitIds.isEmpty()) {
List<CcdiStaffRecruitment> existingRecords = recruitmentMapper.selectBatchIds(recruitIds);
Set<String> existingIds = existingRecords.stream()
.map(CcdiStaffRecruitment::getRecruitId)
.collect(Collectors.toSet());
List<CcdiStaffRecruitment> existingRecords = recruitmentMapper.selectBatchIds(recruitIds);
Set<String> existingIds = existingRecords.stream()
.map(CcdiStaffRecruitment::getRecruitId)
.collect(Collectors.toSet());
// 只插入不存在的记录
List<CcdiStaffRecruitment> toInsert = subList.stream()
.filter(r -> !existingIds.contains(r.getRecruitId()))
.collect(Collectors.toList());
if (!toInsert.isEmpty()) {
recruitmentMapper.insertBatch(toInsert);
}
List<CcdiStaffRecruitment> toInsert = subList.stream()
.filter(record -> !existingIds.contains(record.getRecruitId()))
.toList();
if (!toInsert.isEmpty()) {
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;
}
}
}

View File

@@ -182,24 +182,26 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
*/
@Override
@Transactional
public String importRecruitment(java.util.List<CcdiStaffRecruitmentExcel> excelList) {
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
public String importRecruitment(List<CcdiStaffRecruitmentExcel> recruitmentList,
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("至少需要一条数据");
}
// 生成任务ID
String taskId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
// 获取当前用户名
String userName = SecurityUtils.getUsername();
int totalCount = recruitmentList.size() + workList.size();
// 初始化Redis状态
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("totalCount", totalCount);
statusData.put("successCount", 0);
statusData.put("failureCount", 0);
statusData.put("progress", 0);
@@ -209,44 +211,7 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer
redisTemplate.opsForHash().putAll(statusKey, statusData);
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
// 调用异步导入服务
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);
recruitmentImportService.importRecruitmentAsync(recruitmentList, workList, taskId, userName);
return taskId;
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -4,6 +4,8 @@ import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
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 org.apache.poi.ss.usermodel.CellStyle;
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) {
CellStyle style = sheet.getColumnStyle(columnIndex);
assertNotNull(style, "文本列应设置默认样式");