@@ -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 > excel List,
public void importRecruitmentAsync ( List < CcdiStaffRecruitmentExcel > recruitment List,
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 > excelLis t) {
Lis t< 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 recruitmen t) {
Se t< 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 buildWork Failure( CcdiStaffRecruitmentWorkExcel excel , String errorMessage ) {
private void save Failures ( 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 appendSkippe dFailures ( 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 buil dFailure( 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 > existingRecor ds = recruitmentMapper . selectBatchIds ( recruitIds ) ;
Set < String > existingIds = existingRecords . stream ( )
. map ( CcdiStaffRecruitmen t : : getRecruitId )
. collect ( Collectors . toSet ( ) ) ;
List < CcdiStaffRecruitment > existingRecords = recruitmentMapper . selectBatchIds ( recruitIds ) ;
Set < String > existingI ds = existingRecords . stream ( )
. map ( CcdiStaffRecruitment : : getRecruitId )
. collect ( Collectors . toSe t ( ) ) ;
// 只插入不存在的记录
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 ;
}
}
}