# 简历信息模块开发记录 **日期:** 2026-06-04 ## 模块概述 简历信息模块(resume)是招聘管理系统中个人基本信息模块的子模块,负责管理求职者的简历信息及其关联的子表数据。该模块前端基于 Vue3 + Ant Design Vue,采用 JeecgBoot 的前后端分离架构。 ### 业务关系 - **ResumeInfo(简历信息)**:关联表,与个人信息关联,记录简历概要(简历名称、是否公开、有效期、是否默认简历、个人优势、其他证书) - **简历子表(8个)**:求职意向、工作经历、教育经历、培训经历、职业技能、职称情况、获奖情况、留学经历,与简历形成一对多关系 --- ## 模块文件结构 ### 前端(jeecgboot-vue3/src/views/recruitment/personal/) | 文件 | 用途 | |----------------------------------|----------------------------------| | `components/ResumeInfoForm.vue` | 简历信息表单组件,包含主表字段和 8 个子表 JVxeTable | | `components/ResumeInfoModal.vue` | 简历信息弹窗组件,包裹 ResumeInfoForm | | `components/ResumeListModal.vue` | 简历列表弹窗组件,查看个人信息关联的简历列表 | ### 后端(jeecg-boot-module/jeecg-module-zjrs/.../org/jeecg/modules/zjrs/personal/) | 层次 | 文件 | |--------------|-------------------------------------------| | Service Impl | `service/impl/ResumeInfoServiceImpl.java` | --- ## 开发记录 ### 功能开发 | 日期 | 内容 | 涉及文件 | 说明 | |------------|----------|------------------------------|-------------------------------------| | 2026-06-04 | 默认简历互斥逻辑 | `ResumeInfoServiceImpl.java` | 新增/编辑简历时,若设置当前简历为默认,则同人员其他简历自动置为非默认 | #### 2026-06-04:默认简历互斥逻辑 **需求描述:** 一个求职者只能有一份默认简历。在新增或编辑简历时,如果用户将当前简历的"是否默认简历"设置为"是"( `isDefault = "1"`),保存成功后系统应自动将该人员的其他所有简历的默认状态清除(置为 `"0"`),确保同一人员下最多只有一份默认简历。 **实现方案:** 在 `ResumeInfoServiceImpl` 中新增私有方法 `clearOtherDefaultResumes()`,使用 MyBatis-Plus 的 `lambdaUpdate()` 按人员ID匹配、排除当前操作的简历ID,将匹配到的其他简历的 `isDefault` 字段批量更新为 `"0"`。在 `saveMainAndSub()` 和 `updateMainAndSub()` 方法中,保存或更新主表成功后,判断 `dto.getIsDefault() == "1"` 时调用该方法。 **涉及文件:** - `jeecg-boot-module/jeecg-module-zjrs/.../personal/service/impl/ResumeInfoServiceImpl.java` **主要逻辑代码:** ```java // saveMainAndSub() 中:保存主表后调用 if ("1".equals(dto.getIsDefault())) { clearOtherDefaultResumes(dto.getPersonalId(), main.getId()); } // updateMainAndSub() 中:更新主表后调用 if ("1".equals(dto.getIsDefault())) { clearOtherDefaultResumes(dto.getPersonalId(), main.getId()); } /** * 清除指定人员下除当前简历外的其他简历的默认状态 */ private void clearOtherDefaultResumes(String personalId, String excludeResumeId) { this.lambdaUpdate() .eq(ResumeInfo::getPersonalId, personalId) .ne(ResumeInfo::getId, excludeResumeId) .set(ResumeInfo::getIsDefault, "0") .update(); } ``` ### Bug 修复 #### 2026-06-04:详情查看时表单未禁用修复(先编辑后详情场景) **问题描述:** 在简历列表弹窗(ResumeListModal)中,先点击"修改"后关闭弹窗,再点击"查看"时,表单字段和子表编辑功能未处于禁用/只读状态。 **复现条件:** 页面刷新后先点修改再点查看才会出现;页面刷新后直接点查看则正常禁用。 **原因分析:** Ant Design Vue 的 `` 默认 `destroyOnClose=false`,弹窗关闭时内部 ResumeInfoForm 组件仍然挂载在 DOM 中。 - 先点击"修改"时,ResumeInfoForm 以 `formDisabled=false` 的状态挂载 - 关闭弹窗后组件保持挂载 - 再点击"查看"时,虽然 `disableSubmit` 被设为 `true`,但由于组件实例未重新创建,props 的响应式更新未能正确触发 JFormContainer 的 `
` 状态变更 **修复方案:** 使用 `:key` 方式强制组件重建(与 PersonalInfoModal 相同方案)。 - 在 ResumeInfoModal 中引入 `formKey` ref 和 `nextFormKey()` 方法 - 在 `` 上绑定 `:key="formKey"` - 每次打开弹窗(add/edit)时先调用 `nextFormKey()` 递增 key,触发 Vue 重建组件实例 - 由于组件始终挂载在 DOM 中(非 `v-if` 控制),ref 在重建完成后自然可用 **涉及文件:** - `jeecgboot-vue3/src/views/recruitment/personal/components/ResumeInfoModal.vue` - 新增 `formKey` ref(初始值 0) - 新增 `nextFormKey()` 方法,递增 `formKey` - `` 上添加 `:key="formKey"` 绑定 - `add()` 和 `edit()` 方法中在设置 `visible=true` 之前调用 `nextFormKey()` #### 2026-06-04:日期转换检查 **问题描述:** 检查 ResumeInfoForm 中是否存在与 PersonalInfoForm 相同的日期字符串未转为 dayjs 对象的问题。 **检查结果:** ResumeInfoForm.vue 中日期字段已正确处理,无需修复。 - `validDate` 字段在 `edit()` 方法中已将字符串转为 `dayjs()` 对象 - 子表日期字段(`startDate`、`endDate`、`issueDate`)已通过 `convertDateFields()` 工具方法统一转换 - ResumeListModal.vue 仅作为列表容器,不直接管理表单状态,无此问题 #### 2026-06-04:求职意向从子表迁移至主表 **需求描述:** 求职意向(expectedIndustry、expectedPosition、positionName、expectedSalary、workLocation、workNature)之前被错误地设计为子表(RESUME_INTENTION),实际需求中这些字段应属于简历主表(RESUME_INFO)的一对一字段,不存在一对多关系。 **修复方案:** 1. **SQL** — 将求职意向6个字段移入 RESUME_INFO 主表,删除 RESUME_INTENTION 子表 DDL 2. **实体类** — 在 ResumeInfo.java 中新增求职意向相关字段(含 BigDecimal 类型的 expectedSalary) 3. **DTO/VO** — ResumeInfoDTO 新增求职意向字段并移除 `resumeIntentionList`;ResumeInfoPage 同步调整 4. **Service** — ResumeInfoServiceImpl 移除 `resumeIntentionMapper` 的注入及 save/update/delete 中的子表 CRUD 代码 5. **Controller** — 移除 `resumeIntentionMapper` 注入、`queryIntentionListByMainId` 接口、`queryById` 中的意向列表查询 6. **删除文件** — 删除 ResumeIntention.java、ResumeIntentionMapper.java、ResumeIntentionMapper.xml、IResumeIntentionService.java、ResumeIntentionServiceImpl.java 共5个文件 7. **前端表单** — ResumeInfoForm.vue 移除求职意向 JVxeTable 子表 tab,在原有主表行区域末尾添加6个求职意向表单字段;移除 intentionColumns 导入、intentionTableRef、intentionData、computedIntentionColumns 等 8. **前端列定义** — ResumeInfo.data.ts 移除 intentionColumns 导出 **涉及文件:** - `.docs/sql/个人信息与简历信息.sql` — 求职意向字段合并入主表,删除子表 DDL - `jeecg-boot-module/jeecg-module-zjrs/.../personal/entity/ResumeInfo.java` — 新增6个求职意向字段 - `jeecg-boot-module/jeecg-module-zjrs/.../personal/vo/ResumeInfoDTO.java` — 新增求职意向字段,移除 `resumeIntentionList` - `jeecg-boot-module/jeecg-module-zjrs/.../personal/vo/ResumeInfoPage.java` — 新增求职意向字段,移除 `resumeIntentionList` - `jeecg-boot-module/jeecg-module-zjrs/.../personal/service/impl/ResumeInfoServiceImpl.java` — 移除意向子表注入及 CRUD - `jeecg-boot-module/jeecg-module-zjrs/.../personal/controller/ResumeInfoController.java` — 移除意向子表注入及接口 - `jeecgboot-vue3/src/views/recruitment/personal/components/ResumeInfoForm.vue` — 移除意向子表 tab,新增6个主表表单字段 - `jeecgboot-vue3/src/views/recruitment/personal/ResumeInfo.data.ts` — 移除 intentionColumns 导出 - 删除文件:ResumeIntention.java、ResumeIntentionMapper.java、ResumeIntentionMapper.xml、IResumeIntentionService.java、ResumeIntentionServiceImpl.java #### 2026-06-04:RESUME_INFO 主表 NOT NULL 字段前后端校验补全 **需求描述:** 按照框架开发规范([后端规范](260603-后端框架全局开发规范与最佳实践.md) 第七节 + [前端规范](260603-前端框架全局开发规范与最佳实践.md)第四节),对 RESUME_INFO 表中所有 NOT NULL 字段,前后端均应有对应的数据校验。 **检查结果:** 以下6个 NOT NULL 字段(排除自增主键 ID 和自动填充的 CREATE_TIME/UPDATE_TIME)均缺少校验: | 字段 | SQL | 后端JSR303 | 前端required | |------------------|----------|----------|------------------| | personalId | NOT NULL | 缺失 | 缺失(系统内部填充,非用户输入) | | expectedPosition | NOT NULL | 缺失 | 缺失 | | positionName | NOT NULL | 缺失 | 缺失 | | expectedSalary | NOT NULL | 缺失 | 缺失 | | workLocation | NOT NULL | 缺失 | 缺失 | | workNature | NOT NULL | 缺失 | 缺失 | **修复方案:** 1. **ResumeInfoDTO.java** — 新增 `import jakarta.validation.constraints.NotBlank/NotNull`,对 NOT NULL 字段添加注解: - String 字段 → `@NotBlank(message = "...")` - BigDecimal 字段 → `@NotNull(message = "...")` 2. **ResumeInfoController.java** — `add()` 和 `edit()` 方法的 DTO 参数添加 `@Validated` 注解,触发 JSR303 校验 3. **ResumeInfoForm.vue** — `validatorRules` 新增 expectedPosition、positionName、expectedSalary、workLocation、workNature 的 `required: true` 规则 **涉及文件:** - `jeecg-boot-module/jeecg-module-zjrs/.../personal/vo/ResumeInfoDTO.java` — 新增 JSR303 注解及 import - `jeecg-boot-module/jeecg-module-zjrs/.../personal/controller/ResumeInfoController.java` — add/edit 加 @Validated - `jeecgboot-vue3/src/views/recruitment/personal/components/ResumeInfoForm.vue` — validatorRules 新增5个必填规则 #### 2026-06-04:子表区域从Tabs改为纵向依次排列 **需求描述:** 简历表单中子表区域原来使用 `` 组件以标签页方式展示7个子表(工作经历、教育经历等),新需求改为不使用 tabs,所有子表纵向依次排列,各子表之间用 `` 分隔。 **修复方案:** 在 ResumeInfoForm.vue 中: - 模板:移除 `` 及 `` 包裹层,每个子表的标题改用 `` 显示,JVxeTable 直接放在下方 - 脚本:移除不再需要的 `activeKey` ref 和 `handleChangeTabs()` 方法(该方法用于 tab 切换时重置滚动位置,改为纵向排列后不再需要) **涉及文件:** - `jeecgboot-vue3/src/views/recruitment/personal/components/ResumeInfoForm.vue` — 移除 tabs,子表纵向排布,移除 activeKey/handleChangeTabs #### 2026-06-04:简历表单元数据字典化(主表 workNature + 子表字典字段) **需求描述:** 将简历信息表单中部分字段从普通输入框(input/radio)改为下拉选择框,选项数据来源于数据字典。涉及以下字段: **主表字段:** | 字段 | 字典编码 | 变更前 | 变更后 | |------------------|------------|--------------------|-------------------| | workNature(工作性质) | WorkNature | 硬编码 radio 组(全职/实习) | `` 下拉选择 | **子表 JVxeTable 列:** | 子表 | 列 | 字典编码 | 变更前 | 变更后 | |------|-------------------|-------------|-------------|-----------------------| | 工作经历 | companyType(单位类型) | CompanyType | `` | `JVxeTypes.slot` 下拉选择 | | 工作经历 | companySize(人员规模) | CompanySize | `` | `JVxeTypes.slot` 下拉选择 | | 教育经历 | education(学历) | Education | `` | `JVxeTypes.slot` 下拉选择 | | 教育经历 | degree(学位) | Degree | `` | `JVxeTypes.slot` 下拉选择 | | 教育经历 | eduSystem(学制) | EduSystem | `` | `JVxeTypes.slot` 下拉选择 | | 职业技能 | skillLevel(技能等级) | SkillLevel | `` | `JVxeTypes.slot` 下拉选择 | | 职称情况 | level(级别) | TitleLevel | `` | `JVxeTypes.slot` 下拉选择 | **实现方案:** 1. **列定义文件(ResumeInfo.data.ts)** — 将上述7个 JVxeTable 列的 `type` 从 `JVxeTypes.input` 改为 `JVxeTypes.slot` ,并指定对应的 `slotName`(如 `companyTypeSlot`、`educationSlot` 等),`placeholder` 同步调整为"请选择"。 2. **表单组件(ResumeInfoForm.vue)**: - 引入 `useDict` composable,预加载 `WorkNature`、`CompanyType`、`CompanySize`、`Education`、`Degree`、`EduSystem`、 `SkillLevel`、`TitleLevel` 共8个数据字典 - 通过 `computed` 为每个字典创建对应的选项列表(如 `workNatureOptions`、`companyTypeOptions` 等) - 将主表 `workNature` 字段由硬编码的 `` 改为 `` - 为每个 JVxeTable 添加插槽模板,使用 `` 绑定对应的字典选项,通过 `triggerChange` 将选中值同步回表格数据源 **涉及文件:** - `jeecgboot-vue3/src/views/recruitment/personal/ResumeInfo.data.ts` — 7个字典列由 `JVxeTypes.input` 改为 `JVxeTypes.slot`,添加 `slotName` 配置 - `jeecgboot-vue3/src/views/recruitment/personal/components/ResumeInfoForm.vue` — 引入 `useDict` 加载8个字典,主表 workNature 改为 ``,为5个 JVxeTable 添加字典下拉插槽模板 ### 2026-06-05:详情查看改用独立的 a-descriptions + a-table 详情弹窗 **问题描述:** 原简历详情查看功能复用 `ResumeInfoModal` + `ResumeInfoForm`,打开的是禁用状态的编辑表单,布局与编辑界面相同但字段不可编辑,体验与人员信息详情查看一致。 **解决方案:** 新建独立的 `ResumeInfoDetailModal` 组件,主表字段使用 `a-descriptions` 描述列表展示,子表数据使用 `a-table` 只读展示。 **实现要点:** - **基本信息区** — 使用 `a-descriptions :column="3" bordered` 展示简历名称、有效期、是否公开、是否默认简历 4 个字段 - **求职意向区** — 带 `` 分隔标题,展示期望行业、期望职位、职位名称、期望薪酬、工作地点、工作性质(字典转换)、个人优势(全宽)、证书名称(全宽) - **7个子表** — 工作经历、教育经历、培训经历、职业技能、职称情况、获奖情况、留学经历,均使用 `a-table bordered size="small"` 只读展示,字典字段(公司类型、人员规模、学历、学位、学制、技能等级、职称级别)通过 `getDictText` 转换 - 弹窗默认为全屏模式(`:fullscreen="true"`),底部仅保留「关闭」按钮 - 内容区域添加 `padding: 14px 20px` 内边距,解决内容紧贴弹窗边框问题 - 修复具名插槽 `