diff --git a/assets/api-docs/api/ccdi_staff_recruitment_api.md b/assets/api-docs/api/ccdi_staff_recruitment_api.md index dd150645..3202db15 100644 --- a/assets/api-docs/api/ccdi_staff_recruitment_api.md +++ b/assets/api-docs/api/ccdi_staff_recruitment_api.md @@ -51,6 +51,7 @@ "msg": "查询成功", "rows": [ { + "id": 1002, "recruitId": "REC20250205001", "recruitName": "2025春季校园招聘", "posName": "Java开发工程师", @@ -80,19 +81,19 @@ ### 1.2 查询招聘信息详情 -**接口描述:** 根据招聘项目编号查询详细信息 +**接口描述:** 根据招聘信息主键ID查询详细信息 **请求方式:** `GET` -**接口路径:** `/ccdi/staffRecruitment/{recruitId}` +**接口路径:** `/ccdi/staffRecruitment/{id}` **权限标识:** `ccdi:staffRecruitment:query` **路径参数:** -| 参数名 | 类型 | 必填 | 说明 | 示例值 | -|-----------|--------|----|--------|----------------| -| recruitId | String | 是 | 招聘项目编号 | REC20250205001 | +| 参数名 | 类型 | 必填 | 说明 | 示例值 | +|------|------|----|--------------|-----| +| id | Long | 是 | 招聘信息主键ID | 1002 | **响应示例:** @@ -101,6 +102,7 @@ "code": 200, "msg": "操作成功", "data": { + "id": 1002, "recruitId": "REC20250205001", "recruitName": "2025春季校园招聘", "posName": "Java开发工程师", @@ -237,15 +239,15 @@ **请求方式:** `DELETE` -**接口路径:** `/ccdi/staffRecruitment/{recruitIds}` +**接口路径:** `/ccdi/staffRecruitment/{ids}` **权限标识:** `ccdi:staffRecruitment:remove` **路径参数:** -| 参数名 | 类型 | 必填 | 说明 | 示例值 | -|------------|----------|----|------------------|-------------------------------| -| recruitIds | String[] | 是 | 招聘项目编号数组,多个用逗号分隔 | REC20250205001,REC20250205002 | +| 参数名 | 类型 | 必填 | 说明 | 示例值 | +|------|------|----|-----------------------|----------| +| ids | Long[] | 是 | 招聘信息主键ID数组,多个用逗号分隔 | 1002,1003 | **响应示例:** @@ -276,7 +278,7 @@ | 序号 | 字段名 | 说明 | 必填 | |----|----------|-----------|----| -| 1 | 招聘项目编号 | 唯一标识 | 是 | +| 1 | 招聘项目编号 | 允许重复 | 是 | | 2 | 招聘项目名称 | - | 是 | | 3 | 职位名称 | - | 是 | | 4 | 职位类别 | - | 是 | @@ -326,7 +328,7 @@ ```json { "code": 500, - "msg": "很抱歉,导入完成!成功 8 条,失败 2 条,错误如下:
1、招聘项目编号 REC001 导入失败:该招聘项目编号已存在
2、招聘项目编号 REC002 导入失败:证件号码格式不正确" + "msg": "很抱歉,导入完成!成功 8 条,失败 2 条,错误如下:
1、招聘项目编号 REC001 导入失败:历史工作经历匹配到多条招聘主信息
2、招聘项目编号 REC002 导入失败:证件号码格式不正确" } ``` @@ -375,14 +377,14 @@ Excel导入导出对象,使用EasyExcel注解。 | 401 | 未授权,请先登录 | | 403 | 无权限访问 | | 404 | 资源不存在 | -| 409 | 主键冲突 | +| 409 | 数据冲突 | | 500 | 服务器内部错误 | ### 常见业务错误 | 错误信息 | 说明 | |------------|--------------------| -| 该招聘项目编号已存在 | 新增时recruitId重复 | +| 历史工作经历匹配到多条招聘主信息 | 招聘项目编号重复且候选人、项目名、职位名仍无法唯一匹配从表归属 | | 招聘项目编号不能为空 | recruitId字段为空 | | 证件号码格式不正确 | 身份证号格式验证失败 | | 毕业年月格式不正确 | candGrad不是YYYYMM格式 | diff --git a/assets/database-docs/ccdi_staff_recruitment.csv b/assets/database-docs/ccdi_staff_recruitment.csv index 09348d5b..aaf3bab4 100644 --- a/assets/database-docs/ccdi_staff_recruitment.csv +++ b/assets/database-docs/ccdi_staff_recruitment.csv @@ -1,22 +1,23 @@ 4.员工招聘信息表:ccdi_staff_recruitment,,,,,, 序号,字段名,类型,默认值,是否可为空,是否主键,注释 -1,recruit_id,VARCHAR(32),,否,是,招聘项目编号 -2,recruit_name,VARCHAR(100),,否,否,招聘项目名称 -3,pos_name,VARCHAR(100),,否,否,职位名称 -4,pos_category,VARCHAR(50),,否,否,职位类别 -5,pos_desc,TEXT,,否,否,职位描述 -6,cand_name,VARCHAR(20),,否,否,应聘人员姓名 -7,cand_edu,VARCHAR(20),,否,否,应聘人员学历 -8,cand_id,VARCHAR(18),,否,否,应聘人员证件号码 -9,cand_school,VARCHAR(50),,否,否,应聘人员毕业院校 -10,cand_major,VARCHAR(30),,否,否,应聘人员专业 -11,cand_grad,VARCHAR(6),,否,否,应聘人员毕业年月 -12,admit_status,VARCHAR(10),,否,否,记录录用情况:录用、未录用、放弃等 -13,interviewer_name1,VARCHAR(20),,是,否,面试官1姓名 -14,interviewer_id1,VARCHAR(10),,是,否,面试官1工号 -13,interviewer_name2,VARCHAR(20),,是,否,面试官2姓名 -14,interviewer_id2,VARCHAR(10),,是,否,面试官2工号 -16,created_by,VARCHAR(20),-,否,否,记录创建人 -17,updated_by,VARCHAR(20),-,是,否,记录更新人 -18,create_time,VARCHAR(10),0000-00-00,是,否,创建时间 -19,update_time,VARCHAR(10),0000-00-00,是,否,更新时间 +1,id,BIGINT,,否,是,主键ID +2,recruit_id,VARCHAR(32),,否,否,招聘项目编号(允许重复) +3,recruit_name,VARCHAR(100),,否,否,招聘项目名称 +4,pos_name,VARCHAR(100),,否,否,职位名称 +5,pos_category,VARCHAR(50),,否,否,职位类别 +6,pos_desc,TEXT,,否,否,职位描述 +7,cand_name,VARCHAR(20),,否,否,应聘人员姓名 +8,cand_edu,VARCHAR(20),,否,否,应聘人员学历 +9,cand_id,VARCHAR(18),,否,否,应聘人员证件号码 +10,cand_school,VARCHAR(50),,否,否,应聘人员毕业院校 +11,cand_major,VARCHAR(30),,否,否,应聘人员专业 +12,cand_grad,VARCHAR(6),,否,否,应聘人员毕业年月 +13,admit_status,VARCHAR(10),,否,否,记录录用情况:录用、未录用、放弃等 +14,interviewer_name1,VARCHAR(20),,是,否,面试官1姓名 +15,interviewer_id1,VARCHAR(10),,是,否,面试官1工号 +16,interviewer_name2,VARCHAR(20),,是,否,面试官2姓名 +17,interviewer_id2,VARCHAR(10),,是,否,面试官2工号 +18,created_by,VARCHAR(20),-,否,否,记录创建人 +19,updated_by,VARCHAR(20),-,是,否,记录更新人 +20,create_time,VARCHAR(10),0000-00-00,是,否,创建时间 +21,update_time,VARCHAR(10),0000-00-00,是,否,更新时间 diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java index b3ac38b7..74010b1f 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java @@ -69,9 +69,9 @@ public class CcdiStaffRecruitmentController extends BaseController { */ @Operation(summary = "获取招聘信息详细信息") @PreAuthorize("@ss.hasPermi('ccdi:staffRecruitment:query')") - @GetMapping(value = "/{recruitId}") - public AjaxResult getInfo(@PathVariable String recruitId) { - return success(recruitmentService.selectRecruitmentById(recruitId)); + @GetMapping(value = "/{id}") + public AjaxResult getInfo(@PathVariable Long id) { + return success(recruitmentService.selectRecruitmentById(id)); } /** @@ -102,9 +102,9 @@ public class CcdiStaffRecruitmentController extends BaseController { @Operation(summary = "删除招聘信息") @PreAuthorize("@ss.hasPermi('ccdi:staffRecruitment:remove')") @Log(title = "员工招聘信息", businessType = BusinessType.DELETE) - @DeleteMapping("/{recruitIds}") - public AjaxResult remove(@PathVariable String[] recruitIds) { - return toAjax(recruitmentService.deleteRecruitmentByIds(recruitIds)); + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { + return toAjax(recruitmentService.deleteRecruitmentByIds(ids)); } /** diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitment.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitment.java index cd8b71d8..3ef5c647 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitment.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitment.java @@ -22,8 +22,11 @@ public class CcdiStaffRecruitment implements Serializable { @Serial private static final long serialVersionUID = 1L; + /** 主键ID */ + @TableId(type = IdType.AUTO) + private Long id; + /** 招聘记录编号 */ - @TableId(type = IdType.INPUT) private String recruitId; /** 招聘项目名称 */ diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitmentWork.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitmentWork.java index c78fa10c..356e6202 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitmentWork.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/CcdiStaffRecruitmentWork.java @@ -28,6 +28,9 @@ public class CcdiStaffRecruitmentWork implements Serializable { @TableId(type = IdType.AUTO) private Long id; + /** 关联招聘信息主键ID */ + private Long recruitmentId; + /** 关联招聘记录编号 */ private String recruitId; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentAddDTO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentAddDTO.java index b9338ffa..952903b2 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentAddDTO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentAddDTO.java @@ -3,6 +3,7 @@ package com.ruoyi.info.collection.domain.dto; import com.ruoyi.info.collection.annotation.EnumValid; import com.ruoyi.info.collection.enums.AdmitStatus; import com.ruoyi.info.collection.enums.RecruitType; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; @@ -10,6 +11,7 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; +import java.util.List; /** * 员工招聘信息新增DTO @@ -102,4 +104,8 @@ public class CcdiStaffRecruitmentAddDTO implements Serializable { /** 面试官2工号 */ @Size(max = 10, message = "面试官2工号长度不能超过10个字符") private String interviewerId2; + + /** 历史工作经历列表 */ + @Valid + private List workExperienceList; } diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentEditDTO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentEditDTO.java index 4567379d..3cd05f04 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentEditDTO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentEditDTO.java @@ -26,8 +26,13 @@ public class CcdiStaffRecruitmentEditDTO implements Serializable { @Serial private static final long serialVersionUID = 1L; + /** 主键ID */ + @NotNull(message = "招聘信息ID不能为空") + private Long id; + /** 招聘记录编号 */ - @NotNull(message = "招聘记录编号不能为空") + @NotBlank(message = "招聘记录编号不能为空") + @Size(max = 32, message = "招聘记录编号长度不能超过32个字符") private String recruitId; /** 招聘项目名称 */ diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiStaffRecruitmentVO.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiStaffRecruitmentVO.java index 3afe0cbc..d39e8ba7 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiStaffRecruitmentVO.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/CcdiStaffRecruitmentVO.java @@ -19,6 +19,9 @@ public class CcdiStaffRecruitmentVO implements Serializable { @Serial private static final long serialVersionUID = 1L; + /** 主键ID */ + private Long id; + /** 招聘记录编号 */ private String recruitId; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiStaffRecruitmentMapper.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiStaffRecruitmentMapper.java index 7d812836..ec369f23 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiStaffRecruitmentMapper.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiStaffRecruitmentMapper.java @@ -30,10 +30,10 @@ public interface CcdiStaffRecruitmentMapper extends BaseMapper existingRecruitIds = getExistingRecruitIds( - mainRows.stream().map(MainImportRow::data).toList() - ); - Set processedRecruitIds = new HashSet<>(); - List newRecords = new ArrayList<>(); - Map importedRecruitmentMap = new LinkedHashMap<>(); + Map> importedRecruitmentMap = new LinkedHashMap<>(); + int successCount = 0; for (int index = 0; index < mainRows.size(); index++) { MainImportRow mainRow = mainRows.get(index); @@ -183,31 +179,17 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm 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); + recruitmentMapper.insert(recruitment); + successCount++; + addRecruitment(importedRecruitmentMap, recruitment); - ImportLogUtils.logProgress(log, taskId, index + 1, mainRows.size(), newRecords.size(), failures.size()); + ImportLogUtils.logProgress(log, taskId, index + 1, mainRows.size(), successCount, 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())); @@ -221,16 +203,11 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm } } - if (!newRecords.isEmpty()) { - ImportLogUtils.logBatchOperationStart(log, taskId, "插入招聘信息", (newRecords.size() + 499) / 500, 500); - saveBatch(newRecords, 500); - } - - return new MainImportResult(importedRecruitmentMap, newRecords.size()); + return new MainImportResult(importedRecruitmentMap, successCount); } private int importWorkSheet(List workRows, - Map importedRecruitmentMap, + Map> importedRecruitmentMap, List failures, String userName, String taskId) { @@ -238,7 +215,7 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm return 0; } - Map existingRecruitmentMap = + Map> existingRecruitmentMap = getExistingRecruitmentMap(workRows, importedRecruitmentMap); Map> groupedRows = groupWorkRows(workRows); int successCount = 0; @@ -248,15 +225,18 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm 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 { + RecruitmentMatchKey matchKey = buildMatchKey(firstRow.data()); + CcdiStaffRecruitment recruitment = resolveMatchedRecruitment( + matchKey, + importedRecruitmentMap, + existingRecruitmentMap, + extractWorkRowNums(recruitWorkRows) + ); validateWorkGroup(recruitWorkRows, recruitment); - if (StringUtils.isNotEmpty(recruitId) && hasExistingWorkHistory(recruitId)) { + if (recruitment != null && hasExistingWorkHistory(recruitment.getId())) { throw buildValidationException( WORK_SHEET_NAME, extractWorkRowNums(recruitWorkRows), @@ -264,7 +244,7 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm ); } - List entities = buildWorkEntities(recruitWorkRows, userName); + List entities = buildWorkEntities(recruitWorkRows, recruitment, userName); entities.forEach(entity -> recruitmentWorkMapper.insert(entity)); successCount += recruitWorkRows.size(); @@ -299,33 +279,59 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm } private String buildWorkGroupKey(WorkImportRow workRow) { - String recruitId = trim(workRow.data().getRecruitId()); - if (StringUtils.isNotEmpty(recruitId)) { - return recruitId; + RecruitmentMatchKey key = buildMatchKey(workRow.data()); + if (key.isComplete()) { + return key.value(); } return "__ROW__" + workRow.sheetRowNum(); } - private Map getExistingRecruitmentMap(List workRows, - Map importedRecruitmentMap) { + private Map> getExistingRecruitmentMap( + List workRows, + Map> importedRecruitmentMap + ) { LinkedHashSet recruitIds = workRows.stream() + .filter(row -> !importedRecruitmentMap.containsKey(buildMatchKey(row.data()))) .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 recruitments = recruitmentMapper.selectBatchIds(recruitIds); - return recruitments.stream().collect(Collectors.toMap(CcdiStaffRecruitment::getRecruitId, item -> item)); + List recruitments = selectRecruitmentsByRecruitIds(recruitIds); + Map> result = new LinkedHashMap<>(); + recruitments.forEach(item -> addRecruitment(result, item)); + return result; } - private List buildWorkEntities(List workRows, String userName) { + private CcdiStaffRecruitment resolveMatchedRecruitment( + RecruitmentMatchKey matchKey, + Map> importedRecruitmentMap, + Map> existingRecruitmentMap, + List rowNums + ) { + List matchedRecruitments = new ArrayList<>(); + matchedRecruitments.addAll(importedRecruitmentMap.getOrDefault(matchKey, Collections.emptyList())); + matchedRecruitments.addAll(existingRecruitmentMap.getOrDefault(matchKey, Collections.emptyList())); + if (matchedRecruitments.size() > 1) { + throw buildValidationException( + WORK_SHEET_NAME, + rowNums, + String.format("招聘记录编号[%s]匹配到多条招聘主信息,无法确定历史工作经历归属", matchKey.recruitId()) + ); + } + return matchedRecruitments.isEmpty() ? null : matchedRecruitments.get(0); + } + + private List buildWorkEntities(List workRows, + CcdiStaffRecruitment recruitment, + String userName) { List entities = new ArrayList<>(); for (WorkImportRow workRow : workRows) { CcdiStaffRecruitmentWork entity = new CcdiStaffRecruitmentWork(); BeanUtils.copyProperties(workRow.data(), entity); - entity.setRecruitId(trim(workRow.data().getRecruitId())); + entity.setRecruitmentId(recruitment.getId()); + entity.setRecruitId(recruitment.getRecruitId()); entity.setCreatedBy(userName); entity.setUpdatedBy(userName); entities.add(entity); @@ -333,29 +339,9 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm return entities; } - private Set getExistingRecruitIds(List recruitmentList) { - List recruitIds = recruitmentList.stream() - .map(CcdiStaffRecruitmentExcel::getRecruitId) - .map(this::trim) - .filter(StringUtils::isNotEmpty) - .toList(); - - if (recruitIds.isEmpty()) { - return Collections.emptySet(); - } - - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.in(CcdiStaffRecruitment::getRecruitId, recruitIds); - List existingRecruitments = recruitmentMapper.selectList(wrapper); - - return existingRecruitments.stream() - .map(CcdiStaffRecruitment::getRecruitId) - .collect(Collectors.toSet()); - } - - private boolean hasExistingWorkHistory(String recruitId) { + private boolean hasExistingWorkHistory(Long recruitmentId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(CcdiStaffRecruitmentWork::getRecruitId, recruitId); + wrapper.eq(CcdiStaffRecruitmentWork::getRecruitmentId, recruitmentId); return recruitmentWorkMapper.selectCount(wrapper) > 0; } @@ -568,30 +554,36 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm redisTemplate.opsForHash().putAll(key, statusData); } - private void saveBatch(List list, int batchSize) { - for (int i = 0; i < list.size(); i += batchSize) { - int end = Math.min(i + batchSize, list.size()); - List subList = list.subList(i, end); - - List recruitIds = subList.stream() - .map(CcdiStaffRecruitment::getRecruitId) - .toList(); - if (recruitIds.isEmpty()) { - continue; - } - - List existingRecords = recruitmentMapper.selectBatchIds(recruitIds); - Set existingIds = existingRecords.stream() - .map(CcdiStaffRecruitment::getRecruitId) - .collect(Collectors.toSet()); - - List toInsert = subList.stream() - .filter(record -> !existingIds.contains(record.getRecruitId())) - .toList(); - if (!toInsert.isEmpty()) { - recruitmentMapper.insertBatch(toInsert); - } + private List selectRecruitmentsByRecruitIds(Set recruitIds) { + if (recruitIds == null || recruitIds.isEmpty()) { + return Collections.emptyList(); } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(CcdiStaffRecruitment::getRecruitId, recruitIds); + return recruitmentMapper.selectList(wrapper); + } + + private void addRecruitment(Map> map, + CcdiStaffRecruitment recruitment) { + map.computeIfAbsent(buildMatchKey(recruitment), key -> new ArrayList<>()).add(recruitment); + } + + private RecruitmentMatchKey buildMatchKey(CcdiStaffRecruitment recruitment) { + return new RecruitmentMatchKey( + trim(recruitment.getRecruitId()), + trim(recruitment.getCandName()), + trim(recruitment.getRecruitName()), + trim(recruitment.getPosName()) + ); + } + + private RecruitmentMatchKey buildMatchKey(CcdiStaffRecruitmentWorkExcel excel) { + return new RecruitmentMatchKey( + trim(excel.getRecruitId()), + trim(excel.getCandName()), + trim(excel.getRecruitName()), + trim(excel.getPosName()) + ); } private List buildMainImportRows(List recruitmentList) { @@ -641,10 +633,25 @@ public class CcdiStaffRecruitmentImportServiceImpl implements ICcdiStaffRecruitm private record WorkImportRow(CcdiStaffRecruitmentWorkExcel data, int sheetRowNum) {} - private record MainImportResult(Map importedRecruitmentMap, int successCount) {} + private record MainImportResult(Map> importedRecruitmentMap, + int successCount) {} private record FailureMeta(String sheetName, String sheetRowNum) {} + private record RecruitmentMatchKey(String recruitId, String candName, String recruitName, String posName) { + + private boolean isComplete() { + return StringUtils.isNotEmpty(recruitId) + && StringUtils.isNotEmpty(candName) + && StringUtils.isNotEmpty(recruitName) + && StringUtils.isNotEmpty(posName); + } + + private String value() { + return String.join("|", recruitId, candName, recruitName, posName); + } + } + private static class ImportValidationException extends RuntimeException { private final String sheetName; diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java index c8a799a8..1c37382d 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java @@ -1,6 +1,7 @@ package com.ruoyi.info.collection.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.info.collection.domain.CcdiStaffRecruitment; import com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork; @@ -27,6 +28,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -108,15 +110,15 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer /** * 查询招聘信息详情 * - * @param recruitId 招聘记录编号 + * @param id 主键ID * @return 招聘信息VO */ @Override - public CcdiStaffRecruitmentVO selectRecruitmentById(String recruitId) { - CcdiStaffRecruitmentVO vo = recruitmentMapper.selectRecruitmentById(recruitId); + public CcdiStaffRecruitmentVO selectRecruitmentById(Long id) { + CcdiStaffRecruitmentVO vo = recruitmentMapper.selectRecruitmentById(id); if (vo != null) { vo.setAdmitStatusDesc(AdmitStatus.getDescByCode(vo.getAdmitStatus())); - vo.setWorkExperienceList(selectWorkExperienceList(recruitId)); + vo.setWorkExperienceList(selectWorkExperienceList(vo.getId())); } return vo; } @@ -130,15 +132,14 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer @Override @Transactional public int insertRecruitment(CcdiStaffRecruitmentAddDTO addDTO) { - // 检查招聘记录编号唯一性 - if (recruitmentMapper.selectById(addDTO.getRecruitId()) != null) { - throw new RuntimeException("该招聘记录编号已存在"); - } + String recruitId = trim(addDTO.getRecruitId()); CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment(); BeanUtils.copyProperties(addDTO, recruitment); - int result = recruitmentMapper.insert(recruitment); + recruitment.setRecruitId(recruitId); + int result = recruitmentMapper.insert(recruitment); + insertWorkExperienceList(recruitment.getId(), recruitId, addDTO.getRecruitType(), addDTO.getWorkExperienceList()); return result; } @@ -151,9 +152,20 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer @Override @Transactional public int updateRecruitment(CcdiStaffRecruitmentEditDTO editDTO) { + CcdiStaffRecruitment existing = recruitmentMapper.selectById(editDTO.getId()); + if (existing == null) { + throw new RuntimeException("招聘信息不存在"); + } + + String recruitId = trim(editDTO.getRecruitId()); + editDTO.setRecruitId(recruitId); + CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment(); BeanUtils.copyProperties(editDTO, recruitment); int result = recruitmentMapper.updateById(recruitment); + if (!Objects.equals(existing.getRecruitId(), recruitId)) { + updateWorkRecruitId(editDTO.getId(), recruitId); + } replaceWorkExperienceList(editDTO); return result; @@ -162,16 +174,19 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer /** * 批量删除招聘信息 * - * @param recruitIds 需要删除的招聘记录编号 + * @param ids 需要删除的招聘信息ID * @return 结果 */ @Override @Transactional - public int deleteRecruitmentByIds(String[] recruitIds) { - LambdaQueryWrapper workWrapper = new LambdaQueryWrapper<>(); - workWrapper.in(CcdiStaffRecruitmentWork::getRecruitId, List.of(recruitIds)); - recruitmentWorkMapper.delete(workWrapper); - return recruitmentMapper.deleteBatchIds(List.of(recruitIds)); + public int deleteRecruitmentByIds(Long[] ids) { + List idList = Arrays.asList(ids); + if (!idList.isEmpty()) { + LambdaQueryWrapper workWrapper = new LambdaQueryWrapper<>(); + workWrapper.in(CcdiStaffRecruitmentWork::getRecruitmentId, idList); + recruitmentWorkMapper.delete(workWrapper); + } + return recruitmentMapper.deleteBatchIds(idList); } /** @@ -216,9 +231,9 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer return taskId; } - private List selectWorkExperienceList(String recruitId) { + private List selectWorkExperienceList(Long recruitmentId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(CcdiStaffRecruitmentWork::getRecruitId, recruitId) + wrapper.eq(CcdiStaffRecruitmentWork::getRecruitmentId, recruitmentId) .orderByAsc(CcdiStaffRecruitmentWork::getSortOrder) .orderByDesc(CcdiStaffRecruitmentWork::getId); List workList = recruitmentWorkMapper.selectList(wrapper); @@ -232,9 +247,20 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer }).toList(); } + private void updateWorkRecruitId(Long recruitmentId, String newRecruitId) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); + updateWrapper.eq(CcdiStaffRecruitmentWork::getRecruitmentId, recruitmentId) + .set(CcdiStaffRecruitmentWork::getRecruitId, newRecruitId); + recruitmentWorkMapper.update(null, updateWrapper); + } + + private String trim(String value) { + return value == null ? null : value.trim(); + } + private void replaceWorkExperienceList(CcdiStaffRecruitmentEditDTO editDTO) { LambdaQueryWrapper deleteWrapper = new LambdaQueryWrapper<>(); - deleteWrapper.eq(CcdiStaffRecruitmentWork::getRecruitId, editDTO.getRecruitId()); + deleteWrapper.eq(CcdiStaffRecruitmentWork::getRecruitmentId, editDTO.getId()); if (!Objects.equals(RecruitType.SOCIAL.getCode(), editDTO.getRecruitType())) { recruitmentWorkMapper.delete(deleteWrapper); @@ -246,12 +272,28 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer } recruitmentWorkMapper.delete(deleteWrapper); - List workList = buildWorkExperienceEntities(editDTO); + List workList = buildWorkExperienceEntities( + editDTO.getId(), + editDTO.getRecruitId(), + editDTO.getWorkExperienceList() + ); workList.forEach(recruitmentWorkMapper::insert); } - private List buildWorkExperienceEntities(CcdiStaffRecruitmentEditDTO editDTO) { - List workExperienceList = editDTO.getWorkExperienceList(); + private void insertWorkExperienceList(Long recruitmentId, + String recruitId, + String recruitType, + List workExperienceList) { + if (!Objects.equals(RecruitType.SOCIAL.getCode(), recruitType)) { + return; + } + List workList = buildWorkExperienceEntities(recruitmentId, recruitId, workExperienceList); + workList.forEach(recruitmentWorkMapper::insert); + } + + private List buildWorkExperienceEntities(Long recruitmentId, + String recruitId, + List workExperienceList) { if (workExperienceList == null || workExperienceList.isEmpty()) { return new ArrayList<>(); } @@ -264,7 +306,8 @@ public class CcdiStaffRecruitmentServiceImpl implements ICcdiStaffRecruitmentSer } CcdiStaffRecruitmentWork work = new CcdiStaffRecruitmentWork(); BeanUtils.copyProperties(item, work); - work.setRecruitId(editDTO.getRecruitId()); + work.setRecruitmentId(recruitmentId); + work.setRecruitId(recruitId); work.setSortOrder(i + 1); entityList.add(work); } diff --git a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiStaffRecruitmentMapper.xml b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiStaffRecruitmentMapper.xml index 466d3aad..c82b20e7 100644 --- a/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiStaffRecruitmentMapper.xml +++ b/ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiStaffRecruitmentMapper.xml @@ -6,7 +6,8 @@ - + + @@ -33,17 +34,17 @@ SELECT - recruit_id, recruit_name, pos_name, pos_category, pos_desc, + id, recruit_id, recruit_name, pos_name, pos_category, pos_desc, cand_name, recruit_type, cand_edu, cand_id, cand_school, cand_major, cand_grad, admit_status, interviewer_name1, interviewer_id1, interviewer_name2, interviewer_id2, created_by, create_time, updated_by, update_time FROM ccdi_staff_recruitment - WHERE recruit_id = #{recruitId} + WHERE id = #{id} - + INSERT INTO ccdi_staff_recruitment (recruit_id, recruit_name, pos_name, pos_category, pos_desc, cand_name, recruit_type, cand_edu, cand_id, cand_school, cand_major, cand_grad, @@ -124,7 +125,7 @@ interviewer_id2 = #{item.interviewerId2}, updated_by = #{item.updatedBy}, update_time = NOW() - WHERE recruit_id = #{item.recruitId} + WHERE id = #{item.id} diff --git a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentImportServiceImplTest.java b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentImportServiceImplTest.java index 2f3561f4..c50955ba 100644 --- a/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentImportServiceImplTest.java +++ b/ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffRecruitmentImportServiceImplTest.java @@ -1,6 +1,8 @@ package com.ruoyi.info.collection.service; import com.ruoyi.info.collection.domain.CcdiStaffRecruitment; +import com.ruoyi.info.collection.domain.CcdiStaffRecruitmentWork; +import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentExcel; import com.ruoyi.info.collection.domain.excel.CcdiStaffRecruitmentWorkExcel; import com.ruoyi.info.collection.domain.vo.RecruitmentImportFailureVO; import com.ruoyi.info.collection.mapper.CcdiStaffRecruitmentMapper; @@ -27,6 +29,7 @@ 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.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -55,7 +58,7 @@ class CcdiStaffRecruitmentImportServiceImplTest { void shouldFailWholeWorkGroupWhenExistingHistoryExists() { when(redisTemplate.opsForValue()).thenReturn(valueOperations); when(redisTemplate.opsForHash()).thenReturn(hashOperations); - when(recruitmentMapper.selectBatchIds(any())).thenReturn(List.of(buildRecruitment("RC001"))); + when(recruitmentMapper.selectList(any())).thenReturn(List.of(buildRecruitment("RC001"))); when(recruitmentWorkMapper.selectCount(any())).thenReturn(1L); CcdiStaffRecruitmentWorkExcel workRow = new CcdiStaffRecruitmentWorkExcel(); @@ -86,13 +89,81 @@ class CcdiStaffRecruitmentImportServiceImplTest { assertEquals("招聘记录编号[RC001]已存在历史工作经历,不允许重复导入", failure.getErrorMessage()); } + @Test + void shouldAllowDuplicateRecruitIdsWhenImportingMainSheet() { + when(redisTemplate.opsForHash()).thenReturn(hashOperations); + when(recruitmentMapper.insert(any(CcdiStaffRecruitment.class))).thenReturn(1); + + CcdiStaffRecruitmentExcel first = buildRecruitmentExcel("RC001", "张三"); + CcdiStaffRecruitmentExcel second = buildRecruitmentExcel("RC001", "李四"); + + service.importRecruitmentAsync(List.of(first, second), Collections.emptyList(), "task-2", "admin"); + + verify(recruitmentMapper, times(2)).insert(any(CcdiStaffRecruitment.class)); + verify(valueOperations, never()).set(eq("import:recruitment:task-2:failures"), any(), anyLong(), any()); + } + + @Test + void shouldAttachWorkToMatchedRecruitmentWhenRecruitIdIsDuplicated() { + when(redisTemplate.opsForHash()).thenReturn(hashOperations); + CcdiStaffRecruitment matched = buildRecruitment(10L, "RC001", "张三"); + CcdiStaffRecruitment other = buildRecruitment(11L, "RC001", "李四"); + when(recruitmentMapper.selectList(any())).thenReturn(List.of(matched, other)); + when(recruitmentWorkMapper.selectCount(any())).thenReturn(0L); + + CcdiStaffRecruitmentWorkExcel workRow = buildWorkExcel("RC001", "张三"); + + service.importRecruitmentAsync(Collections.emptyList(), List.of(workRow), "task-3", "admin"); + + ArgumentCaptor workCaptor = ArgumentCaptor.forClass(CcdiStaffRecruitmentWork.class); + verify(recruitmentWorkMapper).insert(workCaptor.capture()); + assertEquals(10L, workCaptor.getValue().getRecruitmentId()); + assertEquals("RC001", workCaptor.getValue().getRecruitId()); + } + private CcdiStaffRecruitment buildRecruitment(String recruitId) { + return buildRecruitment(1L, recruitId, "张三"); + } + + private CcdiStaffRecruitment buildRecruitment(Long id, String recruitId, String candName) { CcdiStaffRecruitment recruitment = new CcdiStaffRecruitment(); + recruitment.setId(id); recruitment.setRecruitId(recruitId); recruitment.setRecruitType("SOCIAL"); - recruitment.setCandName("张三"); + recruitment.setCandName(candName); recruitment.setRecruitName("社会招聘项目"); recruitment.setPosName("Java工程师"); return recruitment; } + + private CcdiStaffRecruitmentExcel buildRecruitmentExcel(String recruitId, String candName) { + CcdiStaffRecruitmentExcel excel = new CcdiStaffRecruitmentExcel(); + excel.setRecruitId(recruitId); + excel.setRecruitName("社会招聘项目"); + excel.setPosName("Java工程师"); + excel.setPosCategory("技术类"); + excel.setPosDesc("负责系统开发"); + excel.setAdmitStatus("录用"); + excel.setCandName(candName); + excel.setRecruitType("SOCIAL"); + excel.setCandEdu("本科"); + excel.setCandId(candName.equals("张三") ? "110105199001010010" : "110105199002020026"); + excel.setCandGrad("202110"); + excel.setCandSchool("四川大学"); + excel.setCandMajor("法学"); + return excel; + } + + private CcdiStaffRecruitmentWorkExcel buildWorkExcel(String recruitId, String candName) { + CcdiStaffRecruitmentWorkExcel workRow = new CcdiStaffRecruitmentWorkExcel(); + workRow.setRecruitId(recruitId); + workRow.setCandName(candName); + workRow.setRecruitName("社会招聘项目"); + workRow.setPosName("Java工程师"); + workRow.setSortOrder(1); + workRow.setCompanyName("测试科技"); + workRow.setPositionName("开发工程师"); + workRow.setJobStartMonth("2022-01"); + return workRow; + } } diff --git a/docs/reports/implementation/2026-05-07-staff-recruitment-add-work-experience.md b/docs/reports/implementation/2026-05-07-staff-recruitment-add-work-experience.md new file mode 100644 index 00000000..380fa63e --- /dev/null +++ b/docs/reports/implementation/2026-05-07-staff-recruitment-add-work-experience.md @@ -0,0 +1,44 @@ +# 招聘信息新增弹窗工作经历维护实施记录 + +## 背景 + +招聘信息维护页面的编辑弹窗已支持维护社招候选人的历史工作经历,但新增弹窗隐藏了同一维护区域,且新增提交时未携带 `workExperienceList`,导致新增招聘记录时无法同步维护候选人工作经历。 + +## 修改内容 + +- 前端 `ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue` + - 将候选人历史工作经历维护区域从“仅编辑弹窗展示”调整为“社招新增和编辑弹窗均展示”。 + - 新增提交时保留 `workExperienceList`,不再删除工作经历数据。 + - 工作经历校验同步覆盖新增和编辑场景,仍仅对社招记录生效。 + +- 后端 `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiStaffRecruitmentAddDTO.java` + - 新增 `workExperienceList` 入参,并使用 `@Valid` 复用现有工作经历字段校验。 + +- 后端 `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java` + - 新增招聘信息保存后,如为社招且提交了工作经历,按新增记录主键写入 `ccdi_staff_recruitment_work`。 + - 复用编辑场景的工作经历实体构造逻辑,保持字段、排序号和必填过滤规则一致。 + +## 影响范围 + +- 仅影响招聘信息维护页面的新增弹窗和新增接口。 +- 编辑弹窗、详情展示、导入功能和校招新增逻辑不改变。 + +## 验证记录 + +- 后端相关模块编译通过: + - `mvn -pl ccdi-info-collection -am test -DskipTests` +- 前端生产构建通过: + - `cd ruoyi-ui && source ~/.nvm/nvm.sh && nvm use && node -v && npm run build:prod` + - 使用 Node `v14.21.3`,构建仅保留既有资源体积 warning。 +- 真实接口新增与编辑验证通过: + - 新增社招招聘记录 `AUTO-WORK-202605070053`,新增详情回查 `workExperienceList` 为 1 条。 + - 编辑同一条招聘记录,工作经历由 1 条覆盖为 2 条,详情回查包含 `编辑后公司B`、`编辑新增公司C`。 +- 主键关联数据库回查通过: + - 使用 `bin/mysql_utf8_exec.sh output/sql/2026-05-07-staff-recruitment-work-pk-check.sql` 回查,主表自增主键 `id=6006` 下关联 2 条子表记录。 + - 回查 `work_links` 为 `6006:1:编辑后公司B | 6006:2:编辑新增公司C`,`orphan_work_count=0`。 +- 真实页面验证通过: + - 使用 `browser-use` 打开真实页面 `http://localhost:1025/maintain/staffRecruitment`。 + - 新增弹窗已展示“候选人历史工作经历”区域,点击“新增经历”后出现工作单位、入职时间等输入列。 + - 编辑同一条测试记录时,编辑弹窗按主键回显 2 条历史工作经历,列表页显示历史工作经历为 `2段`。 +- 清理验证通过: + - 测试结束后调用删除接口清理 `id=6006`,再次数据库回查 `orphan_work_count=0`,无测试工作经历孤儿数据残留。 diff --git a/docs/reports/implementation/2026-05-07-staff-recruitment-allow-duplicate-recruit-id.md b/docs/reports/implementation/2026-05-07-staff-recruitment-allow-duplicate-recruit-id.md new file mode 100644 index 00000000..46706602 --- /dev/null +++ b/docs/reports/implementation/2026-05-07-staff-recruitment-allow-duplicate-recruit-id.md @@ -0,0 +1,43 @@ +# 招聘项目编号允许重复实施记录 + +## 保存路径确认 + +- 实施记录保存路径:`docs/reports/implementation/2026-05-07-staff-recruitment-allow-duplicate-recruit-id.md` +- 本次为招聘信息主从表关联修正,使用实施记录目录保存。 + +## 修改内容 + +1. 移除新增、编辑、导入时对 `recruit_id` 的重复拦截,招聘项目编号允许重复。 +2. `ccdi_staff_recruitment_work` 新增 `recruitment_id`,历史工作经历改为关联招聘主表自增 `id`。 +3. 列表历史工作经历条数、详情历史工作经历、编辑保存和删除清理均改为按 `recruitment_id` 处理。 +4. 导入模板字段保持不变,历史工作经历导入时通过“招聘项目编号 + 候选人姓名 + 招聘项目名称 + 职位名称”匹配主记录;匹配多条时返回失败,避免错误归属。 +5. 更新初始化 SQL、增量 SQL、数据库字段说明和 API 文档。 + +## 影响范围 + +- 后端服务:招聘信息 CRUD、双 Sheet 导入、历史工作经历查询与清理。 +- 数据库表:`ccdi_staff_recruitment`、`ccdi_staff_recruitment_work`。 +- 前端页面:接口仍传主表 `id`,页面字段无新增。 +- 导入功能:模板不新增主键列。 + +## 验证情况 + +1. 数据库迁移已执行: + - `sh bin/mysql_utf8_exec.sh sql/migration/2026-05-07-allow-duplicate-staff-recruitment-id.sql` + - 回查确认 `ccdi_staff_recruitment.recruit_id` 为普通索引,`ccdi_staff_recruitment_work.recruitment_id` 为非空字段并已建立索引。 +2. 后端定向测试通过: + - `mvn -pl ccdi-info-collection -am -Dtest=CcdiStaffRecruitmentImportServiceImplTest,CcdiStaffRecruitmentDualImportContractTest -Dsurefire.failIfNoSpecifiedTests=false test` +3. 前端构建通过: + - `cd ruoyi-ui && nvm use && npm run build:prod` + - 构建仅保留既有资源体积 warning。 +4. 真实页面验证: + - 已按项目规则优先尝试 `browser-use`,但当前 Codex 环境没有可用 in-app browser pane,运行时返回 `No active Codex browser pane available`。 + - 使用本机 Playwright 兜底打开真实页面 `http://localhost:8080/maintain/staffRecruitment` 验证。 + - 通过接口新增两条相同招聘项目编号 `RC-DUP-20260507003554` 的招聘记录,均返回操作成功。 + - 给第一条记录插入 1 条历史工作经历后,页面列表展示两条同编号记录:`TestA` 为 `1段`,`TestB` 为 `0段`。 + - 打开 `TestA` 详情可见 `Company A` 历史工作经历;打开 `TestB` 详情显示“暂无历史工作经历”,未发生串数据。 + - 后端日志确认列表按 `recruitment_id` 聚合历史工作经历,详情查询分别按 `recruitment_id = 6003` 与 `recruitment_id = 6004` 查询从表。 +5. 清理情况: + - 已删除本轮测试主表和从表造数。 + - 已关闭 Playwright 浏览器。 + - 已停止本轮启动的前端 `8080` 与后端 `62318` 进程,端口回查无监听。 diff --git a/docs/reports/implementation/2026-05-07-staff-recruitment-auto-id-primary-key.md b/docs/reports/implementation/2026-05-07-staff-recruitment-auto-id-primary-key.md new file mode 100644 index 00000000..27ca7fb3 --- /dev/null +++ b/docs/reports/implementation/2026-05-07-staff-recruitment-auto-id-primary-key.md @@ -0,0 +1,47 @@ +# 招聘信息自增主键实施记录 + +## 保存路径确认 + +- 实施记录保存路径:`docs/reports/implementation/2026-05-07-staff-recruitment-auto-id-primary-key.md` +- 本次为招聘信息主键结构调整,使用实施记录目录保存。 + +## 修改内容 + +1. `ccdi_staff_recruitment` 新增 `id` 自增主键,`recruit_id` 调整为普通业务编号。 +2. 后端实体 `CcdiStaffRecruitment` 的 MyBatis Plus 主键从 `recruitId` 切换为 `id`。 +3. 招聘信息详情、编辑、删除接口改为按 `id` 定位记录。 +4. 招聘信息列表和详情 SQL 返回 `id` 字段,前端列表行操作改为传递 `id`。 +5. 导入链路继续在模板中填写 `recruit_id`,后续补充改为由后端解析到招聘主表 `id`。 +6. 补充迁移脚本 `sql/migration/2026-05-07-add-staff-recruitment-auto-id.sql`,同步更新初始化 SQL 与数据库字段说明。 +7. 同步更新招聘信息 API 文档中详情、编辑、删除的主键参数说明。 + +## 影响范围 + +- 后端接口:`/ccdi/staffRecruitment/{id}`、`PUT /ccdi/staffRecruitment`、`DELETE /ccdi/staffRecruitment/{ids}` +- 前端页面:`ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue` +- 数据库表:`ccdi_staff_recruitment` +- 导入功能:招聘主信息与历史工作经历模板字段不变,后续补充改为落库时关联招聘主表 `id`。 + +## 数据库执行 + +- 已通过 `bin/mysql_utf8_exec.sh sql/migration/2026-05-07-add-staff-recruitment-auto-id.sql` 执行本地 dev 库迁移。 +- 回查结果: + - `id`:`PRIMARY`,`auto_increment` + - `recruit_id`:普通索引,允许重复。 + +## 验证情况 + +1. 后端定向测试通过: + - `mvn -pl ccdi-info-collection -am -Dtest=CcdiStaffRecruitmentImportServiceImplTest,CcdiStaffRecruitmentDualImportContractTest -Dsurefire.failIfNoSpecifiedTests=false test` +2. 前端生产构建通过: + - `cd ruoyi-ui && nvm use && npm run build:prod` + - 构建仅保留既有资源体积 warning。 +3. 后端重启验证通过: + - `sh bin/restart_java_backend.sh restart` + - 应用启动成功。 +4. 真实页面验证通过: + - 使用 browser-use 打开 `http://localhost:8080/maintain/staffRecruitment` + - 登录后招聘信息维护列表正常展示。 + - 点击 `RC2025001001` 详情,后端日志显示详情 SQL 使用 `WHERE id = ?` 查询主表。 + - 历史工作经历后续补充改为使用招聘主表 `id` 查询,避免编号重复时串数据。 +5. 测试后已关闭本轮启动的前端 `8080` 与后端 `62318` 进程,端口回查无监听。 diff --git a/ruoyi-ui/src/api/ccdiStaffRecruitment.js b/ruoyi-ui/src/api/ccdiStaffRecruitment.js index b0876c58..f1e1c9ab 100644 --- a/ruoyi-ui/src/api/ccdiStaffRecruitment.js +++ b/ruoyi-ui/src/api/ccdiStaffRecruitment.js @@ -10,9 +10,9 @@ export function listStaffRecruitment(query) { } // 查询招聘信息详细 -export function getStaffRecruitment(recruitId) { +export function getStaffRecruitment(id) { return request({ - url: '/ccdi/staffRecruitment/' + recruitId, + url: '/ccdi/staffRecruitment/' + id, method: 'get' }) } @@ -36,9 +36,9 @@ export function updateStaffRecruitment(data) { } // 删除招聘信息 -export function delStaffRecruitment(recruitIds) { +export function delStaffRecruitment(ids) { return request({ - url: '/ccdi/staffRecruitment/' + recruitIds, + url: '/ccdi/staffRecruitment/' + ids, method: 'delete' }) } diff --git a/ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue b/ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue index 9ceb8805..71fdbd54 100644 --- a/ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue +++ b/ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue @@ -317,10 +317,10 @@ -