# 招聘信息管理双 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)` 2. `importRecruitmentWork(List)` 对应的异步实现也拆成: 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` - `历史工作经历` -> `List` 若两个 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、失败行号和失败原因