Files
ccdi/docs/design/2026-04-23-staff-recruitment-dual-sheet-import-design.md

12 KiB
Raw Blame History

招聘信息管理双 Sheet 导入设计文档

1. 背景

当前招聘信息管理已经具备以下能力:

  • 主信息维护:ccdi_staff_recruitment
  • 历史工作经历维护:ccdi_staff_recruitment_work
  • 页面编辑态支持手工维护历史工作经历
  • 导入能力仍拆分为两条独立入口:
    • 招聘信息导入
    • 历史工作经历导入

对比已经完成改造的招投标信息维护,招聘信息管理当前导入模式存在三个问题:

  1. 导入入口与当前主从结构不一致,用户需要自行判断先后顺序
  2. 双 Sheet 场景无法在同一个文件中同时提交主信息和工作经历
  3. 失败记录无法直接定位失败来源 Sheet 和 Excel 行号

本次需求要求参考招投标信息维护的双 Sheet 模式,将招聘信息管理改造成单入口双 Sheet 导入,并补齐失败定位信息。

2. 目标与范围

2.1 目标

  1. 招聘信息管理页面只保留一个导入按钮
  2. 导入模板改为双 Sheet
    • 招聘信息
    • 历史工作经历
  3. 后端统一用一个异步任务处理整份文件
  4. 支持以下三类导入方式:
    • 只导 招聘信息 Sheet
    • 只导 历史工作经历 Sheet
    • 两个 Sheet 同时导入
  5. 失败列表必须展示:
    • 失败 Sheet
    • 失败行号
    • 失败原因

2.2 已确认业务口径

  1. 页面导入入口合并为单按钮
  2. 历史工作经历 Sheet 允许独立导入
  3. 历史工作经历 独立导入时,继续按库内已存在的招聘主信息匹配
  4. 如果某个招聘记录已经存在历史工作经历,再次导入该招聘记录的工作经历时直接报错,不允许覆盖旧数据
  5. 失败展示统一为一个列表,通过 失败Sheet失败行号失败原因 定位问题

2.3 非目标

  1. 不调整招聘信息手工新增、编辑、详情、列表查询主体逻辑
  2. 不调整编辑页中手工维护历史工作经历的交互
  3. 不新增数据库表,不修改现有表结构
  4. 不保留旧的“导入工作经历”按钮或旧接口作为兼容入口
  5. 不新增独立任务中心或额外状态体系

3. 现状分析

3.1 前端现状

招聘信息管理页面当前有两个导入按钮:

  1. 导入
  2. 导入工作经历

两个按钮分别对应不同模板和不同上传接口:

  • /ccdi/staffRecruitment/importTemplate
  • /ccdi/staffRecruitment/workImportTemplate
  • /ccdi/staffRecruitment/importData
  • /ccdi/staffRecruitment/importWorkData

页面当前仅维护一套失败记录弹窗,但会根据 currentImportType 切换展示字段,本质仍然是两条独立导入链路。

3.2 后端现状

当前招聘导入服务拆成两条独立任务:

  1. importRecruitment(List<CcdiStaffRecruitmentExcel>)
  2. importRecruitmentWork(List<CcdiStaffRecruitmentWorkExcel>)

对应的异步实现也拆成:

  1. importRecruitmentAsync(...)
  2. importRecruitmentWorkAsync(...)

其中历史工作经历导入现有语义为:

  • 允许独立导入
  • recruitId 匹配招聘主信息
  • 若同一 recruitId 任一行失败,则整组不落库
  • 导入前会删除该 recruitId 旧工作经历,再导入新数据

本次要调整的是最后一条覆盖语义:改为“已有旧记录则失败,不覆盖”。

4. 方案对比

4.1 方案 A单入口 + 单任务编排 + 单失败列表

做法:

  • 页面只保留一个导入按钮
  • 后端一次读取双 Sheet
  • 用一个异步任务统一处理主信息和工作经历
  • 用一个失败列表展示全部失败记录

优点:

  1. 与招投标信息维护双 Sheet 模式一致
  2. 支持同文件主从联动导入
  3. 失败展示口径最统一
  4. 页面交互最简单

缺点:

  1. 需要把当前两条招聘导入任务重构为一条统一任务

4.2 方案 B单入口 + 双任务复用 + 前端聚合失败

做法:

  • 上传一个双 Sheet 文件
  • 后端仍拆成主信息任务和工作经历任务
  • 前端再聚合两个任务的失败记录

