日期: 2026-06-04 ~ 2026-06-05 最后更新: 2026-06-15
个人基本信息模块(personal)是招聘管理系统中的核心基础模块,负责管理求职者的个人信息和简历信息。该模块采用 JeecgBoot 的前后端分离架构,前端基于 Vue3 + Ant Design Vue,后端基于 Spring Boot + MyBatis-Plus。
PersonalInfo(个人信息) 1 ── N ResumeInfo(简历信息)
│
│ 1 ── N
├── ResumeWorkExp(工作经历)
├── ResumeEducation(教育经历)
├── ResumeTraining(培训经历)
├── ResumeSkill(职业技能)
├── ResumeTitle(职称情况)
├── ResumeAward(获奖情况)
└── ResumeStudyAbroad(留学经历)
PersonalInfo(个人信息)── M:N ── PersonalTag(个人标签)
通过中间表 personal_tag_relation 关联
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/
├── controller/
│ ├── PersonalInfoController.java # 个人信息控制器 — 列表/增删改查/导入导出/标签关联
│ └── ResumeInfoController.java # 简历控制器 — 主表 CRUD + 7个子表独立 CRUD(21个接口)
├── dto/
│ ├── ImportResultItem.java # 导入结果行数据载体(行号 + 原始列值 + 错误信息)
│ ├── PersonalInfoDTO.java # 个人信息 DTO(extends PersonalInfo,新增 List<String> tagIds)
│ └── ResumeInfoDTO.java # 简历 DTO(主表字段 + 7个子表List)
├── entity/
│ ├── PersonalInfo.java # 个人信息实体(27个业务字段 + tagNames虚拟字段)
│ ├── ResumeInfo.java # 简历主表实体(14个业务字段)
│ ├── ResumeWorkExp.java # 工作经历实体
│ ├── ResumeEducation.java # 教育经历实体
│ ├── ResumeTraining.java # 培训经历实体
│ ├── ResumeSkill.java # 职业技能实体
│ ├── ResumeTitle.java # 职称情况实体
│ ├── ResumeAward.java # 获奖情况实体
│ └── ResumeStudyAbroad.java # 留学经历实体
├── mapper/
│ ├── PersonalInfoMapper.java # 个人信息 Mapper(无自定义方法)
│ ├── ResumeInfoMapper.java # 简历 Mapper(无自定义方法)
│ ├── ResumeWorkExpMapper.java # 工作经历 Mapper(含 deleteByMainId、selectByMainId)
│ ├── ResumeEducationMapper.java # 教育经历 Mapper(同上)
│ ├── ResumeTrainingMapper.java # 培训经历 Mapper(同上)
│ ├── ResumeSkillMapper.java # 职业技能 Mapper(同上)
│ ├── ResumeTitleMapper.java # 职称情况 Mapper(同上)
│ ├── ResumeAwardMapper.java # 获奖情况 Mapper(同上)
│ ├── ResumeStudyAbroadMapper.java # 留学经历 Mapper(同上)
│ └── xml/ # 9个 XML 映射文件(均为空 namespace 声明,SQL 全部在 Mapper 接口注解中)
├── service/
│ ├── IPersonalInfoService.java # 个人信息 Service 接口(6个自定义方法)
│ ├── IResumeInfoService.java # 简历 Service 接口(3个自定义方法)
│ ├── IResumeWorkExpService.java 等7个 # 子表 Service 接口(无自定义方法,纯 IService 继承)
│ └── impl/
│ ├── PersonalInfoServiceImpl.java # 个人信息 Service 实现(核心:标签保存/查询/导入导出/字典翻译/校验)
│ ├── ResumeInfoServiceImpl.java # 简历 Service 实现(主表+子表联动:先删后插策略)
│ ├── ResumeWorkExpServiceImpl.java 等7个 # 子表 Service 空实现
├── vo/
│ ├── PersonalInfoVO.java # 个人信息 VO(extends PersonalInfo,新增 String tagIds)
│ └── ResumeInfoPage.java # 简历详情 VO(主表字段 + 7个子表List)
jeecgboot-vue3/src/views/recruitment/personal/
├── PersonalInfo.api.ts # 个人信息 API(9个接口)
├── PersonalInfo.data.ts # 列表页表格列配置(14列,含年龄动态计算)
├── PersonalInfoList.vue # 个人信息列表主页面(搜索 + 表格 + 增删改查入口)
├── ResumeInfo.api.ts # 简历 API(6个接口)
├── ResumeInfo.data.ts # 子表列类型定义(7个子表的 JVxeTypes 列配置,供各模块复用)
└── components/
├── PersonalInfoModal.vue # 个人信息弹窗容器(管理 visible/title + :key 强制重建子组件)
├── PersonalInfoForm.vue # 个人信息编辑表单(30+字段 + 头像上传 + 行政区划懒加载 + 标签多选)
├── PersonalInfoDetailModal.vue # 个人信息只读详情弹窗(a-descriptions + a-image 头像)
├── PersonalInfoSelector.vue # 人员选择器输入框(可嵌入其他表单,单选/多选)
├── PersonalInfoSelectorModal.vue # 人员选择弹窗(带查询表格 + 跨页多选)
├── ResumeListModal.vue # 某人简历列表弹窗(支持快速新增)
├── ResumeInfoModal.vue # 简历弹窗容器
├── ResumeInfoForm.vue # 简历编辑表单(主表 + 7个子表本地 CRUD 操作)
├── ResumeInfoDetailModal.vue # 简历只读详情弹窗
├── ResumeWorkExpModal.vue # 工作经历弹窗(树形字典:所属行业/经济类型)
├── ResumeEducationModal.vue # 教育经历弹窗
├── ResumeTrainingModal.vue # 培训经历弹窗
├── ResumeSkillModal.vue # 职业技能弹窗
├── ResumeTitleModal.vue # 职称情况弹窗
├── ResumeAwardModal.vue # 获奖情况弹窗
└── ResumeStudyAbroadModal.vue # 留学经历弹窗(国家过滤掉中国)
| 属性 | 说明 |
|---|---|
| 路径映射 | @RequestMapping("/personal/personalInfo") |
| 继承 | JeecgController<PersonalInfo, IPersonalInfoService> |
| 方法 | HTTP | 权限 | 功能描述 |
|---|---|---|---|
queryPageList() |
GET /list |
无 | 分页查询,调用 queryPageListWithTags() 返回含标签名的 IPage<PersonalInfoVO> |
add(PersonalInfoDTO) |
POST /add |
personal:personal_info:add |
校验必填字段 → 调用 saveWithTags() 保存人员+标签关联 |
edit(PersonalInfoDTO) |
PUT /edit |
personal:personal_info:edit |
校验必填字段 → 调用 updateWithTags() 更新人员+标签关联 |
delete(String id) |
DELETE /delete |
personal:personal_info:delete |
通过 ID 删除 |
deleteBatch(String ids) |
DELETE /deleteBatch |
personal:personal_info:deleteBatch |
批量删除 |
queryById(String id) |
GET /queryById |
无 | 返回 PersonalInfoVO(含标签ID列表) |
exportXls(...) |
GET /exportXls |
personal:personal_info:exportXls |
三步导出:组装查询条件 → 查数据并填充标签+字典翻译 → AutoPoi 导出(13列) |
importExcel(...) |
POST /importExcel |
personal:personal_info:importExcel |
解析 Excel → importExcelData() 字典反向翻译+校验+保存 → 错误行返回 |
importTemplate(...) |
GET /importTemplate |
personal:personal_info:importExcel |
生成含 27 字段表头 + 示例数据的导入模板 |
导出字典翻译映射(EXPORT_DICT_FIELD_MAP):
gender→Gender, nation→Ethnicity, nationality→Nationality, education→Education, jobSeekerCategory→JobSeekerCategory, jobSearchStatus→JobSeekerStatus, dataSource→DataSource
| 属性 | 说明 |
|---|---|
| 路径映射 | @RequestMapping("/personal/resumeInfo") |
| 注入依赖 | IResumeInfoService + 7 个子表 Mapper |
主表接口:
| 方法 | HTTP | 功能描述 |
|---|---|---|
queryPageList() |
GET /list |
分页查询 |
add(ResumeInfoDTO) |
POST /add |
调用 saveMainAndSub() 保存主表+7个子表 |
addBasic(ResumeInfo) |
POST /addBasic |
快速新增简历(仅基本信息) |
edit(ResumeInfoDTO) |
PUT /edit |
调用 updateMainAndSub() 更新主表+子表(先删后插) |
editMain(ResumeInfo) |
PUT /editMain |
仅更新主表,不影响子表 |
delete(String id) |
DELETE /delete |
调用 deleteMainWithSubs() 级联删除 |
queryById(String id) |
GET /queryById |
组装 ResumeInfoPage(主表+7个子表列表) |
7 个子表独立查询接口: 每个子表提供 /queryXxxListByMainId 接口(共 7 个)
7 个子表独立 CRUD 接口(21 个,供小程序端使用): 每个子表提供 addXxx/editXxx/deleteXxx 三个接口(如 addWorkExp/editWorkExp/deleteWorkExp)
所有 Entity 共同特征:
@Data, @Accessors(chain = true), @EqualsAndHashCode(callSuper = false), implements Serializable@TableId(type = IdType.ASSIGN_ID)createBy, createTime, updateBy, updateTime, sysOrgCode数据库保留字特殊处理:
ResumeWorkExp 中的 position 字段使用 @TableField("\"POSITION\"") 转义ResumeTitle 中的 level 字段使用 @TableField("\"LEVEL\"") 转义PersonalInfo 关键字段(共 27 个业务字段):
idType(证件类型), idNumber(证件号码), fullName(姓名), gender(性别), birthDate(出生日期), nation(民族), nationality(国籍), maritalStatus(婚姻状况), education(学历), graduationDate(毕业日期), graduateSchool(毕业院校), major(专业), politicalStatus(政治面貌), workExperience(工作经验), householdType(户口性质), householdLocation(户口所在地代码), currentResidence(现居住地代码), currentAddress(现居住地址), householdAreaName(户口所在地完整路径), residenceAreaName(现居住地完整路径), jobSeekerCategory(求职人员类别), contactPhone(联系电话), email(邮箱), qqNumber(QQ号), wechatId(微信号), isOverseasTalent(是否留学人才, 1/0), skillLevel(职业技能等级), jobSearchStatus(求职状态), acceptRecommend(是否接受推荐, 1/0), avatarPath(头像路径), dataSource(数据来源, 默认 '2')
虚拟字段:
tagNames:通过 @TableField(exist = false) 标注为非数据库字段,由 Service 层的 populateTagNames() / queryPageListWithTags() 方法动态填充PersonalInfoDTO:继承 PersonalInfo,新增 List<String> tagIds,用于新增/编辑时接收前端提交的标签 ID 列表。
ResumeInfoDTO:独立的 DTO 类(非继承),包含主表字段 + 7 个子表 List:
List<ResumeWorkExp> resumeWorkExpListList<ResumeEducation> resumeEducationListList<ResumeTraining> resumeTrainingListList<ResumeSkill> resumeSkillListList<ResumeTitle> resumeTitleListList<ResumeAward> resumeAwardListList<ResumeStudyAbroad> resumeStudyAbroadList必填校验注解:personalId(@NotBlank), expectedPosition(@NotBlank), positionName(@NotBlank), expectedSalary(@NotNull), workLocation(@NotBlank), workNature(@NotBlank)
ImportResultItem:导入结果行载体。
int rowNum — Excel 行号Map<String, String> rowData — 原始列值(列名→值)String errorMsg — 错误信息(null 表示校验通过)PersonalInfoVO:继承 PersonalInfo,新增 String tagIds(逗号分隔的标签 ID 列表,用于表单回显及新增/编辑数据传递)。注意:tagNames 字段在 PersonalInfo 父类的 @TableField(exist = false) 中,VO 直接继承使用。
ResumeInfoPage:独立 VO 类,包含主表字段 + 7 个 @ExcelCollection 注解的子表 List,用于 queryById 返回完整简历信息。
| Mapper | 自定义方法 |
|---|---|
PersonalInfoMapper |
无,纯 BaseMapper 继承 |
ResumeInfoMapper |
无,纯 BaseMapper 继承 |
| 7 个子表 Mapper | 各含 2 个注解 SQL:deleteByMainId(mainId) 按简历ID删除、selectByMainId(mainId) 按简历ID查询列表 |
所有 XML 文件均为空 namespace 声明文件,所有 SQL 通过 Mapper 接口的 @Select/@Delete 注解实现。
| 依赖注入 | 用途 |
|---|---|
IDictionaryItemService |
字典数据查询(翻译) |
PersonalTagRelationMapper |
标签关联表的 Mapper |
PersonalTagMapper |
标签表的 Mapper |
DictionaryItemMapper |
字典项 Mapper(批量查询用) |
核心业务方法:
| 方法 | 事务 | 功能 |
|---|---|---|
saveWithTags(DTO) |
@Transactional |
校验证件号重复 → 保存人员 → 保存标签关联 |
updateWithTags(DTO) |
@Transactional |
校验证件号重复(排除自身)→ 更新人员 → 删旧关联 → 插入新关联 |
deleteTagRelations(personalId) |
— | 按 personalId 删除所有旧的标签关联 |
saveTagRelations(personalId, tagIds) |
— | 遍历 tagIds 逐个创建 PersonalTagRelation 并插入 |
queryPersonalInfoVOById(id) |
— | 查询 Person → 复制到 VO → 查标签关联 → 查标签名 → 设置 tagIds |
queryPageListWithTags(page, params) |
— | 分页查询 → 批量查标签关联(按 personIds 收集分组)→ 批量查标签名 → 构建 VO 分页结果(两次批量查询避免 N+1) |
populateTagNames(list) |
— | 批量填充导出数据的标签名称 |
translateDictFields(list) |
— | 导出字典翻译:批量查字典 → 构建 value→label 映射 → switch 替换字段值 |
importExcelData(list) |
— | 导入核心逻辑(见下方"导入流程") |
validateRequiredFields(entity) |
— | 校验 18 个必填字段,校验失败抛异常 |
checkIdNumberDuplicate(idNumber, excludeId) |
— | 查询同证件号数量,重复抛 JeecgBootBizTipException |
导入流程(importExcelData):
householdLocation↔householdAreaName、currentResidence↔residenceAreaName)→ 设置 dataSource="2"导入字典反向翻译映射(IMPORT_DICT_FIELD_MAP):
idType→IDType, gender→Gender, nation→Ethnicity, nationality→Nationality, maritalStatus→MaritalStatus, education→Education, politicalStatus→PoliticalStatus, workExperience→WorkExperience, householdType→HouseholdType, jobSeekerCategory→JobSeekerCategory, skillLevel→SkillLevel, jobSearchStatus→JobSeekerStatus
| 依赖注入 | 用途 |
|---|---|
| 7 个子表 Mapper | 子表 CRUD 操作 |
核心业务方法:
| 方法 | 事务 | 功能 |
|---|---|---|
saveMainAndSub(DTO) |
@Transactional |
保存主表 → 如果 isDefault="1" 则清除该人员其他简历的默认状态 → 遍历 7 个子表分别设置 resumeId 后批量插入 |
updateMainAndSub(DTO) |
@Transactional |
更新主表 → 处理默认简历逻辑 → 删除 7 个子表旧数据(deleteByMainId)→ 遍历插入 7 个子表新数据(先删后插策略) |
deleteMainWithSubs(id) |
@Transactional |
删除 7 个子表数据 → 删除主表数据 |
clearOtherDefaultResumes(personalId, resumeId) |
— | 通过 lambdaUpdate() 将同一人员下其他简历的 isDefault 设为 "0" |
全部为纯模板实现:继承 ServiceImpl<XxxMapper, XxxEntity> 实现 IXxxService,无自定义方法。
PersonalInfoList.vue(列表主页)
├── PersonalInfoModal.vue(新增/编辑弹窗容器)
│ └── PersonalInfoForm.vue(编辑表单)
├── PersonalInfoDetailModal.vue(只读详情弹窗)
└── ResumeListModal.vue(简历列表弹窗)
├── ResumeInfoModal.vue(简历编辑弹窗容器)
│ └── ResumeInfoForm.vue(简历编辑表单)
│ ├── ResumeWorkExpModal.vue
│ ├── ResumeEducationModal.vue
│ ├── ResumeTrainingModal.vue
│ ├── ResumeSkillModal.vue
│ ├── ResumeTitleModal.vue
│ ├── ResumeAwardModal.vue
│ └── ResumeStudyAbroadModal.vue
└── ResumeInfoDetailModal.vue(简历详情弹窗)
PersonalInfoSelector.vue(人员选择器输入框)
└── PersonalInfoSelectorModal.vue(人员选择弹窗)
| 函数 | HTTP | 接口路径 | 说明 |
|---|---|---|---|
list(params) |
GET | /personal/personalInfo/list |
分页查询 |
queryById(id) |
GET | /personal/personalInfo/queryById |
查询详情(含标签) |
saveOrUpdate(params, isUpdate) |
POST | /personal/personalInfo/add 或 /edit |
新增/编辑 |
deleteOne(params, onSuccess) |
DELETE | /personal/personalInfo/delete |
单个删除 |
batchDelete(params, onSuccess) |
DELETE | /personal/personalInfo/deleteBatch |
批量删除(带确认框) |
getPersonalTagListForSelect() |
GET | /tag/personalTag/listForSelect |
获取标签下拉列表 |
| 函数 | HTTP | 接口路径 | 说明 |
|---|---|---|---|
resumeList(params) |
GET | /personal/resumeInfo/list |
分页查询(含 personalId 过滤) |
queryResumeById(id) |
GET | /personal/resumeInfo/queryById |
查询简历详情(含子表) |
saveResume(params) |
POST | /personal/resumeInfo/add |
新增简历 |
editResume(params) |
PUT | /personal/resumeInfo/edit |
编辑简历 |
deleteResume(params, onSuccess) |
DELETE | /personal/resumeInfo/delete |
删除简历 |
addBasicResume(params) |
POST | /personal/resumeInfo/addBasic |
快速新增简历 |
导出 14 列表格配置,包含:
birthDate 通过 customRender 动态计算周岁)、学历(字典 Education)、民族(字典 Ethnicity)、国籍(字典 Nationality)、户口所在地(householdAreaName)、现居住地(residenceAreaName)、求职人员类别(字典 JobSeekerCategory)、联系电话、个人标签(tagNames,长文本省略号)、求职状态(字典 JobSeekerStatus)、数据来源(字典 DataSource)导出 8 个列配置数组(供 JVxeTable 等组件复用),每个数组使用 JVxeTypes 定义列类型和校验规则:
| 导出变量 | 对应子表 | 列数 | 特殊列类型 |
|---|---|---|---|
workExpColumns |
工作经历 | 10 | slot(单位类型/人员规模), textarea(工作职责/工作业绩) |
educationColumns |
教育经历 | 8 | slot(学历/学位/学制) |
trainingColumns |
培训经历 | 5 | — |
skillColumns |
职业技能 | 7 | slot(技能等级) |
titleColumns |
职称情况 | 7 | slot(级别) |
awardColumns |
获奖情况 | 8 | textarea(成果说明/其他说明) |
studyAbroadColumns |
留学经历 | 8 | — |
功能概述: 个人信息管理主页面,包含搜索区、操作工具栏、数据表格、导入导出。
查询条件: 姓名、性别、学历、户口所在地(XZQH 树形懒加载)、现居住地点(XZQH 树形懒加载)、年龄范围(ageBegin/ageEnd → 实际查询时转为 birthDate_begin/birthDate_end)、求职人员类别、求职状态、数据来源
行政区划树加载: 使用 XZQH 字典,通过 loadAreaRootNodes() 加载根节点,loadSearchTreeChildren() 实现子节点懒加载
关键操作流程:
| 操作 | 流程 |
|---|---|
| 新增 | handleAdd() → registerModal.add() → 打开 PersonalInfoModal |
| 编辑 | handleEdit(record) → 先调 queryById 获取含标签完整数据 → registerModal.edit(fullRecord) |
| 详情 | handleDetail(record) → 先调 queryById 获取含标签完整数据 → personalInfoDetailModal.open(fullRecord) |
| 个人简历 | handleResume(record) → resumeListModal.open(record) |
| 单个删除 | handleDelete(record) → deleteOne() |
| 批量删除 | batchHandleDelete() → batchDelete() |
| 导入 | CustomImportModal 组件 |
| 导出 | getExportUrl() → 直接下载 |
表格操作按钮: "编辑"、"个人简历"为直接操作按钮;"详情"、"删除"在下拉菜单中
设计要点:
formKey ref + nextFormKey() 方法 + <PersonalInfoForm :key="formKey"> 实现每次打开弹窗强制重建子组件,解决"先编辑后详情表单未禁用"的问题disableSubmit prop 控制确认按钮显隐(详情场景隐藏确认按钮)add、edit、disableSubmit表单字段(30+):
| 区域 | 字段 |
|---|---|
| 基本信息 | 证件类型、证件号码、姓名、性别、出生日期 |
| 身份信息 | 民族、国籍、婚姻状况、政治面貌、是否留学人才 |
| 教育信息 | 学历、毕业日期、毕业院校、专业 |
| 工作/技能 | 工作经验、职业技能等级 |
| 户籍/居住 | 户口性质、户口所在地(XZQH 树形懒加载)、现居住地(XZQH 树形懒加载)、现居住地址 |
| 联系方式 | 联系电话、邮箱、QQ号码、微信号 |
| 求职信息 | 求职人员类别、求职状态、是否接受推荐 |
| 标签/头像 | 个人标签(多选下拉)、个人头像(JImageUpload)、数据来源(默认"2") |
必填校验(18 个字段): idType, idNumber, fullName, gender, birthDate, nation, nationality, education, graduationDate, graduateSchool, workExperience, householdLocation, householdAreaName, currentResidence, residenceAreaName, jobSeekerCategory, contactPhone, isOverseasTalent, jobSearchStatus, acceptRecommend
行政区划处理:
a-tree-select + 懒加载展示行政区划树householdAreaName 和 residenceAreaNamehouseholdLocation / currentResidence 存储叶子节点的字典代码个人标签处理:
a-select mode="multiple" 多选下拉tagIds 字符串,前端转为数组回显(空字符串转为 [])tagIds 保持数组原样(不执行 .join(',')),因为后端 DTO 接收 List<String>日期处理: 后端返回 "yyyy-MM-dd" 字符串,edit() 中转为 dayjs 对象赋值给 a-date-picker;submitForm() 中通过 dayjs().format('YYYY-MM-DD') 还原为字符串提交
设计特点:
getFileAccessHttpUrl 获取文件 URL:column="3" bordered)展示 30+ 字段,与编辑表单布局对应useDict + getDictText 自动转为中文显示(使用 12 个字典)formatYesNo() 转为"是/否"PersonalInfoSelector.vue: 可嵌入其他表单的人员选择输入框组件
selectionType='single'/'multiple'PersonalInfoSelectorModal.vue: 选择弹窗
selectionType='radio'/'checkbox')select(records) 回传已选人员列表功能: 查看指定人员的简历列表,支持查看/编辑/删除简历 + 快速新增简历
addBasicResume API 快速新增,不触发完整的 ResumeInfoModal核心设计模式:
reactive dataSource 数组中完成(增删改),临时 ID 使用 __temp_N 格式ResumeInfoDTO 统一提交期望行业/职位:
子表弹窗通用模式: 各子表弹窗(如 ResumeWorkExpModal)通过 defineProps/defineEmits 对外开放:
open(record?) — 打开弹窗,带数据回填emit('success', record) — 编辑/新增确认后回传数据给父表单getDictText 转换getDictText,改为调用 API 获取完整名称| 组件 | 特殊处理 |
|---|---|
| ResumeWorkExpModal | 所属行业(EconomicIndustry)和经济类型(EconomicType)使用 getDictTree 全量加载(因为这些字典 Code 列为空,getDictTreeChildren 会返回全为空 key,导致选中 bug) |
| ResumeEducationModal | 字典:Education、Degree、EduSystem |
| ResumeTrainingModal | 纯文本框表单,无字典依赖 |
| ResumeSkillModal | 字典:SkillLevel |
| ResumeTitleModal | 字典:TitleLevel |
| ResumeAwardModal | 纯文本框/文本区域表单,无字典依赖 |
| ResumeStudyAbroadModal | 国家/地区下拉从 Nationality 字典过滤掉"中国" |
| 日期 | 内容 | 说明 |
|---|---|---|
| 2026-06-04 | 个人基本信息模块初始构建 | 完成个人信息与简历管理的基础 CRUD 功能,前后端共 50+ 文件 |
| 2026-06-04 | 详情查看时表单未禁用修复(先编辑后详情场景) | 详见下方 Bug 修复记录 |
| 2026-06-04 | 打开编辑弹窗多个控制台错误修复 | 详见下方 Bug 修复记录 |
| 2026-06-05 | 详情查看改用独立的 a-descriptions 详情弹窗 | 详见下方功能变更记录 |
| 2026-06-05 | 人员编辑表单增加个人标签字段 | 详见下方功能变更记录 |
| 2026-06-05 | 导入/导出功能实现 | 模板下载 + 导入校验 + 导出(含字典翻译和标签填充) |
问题描述: 在个人信息列表页,先点击"编辑"后关闭弹窗,再点击"详情"时,表单字段和图片上传组件未处于禁用/只读状态。
复现条件: 页面刷新后先点编辑再点详情才会出现;页面刷新后直接点详情则正常禁用。
原因分析: Ant Design Vue 的 <a-modal> 默认 destroyOnClose=false,弹窗关闭时内部组件(PersonalInfoForm)仍然挂载在 DOM 中。先点击"编辑"时,PersonalInfoForm 以 formDisabled=false 的状态挂载;关闭弹窗后组件保持挂载;再点击"详情"时,虽然 disableSubmit 被设为 true,但由于组件实例未重新创建,props 的响应式更新未能正确触发 JFormContainer 的 <fieldset disabled> 状态变更。
最终修复方案: 使用 :key 方式强制组件重建。
formKey ref 和 nextFormKey() 方法<PersonalInfoForm> 上绑定 :key="formKey"nextFormKey() 递增 key,触发 Vue 重建组件实例v-if 控制),ref 在重建完成后自然可用涉及文件:
| 文件 | 操作 | 原因 |
|---|---|---|
jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoModal.vue |
修改 | 引入 formKey + nextFormKey(),:key 绑定强制重建子组件 |
问题描述: 打开编辑弹窗时控制台出现 5 类错误:
FormItem can only collect one field item — 年龄搜索字段两个 AInputNumber 在同一 a-form-item 中date.locale is not a function — 日期字符串未转为 dayjs 对象Cannot read properties of null (reading 'type') — 日期错误的级联报错Cannot read properties of null (reading 'submitForm') — registerForm.value 为空Cannot read properties of undefined (reading '__asyncLoader') — 组件异步加载异常原因分析与修复方案:
| 错误 | 原因 | 修复 |
|---|---|---|
| FormItem 警告 | 年龄范围搜索字段中两个 AInputNumber 被同一个 a-form-item 收集 |
用 <a-form-item-rest> 包裹第二个 AInputNumber,告知 Form 不收集该字段 |
date.locale is not a function |
API 返回的日期字符串 "2000-01-15" 直接赋给 a-date-picker 的 v-model:value,但 a-date-picker 期望 dayjs 对象 |
在 PersonalInfoForm.edit() 方法中将 birthDate 和 graduationDate 字符串转为 dayjs() 对象 |
| 级联错误 | 日期转换错误导致 a-date-picker 内部操作失败 |
修复日期转换后自动解决 |
submitForm 报错 |
v-if="visible" 导致组件未挂载时 ref 为 null |
改用 :key 方式重建组件(同修复1) |
__asyncLoader 报错 |
组件重建时序问题导致异步组件加载异常 | 改用 :key 重建后解决 |
涉及文件:
| 文件 | 操作 | 原因 |
|---|---|---|
jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoForm.vue |
修改 | edit 方法增加日期字符串转 dayjs 对象逻辑;引入 dayjs 依赖 |
jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoModal.vue |
修改 | 改用 :key 替代 v-if |
jeecgboot-vue3/src/views/recruitment/personal/PersonalInfoList.vue |
修改 | 年龄搜索字段用 <a-form-item-rest> 包裹第二个 AInputNumber |
问题描述: 原详情查看功能复用 PersonalInfoModal + PersonalInfoForm,打开的是禁用状态的编辑表单,布局与编辑界面相同但字段不可编辑,用户体验不佳。
解决方案: 新建独立的 PersonalInfoDetailModal 组件,使用 a-descriptions 描述列表以只读方式展示数据。
实现要点:
a-descriptions :column="3" bordered 呈现三列带边框的字段列表,布局与编辑表单完全一致(10 行字段)a-image 正方形组件展示头像(150x150px),与描述列表形成左右布局(左侧 span=2,右侧 span=22)getDictText 自动转为中文显示:fullscreen="true"),底部仅保留「关闭」按钮a-image 预览按钮铺满父容器的问题:外层包裹 display: inline-block 的 div 约束宽度涉及文件:
| 文件 | 操作 | 原因 |
|---|---|---|
jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoDetailModal.vue |
新增 | 独立的只读详情弹窗,使用 a-descriptions 展示 |
jeecgboot-vue3/src/views/recruitment/personal/PersonalInfoList.vue |
修改 | 导入新组件,修改 handleDetail 方法调用 personalInfoDetailModal.open() |
需求描述: 在人员编辑表单(PersonalInfoForm)中增加"个人标签"字段,支持多选,数据来源于个人标签库(PersonalTag)。新增/编辑人员时同步保存标签关联关系,详情弹窗展示标签名称。
实现方案:
后端:
PersonalInfoDTO(extends PersonalInfo),含 List<String> tagIds,用于新增/编辑时接收前端提交的标签 ID 列表PersonalInfoVO(extends PersonalInfo),含 String tagIds(逗号分隔,表单回显用),tagNames 继承自父类的虚拟字段PersonalTagController 新增 /listForSelect 接口,返回所有已启用的标签 {id, tagName} 列表PersonalInfoServiceImpl 注入 PersonalTagRelationMapper 和 PersonalTagMapper,提供 saveWithTags/updateWithTags 方法,在保存人员基本信息后同步维护 personal_tag_relation 关联表PersonalInfoController 的 /add 和 /edit 接收 PersonalInfoDTO,/queryById 返回 PersonalInfoVO;分页列表 /list 返回带标签名的 VO 列表前端:
PersonalInfo.api.ts 新增 queryById 和 getPersonalTagListForSelect APIPersonalInfoForm.vue 在"数据来源"前添加"个人标签"多选下拉(a-select mode="multiple"),编辑时自动将后端返回的逗号分隔字符串转为数组回显PersonalInfoDetailModal.vue 新增"个人标签"展示行PersonalInfoList.vue 的 handleEdit/handleDetail 改为先调用 queryById 获取含标签的完整数据再传给弹窗Bug 修复(2026-06-05):
tagIds 空字符串导致选中空选项: queryById 返回 tagIds: "" 时,edit 方法中转换条件用 tmpData.tagIds && ... 会因空字符串为 falsy 跳过转换,Object.assign 把 "" 赋给 formData.tagIds,a-select mode="multiple" 收到非数组后误渲染空白选项。修复:改用 typeof tmpData.tagIds === 'string' 判断,空字符串转为 []。
tagIds 数组被误转为字符串导致 JSON 解析失败: submitForm 的循环中,getValueType 将 tagIds 误判为 string 类型,执行 .join(',') 转为逗号字符串,后端 DTO 接收 List<String> 时报 Cannot deserialize from String value。修复:在循环条件中排除 tagIds(data !== 'tagIds'),保持数组原样提交。
涉及文件:
| 文件 | 操作 | 原因 |
|---|---|---|
jeecg-boot/../personal/dto/PersonalInfoDTO.java |
新增 | DTO 类,extends PersonalInfo,含 List<String> tagIds,用于 add/edit 接收前端标签数据 |
jeecg-boot/../personal/vo/PersonalInfoVO.java |
新增 | VO 类,extends PersonalInfo,含 String tagIds,用于 queryById 返回含标签的完整数据 |
jeecg-boot/../personal/service/IPersonalInfoService.java |
修改 | 新增 saveWithTags、updateWithTags、queryPersonalInfoVOById、queryPageListWithTags 方法声明 |
jeecg-boot/../personal/service/impl/PersonalInfoServiceImpl.java |
修改 | 注入 PersonalTagRelationMapper 和 PersonalTagMapper;实现标签关联保存/更新/查询逻辑;分页查询批量填充标签名 |
jeecg-boot/../personal/controller/PersonalInfoController.java |
修改 | add/edit 接收 PersonalInfoDTO;queryById 返回 PersonalInfoVO;页面列表返回含标签名的 VO |
jeecg-boot/../tag/controller/PersonalTagController.java |
修改 | 新增 /listForSelect 接口,返回已启用的标签列表供前端下拉选择 |
jeecgboot-vue3/../personal/PersonalInfo.api.ts |
修改 | 新增 queryById 和 getPersonalTagListForSelect API |
jeecgboot-vue3/../personal/PersonalInfoList.vue |
修改 | handleEdit/handleDetail 改为调用 queryById 获取含标签的完整数据 |
jeecgboot-vue3/../personal/components/PersonalInfoForm.vue |
修改 | 添加个人标签多选下拉;编辑时字符串→数组回显;提交时排除 tagIds 的数组→字符串转换 |
jeecgboot-vue3/../personal/components/PersonalInfoDetailModal.vue |
修改 | 新增个人标签展示行 |
功能描述: 简历编辑支持主表字段 + 7 个子表(工作经历、教育经历、培训经历、职业技能、职称情况、获奖情况、留学经历)的新增/编辑。
实现方案:
后端:
ResumeInfoDTO 包含主表字段 + 7 个子表 ListResumeInfoServiceImpl.saveMainAndSub() 事务方法:保存主表 → 处理默认简历逻辑 → 遍历插入 7 个子表ResumeInfoServiceImpl.updateMainAndSub() 事务方法:更新主表 → 先删后插 7 个子表ResumeInfoController 额外提供 21 个子表独立 CRUD 接口(小程序端使用)前端:
ResumeInfoForm.vue 主表单 + 7 个子表 area,子表操作在本地 dataSource 执行,最终统一提交ResumeInfoDetailModal.vue 只读展示主表+7个子表涉及文件:
| 文件 | 操作 | 原因 |
|---|---|---|
jeecg-boot/../personal/dto/ResumeInfoDTO.java |
新增 | 简历 DTO,含主表字段 + 7 个子表 List |
jeecg-boot/../personal/vo/ResumeInfoPage.java |
新增 | 简历 VO,用于 queryById 组装完整信息 |
jeecg-boot/../personal/service/IResumeInfoService.java |
修改 | 新增 saveMainAndSub、updateMainAndSub、deleteMainWithSubs 方法 |
jeecg-boot/../personal/service/impl/ResumeInfoServiceImpl.java |
修改 | 实现主表+子表联动逻辑(先删后插策略) |
jeecg-boot/../personal/controller/ResumeInfoController.java |
修改 | 新增主表+子表的 add/edit/delete/queryById + 21 个子表独立 CRUD 接口 |
| 7 个子表 Mapper | 修改 | 各新增 deleteByMainId 和 selectByMainId 注解 SQL 方法 |
jeecgboot-vue3/../personal/ResumeInfo.api.ts |
新增 | 简历 API 封装(6个接口) |
jeecgboot-vue3/../personal/ResumeInfo.data.ts |
新增 | 7 个子表列类型定义(JVxeTypes) |
jeecgboot-vue3/../personal/components/ResumeInfoForm.vue |
新增 | 简历主表+子表编辑表单 |
jeecgboot-vue3/../personal/components/ResumeInfoModal.vue |
新增 | 简历编辑弹窗容器,类似 PersonalInfoModal 模式 |
jeecgboot-vue3/../personal/components/ResumeInfoDetailModal.vue |
新增 | 简历只读详情弹窗 |
jeecgboot-vue3/../personal/components/ResumeListModal.vue |
新增 | 简历列表弹窗(含快速新增) |
| 7 个子表 Modal(ResumeWorkExpModal 等) | 新增 | 各子表的新增/编辑弹窗 |
功能描述: 提供可嵌入其他表单的人员选择器组件,支持单选和多选,支持跨页选择。
前端组件:
PersonalInfoSelector.vue — 只读输入框,展示已选人员姓名,点击打开选择弹窗PersonalInfoSelectorModal.vue — 带查询功能的弹窗表格,支持 radio/checkbox 行选择涉及文件:
| 文件 | 操作 | 原因 |
|---|---|---|
jeecgboot-vue3/../personal/components/PersonalInfoSelector.vue |
新增 | 人员选择器输入框,可嵌入其他业务表单 |
jeecgboot-vue3/../personal/components/PersonalInfoSelectorModal.vue |
新增 | 人员选择弹窗,含查询表格 + 跨页多选 |
ResumeInfoServiceImpl.updateMainAndSub() 采用"先删后插"策略处理子表编辑:先通过各子表 Mapper 的 deleteByMainId(resumeId) 删除所有旧数据,再遍历 DTO 中的子表列表逐一插入新数据。这样避免了逐行判断增删改的复杂逻辑,简化了前端与后端的协同。
前端: 所有日期字段在组件 open() 回填时将后端返回的 "yyyy-MM-dd" 字符串转为 dayjs 对象赋给 a-date-picker;在 handleOk() 提交时通过 dayjs().format('YYYY-MM-DD') 还原为字符串提交给后端。
后端: PersonalInfo 实体中 birthDate、graduationDate 使用 @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd") 和 @DateTimeFormat(pattern = "yyyy-MM-dd") 注解,统一为 Date 类型。格式化为字符串在前端完成。
PersonalInfo 中户口所在地和现居住地各有两个字段:
householdLocation / currentResidence:存储行政区划字典的叶子节点代码householdAreaName / residenceAreaName:存储完整路径(如"广东省/湛江市/赤坎区")前端通过 a-tree-select 懒加载 XZQH 字典树,选择节点后调用后端 API 获取完整路径存入 areaName。导入时两者互为补充——Excel 通常填写完整路径,导入逻辑会反向查找对应的字典代码。
ResumeWorkExp 的 position 字段和 ResumeTitle 的 level 字段与数据库保留字同名,在实体类中使用 @TableField("\"COLUMN_NAME\"") 注解进行转义。
getDictTreeChildren 懒加载getDictTreeChildren 返回的 key 全为空字符串,造成"选中一个全部选中"的 bug,因此改用 getDictTree 全量加载(使用 Value 列做标识)PersonalInfoServiceImpl.queryPageListWithTags() 中通过两次批量查询避免 N+1 问题:
personal_tag_relation),按 personId 分组personal_tag)populateTagNames() 采用相同策略,用于导出时批量填充标签名称。