缺点:

  1. 工作经历 Sheet 无法天然引用“本次主 Sheet 刚成功导入的数据”
  2. 会引入跨任务依赖和前端拼装逻辑
  3. 导入状态与失败记录口径不够干净

4.3 方案 C仅合并模板和按钮不做真实双 Sheet 联动

做法:

  • 页面只有一个导入按钮
  • 模板做成双 Sheet
  • 但工作经历仍只能引用库内已有招聘主信息

缺点:

  1. 无法完成同文件主从联动
  2. 不能真正对齐招投标信息维护的双 Sheet 模式
  3. 业务理解成本仍然偏高

4.4 最终选择

采用方案 A单入口 + 单异步任务 + 单失败列表

理由:

  1. 最符合“参考招投标信息维护双 Sheet 导入”的目标
  2. 业务链路闭环最完整
  3. 用户侧交互最清晰
  4. 后续维护成本最低

5. 总体设计

5.1 模板设计

统一模板文件名:招聘信息管理导入模板

模板包含两个 Sheet

  1. 招聘信息
  2. 历史工作经历

约束:

  1. 两个 Sheet 都允许为空表头,但整份文件至少一个 Sheet 有数据
  2. 历史工作经历 Sheet 保持当前字段结构,不新增业务字段
  3. 招聘信息 Sheet 保持当前字段结构,不新增业务字段

5.2 页面交互设计

页面顶部仅保留一个 导入 按钮。

上传弹窗说明文案明确提示:

  • 模板包含 招聘信息历史工作经历 两个 Sheet
  • 支持只填写一个 Sheet也支持两个 Sheet 同时填写

上传成功后:

  1. 只返回一个 taskId
  2. 前端只启动一套轮询
  3. 页面只保留一个“查看导入失败记录”入口

5.3 接口设计

保留并调整以下接口:

  1. POST /ccdi/staffRecruitment/importTemplate
    • 输出双 Sheet 模板
  2. POST /ccdi/staffRecruitment/importData
    • 一次接收整份双 Sheet 文件
    • 返回统一 taskId
  3. GET /ccdi/staffRecruitment/importStatus/{taskId}
    • 查询统一任务状态
  4. GET /ccdi/staffRecruitment/importFailures/{taskId}
    • 查询统一失败记录

移除以下独立导入入口:

  1. POST /ccdi/staffRecruitment/workImportTemplate
  2. POST /ccdi/staffRecruitment/importWorkData

6. 后端处理流程设计

6.1 任务编排

/importData 收到文件后,一次读取两个 Sheet

  • 招聘信息 -> List<CcdiStaffRecruitmentExcel>
  • 历史工作经历 -> List<CcdiStaffRecruitmentWorkExcel>

若两个 Sheet 都为空,直接返回错误:至少需要一条数据

否则初始化一条统一导入任务,并异步执行以下两个阶段:

  1. 阶段一:处理 招聘信息 Sheet
  2. 阶段二:处理 历史工作经历 Sheet

6.2 阶段一:招聘信息 Sheet

沿用现有主信息导入规则:

  1. 招聘记录编号不能为空
  2. 招聘项目名称、职位名称、职位类别、职位描述、候选人姓名、学历、证件号码、毕业院校、专业、毕业年月、录用情况不能为空
  3. 证件号码格式必须合法
  4. 毕业年月必须为 YYYYMM
  5. 录用情况只能填写合法枚举
  6. 招聘类型继续按现有逻辑推断
  7. 数据库中已存在的 recruitId 直接失败
  8. 文件内重复 recruitId 直接失败

阶段一成功后,建立“本次主 Sheet 成功导入的招聘记录映射”,供阶段二匹配使用。

6.3 阶段二:历史工作经历 Sheet

6.3.1 匹配规则

工作经历按 recruitId 分组处理。

每组匹配顺序固定为:

  1. 先匹配本次 招聘信息 Sheet 中成功导入的招聘主信息
  2. 再匹配数据库中已存在的招聘主信息

若都匹配不到,整组失败。

6.3.2 基础校验

沿用并收敛现有规则:

  1. recruitId 不能为空
  2. 候选人姓名不能为空
  3. 招聘项目名称不能为空
  4. 职位名称不能为空
  5. 排序号不能为空且必须大于 0
  6. 工作单位不能为空
  7. 岗位不能为空
  8. 入职年月不能为空,格式必须为 YYYY-MM
  9. 离职年月若有值,格式必须为 YYYY-MM
  10. 候选人姓名、招聘项目名称、职位名称必须与匹配到的招聘主信息一致
  11. 匹配到的招聘主信息 recruitType 必须为 SOCIAL
  12. 同一 recruitId 下,文件内 sortOrder 不能重复

6.3.3 已有工作经历报错规则

若数据库中某个 recruitId 已经存在任意历史工作经历记录,则本次再导入该 recruitId 的工作经历时:

  1. 直接整组失败
  2. 不删除旧记录
  3. 不做覆盖
  4. 不做部分追加

失败原因统一返回明确文案,例如:

招聘记录编号[xxx]已存在历史工作经历,不允许重复导入

6.3.4 分组成功与失败策略

recruitId 维度整组处理:

  1. 若该组任意一行失败,则整组不落库
  2. 若该组全部通过校验,则整组批量插入

这样可以保证同一招聘记录下的工作经历要么整组成功,要么整组失败,不会出现排序半成功半失败的问题。

6.4 失败记录结构

统一失败记录对象新增以下字段:

  1. sheetName
  2. sheetRowNum
  3. errorMessage

并继续保留必要业务定位字段:

  1. recruitId
  2. recruitName
  3. posName
  4. candName
  5. candId
  6. companyName
  7. positionName

其中:

  • 主 Sheet 失败时,sheetName = 招聘信息
  • 工作经历 Sheet 失败时,sheetName = 历史工作经历
  • sheetRowNum 返回 Excel 实际数据行号

7. 前端展示设计

7.1 导入入口

页面只保留一个导入按钮,不再保留 导入工作经历

7.2 轮询状态

前端本地只维护一条最近导入任务状态:

  1. taskId
  2. status
  3. hasFailures
  4. totalCount
  5. successCount
  6. failureCount

不再按导入类型区分任务状态。

7.3 失败弹窗

失败弹窗统一展示一个列表,至少包含以下列:

  1. 失败Sheet
  2. 失败行号
  3. 招聘记录编号
  4. 招聘项目名称
  5. 职位名称
  6. 候选人姓名
  7. 工作单位
  8. 失败原因

展示规则:

  1. 主 Sheet 失败时,工作单位 为空
  2. 工作经历 Sheet 失败时,工作单位 有值
  3. 失败行号 展示为 第X行

8. 影响范围

8.1 后端

  • ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffRecruitmentController.java
  • ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentService.java
  • ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffRecruitmentImportService.java
  • ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentServiceImpl.java
  • ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffRecruitmentImportServiceImpl.java
  • ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/RecruitmentImportFailureVO.java

8.2 前端

  • ruoyi-ui/src/views/ccdiStaffRecruitment/index.vue
  • ruoyi-ui/src/api/ccdiStaffRecruitment.js

8.3 不影响范围

  1. 招聘详情页展示结构
  2. 招聘手工新增、编辑、删除接口
  3. 数据库结构与 SQL 迁移
  4. 路由、菜单、权限标识

9. 验证方案

9.1 后端验证

  1. mvn -pl ccdi-info-collection,ruoyi-admin -am -DskipTests compile

9.2 前端验证

  1. source ~/.nvm/nvm.sh && nvm use 14.21.3 >/dev/null && cd ruoyi-ui && npm run build:prod

9.3 页面实测

必须通过真实页面下载模板后再构造测试文件,至少覆盖:

  1. 只导 招聘信息 Sheet 成功
  2. 只导 历史工作经历 Sheet匹配库内已有主信息成功
  3. 双 Sheet 同时导入,工作经历引用本次主 Sheet 成功
  4. 工作经历命中已存在旧记录时报错
  5. 工作经历命中非社招主信息时报错
  6. 文件内工作经历排序号重复时报错
  7. 失败弹窗正确展示 失败Sheet失败行号失败原因

测试结束后需关闭测试过程中启动的前后端进程。

10. 结论

本次采用“单入口 + 双 Sheet 模板 + 单异步任务 + 单失败列表”的最短路径实现:

  1. 页面导入入口统一为一个按钮
  2. 双 Sheet 模板统一为 招聘信息 + 历史工作经历
  3. 工作经历继续允许独立导入
  4. 工作经历匹配顺序为“本次主 Sheet 成功数据优先,其次数据库已有主信息”
  5. 数据库里已存在工作经历时,本次重复导入直接报错,不覆盖旧数据
  6. 失败列表统一展示失败 Sheet、失败行号和失败原因