
简介这是一份面向小型企业人事档案管理场景的毕业设计资料基于Java、Spring Boot框架与MySQL数据库构建B/S架构应用。系统覆盖用户管理、企业人事管理、部门管理、人事调动管理、职务管理、薪酬管理、培训管理、招聘信息管理、求职简历管理、邀请面试管理、录用信息管理、员工应聘管理及系统管理等模块适合小型企业管理者、人力资源人员及相关专业学生参考学习。压缩包内共1个docx文件大小约4.34MB即论文全文包含中英文摘要、目录、系统分析、系统设计与测试等完整内容可帮助读者理清从需求分析到功能实现的全过程思路。目前已有115人学习下载适合需要完成同类课题或搭建企业人事管理系统的开发者使用。论文测试结果显示系统各功能均可正常运行界面友好并支持按企业实际情况进行定制扩展具有较好的参考价值与可扩展性。1. Java 人事档案管理系统这题考的不是增删改查把基于 Java 的企业人事档案管理系统当成普通 CRUD 来写是最容易拿低分的做法。真正决定项目深度的部分在于三个地方档案的版本怎么留痕数据权限怎么按组织维度和角色收敛以及检索怎么写才能在十几万条人事记录下不出现明显的响应劣化。这套系统在课程设计和企业内部系统里都很常见主线是员工信息的录入、变更、查询、导入导出和操作审计适合用 Java 生态里的 Spring Boot MyBatis 来做环境依赖少、部署快论文里也好解释。如果你正在找一个既能本地跑通、又能写进论文的 Java 课程设计案例这个标题的可靠落地方式是先明确边界再按员工主表、部门组织树、任职变更表和档案附件四类数据做建模最后用动态 SQL 把组合查询做厚。下面按数据建模、后端实现、权限检索和验收证明四条线展开代码全部按可直接复现的体量给出数据库和 JDK 版本用当前最稳定的一档。2. 人事档案数据建模稳定表、变更表与版本表怎么切分2.1 档案主表拆成基础信息和现职信息避免一张大表字段爆炸人事档案和普通业务表最大的区别在字段增长率。刚入职时只有姓名、身份证、学历、入职时间、部门岗位半年后开始积累合同记录、考核结果、奖惩信息、培训经历。如果全部塞进 employee 表表会越来越宽索引也会随着字段增加而失效。常见做法是拆成 employee_base 和 employee_current 两张表base 存身份证号、姓名、性别、出生日期、籍贯、民族、政治面貌、学历等不常变的信息current 存员工编号、部门 id、岗位 id、职级、入职日期、转正日期、合同到期日这类会随任职状态变动的内容。拆表之后档案查询的默认连接是 base 做条件过滤、current 做展示。需要注意的是 current 表不能直接当成最新记录来覆盖后面版本的留痕需求要求它只能被新增版本驱动更新而不是业务接口里随意 update。下面给出最小建表语句CREATE TABLE employee_base ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT 工号, id_card VARCHAR(18) NOT NULL COMMENT 身份证号, full_name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL COMMENT 1男 2女, birth_date DATE NOT NULL COMMENT 出生日期, education VARCHAR(20) DEFAULT NULL COMMENT 最高学历, native_place VARCHAR(100) DEFAULT NULL COMMENT 籍贯, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_id_card (id_card) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工基础信息表;建表时有两点值得说明一是身份证号必须建唯一索引这是人事系统里最容易被忽略的约束同一身份证号在系统里出现两遍后续发薪、社保、合同关联全部会错位二是 create_time 和 update_time 用数据库默认值而不是 Java 层填充能减少业务代码的重复。gender 用 TINYINT 而不是 VARCHAR是为了在论文的性能对比里体现字段类型对索引体积的影响。2.2 部门组织树用闭包表还是邻接表部门通常有层级关系比如集团-事业部-研发中心-平台组。最简单的是邻接表只保存 parent_id查子部门需要用递归或多次查询最严谨的是闭包表把每个祖先和后代关系都记录成一行。人事档案系统里部门层级一般不超过四层我通常会选邻接表加一层本地缓存查询时一次查出全部部门用 Java 8 的 Stream 在内存里组装树而不是在 SQL 里递归。缓存的前提是部门表的修改频率低。可以在部门表的增删改接口上主动清掉一个本地 Map避免数据不一致。部门表的结构可以简化成下面这样CREATE TABLE department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(100) NOT NULL COMMENT 部门名称, parent_id BIGINT NOT NULL DEFAULT 0 COMMENT 父部门id0表示根, dept_code VARCHAR(20) NOT NULL UNIQUE COMMENT 部门编码, sort_order INT DEFAULT 0 COMMENT 同级排序, status TINYINT DEFAULT 1 COMMENT 1启用 0停用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT部门组织表;这里有个容易做错的地方employee_current 表里保存的部门 id 应该指向叶子部门而不是任意层级部门。否则查询某部门下所有人时用 LIKE 匹配 dept_code 前缀会出现歧义比如 dept_code 1001 会错误匹配 10010。如果一定要用编码前缀实现下级部门汇总部门编码必须定长或者干脆用 parent_id 递归先把部门 id 集合查出来再放进 IN 条件。2.3 档案版本表用生效时间区间保存每一次变更快照人事档案的每一次变更比如转岗、升职、合同续签都应该留下一个可回溯的版本。这时候需要一张 employee_archive_version 表把变更前后的字段对拍存进去。最简单的设计是只存 changed_fields 的 JSON 字符串严谨一点就按需要回看的字段冗余成行。从论文评审和实际查询的角度我更推荐冗余成行因为可以直接用一条 SQL 查出一名员工某时间点的档案快照不需要在 Java 里解析 JSON 再拼装对象。CREATE TABLE employee_archive_version ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL COMMENT 工号, version_no INT NOT NULL COMMENT 版本号从1递增, old_data JSON COMMENT 变更前字段快照, new_data JSON COMMENT 变更后字段快照, change_type VARCHAR(20) COMMENT TRANSFER/PROMOTE/RENEW/RESET, change_desc VARCHAR(200) COMMENT 变更说明, operator_id BIGINT NOT NULL COMMENT 操作人id, effective_date DATE NOT NULL COMMENT 生效日期, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_emp_no_version (emp_no, version_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT档案版本记录表;2.4 表结构落地的参数对照表名主键策略关键索引必备字段典型错误employee_base自增 idemp_no 唯一、id_card 唯一create_time/update_time用员工编号做主键导致后期调整编码困难department自增 iddept_code 唯一parent_id 默认 0删除部门时硬删除历史档案指向悬空employee_current自增 idemp_no 唯一、dept_id 普通索引入职日期/合同到期日和 base 表耦合state 变更直接 updateemployee_archive_version自增 id(emp_no, version_no)old_data/new_data只存新值不存旧值无法做比对建表完成后建议用一个脚本把表结构导出成 markdown后面写论文的数据表设计章节直接引用不用重新整理。3. Spring Boot MyBatis 实现档案核心后端从建表到跑通3.1 Spring Boot 3 MyBatis-Plus 的依赖清单与 JDK 版本搭配创建项目的合理选择是 Spring Boot 3.x 搭配 JDK 17。JDK 17 是 LTS 版本Spring Boot 3 也明确要求 JDK 17 起步网页上能找到的 Java 基础面试题里反复出现的 Module、Record 语法在 17 里是原生支持的。MyBatis 用 MyBatis-Plus 而不是原生 MyBatis是因为它在分页、条件构造器、字段填充上能省掉大量重复的 Mapper XML 代码同时对原生 MyBatis 的 SQL 控制力没有损失。需要引入的依赖保持精简避免把企业管理人事系统做成一个大杂烩工程。下面是一个可用的 pom.xml 主体parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies依赖说明mybatis-plus-spring-boot3-starter 是 MyBatis-Plus 针对 Spring Boot 3 的专用 starter老教程里写的 mybatis-plus-boot-starter 在 Spring Boot 3 下会启动报错。mysql-connector-j 是 MySQL 8 的官方驱动坐标旧坐标 com.mysql:mysql-connector-java 只适合 5.x 驱动。Validation 依赖用于参数校验在人事档案保存接口上它比手写 if 判断要整洁得多。3.2 员工新增接口事务、参数校验和编码规则放一层处理新增员工是人事系统最高频的写操作它至少要完成三件事生成唯一工号、写入 employee_base 和 employee_current、记录操作日志。这三步必须在一个事务里完成否则出现 base 写入成功而 current 写入失败时系统里会有一个没有任职记录的幽灵员工。MyBatis-Plus 的 Transactional 注解放在 Service 类的公开方法上注意要使用代理调用同类里的私有方法互相调用不生效。Service RequiredArgsConstructor public class EmployeeService { private final EmployeeBaseMapper baseMapper; private final EmployeeCurrentMapper currentMapper; private final ArchiveVersionService versionService; Transactional(rollbackFor Exception.class) public Long addEmployee(EmployeeAddDTO dto) { // 1. 工号自动生成前缀 EMP 加日期加序列 String empNo generateEmpNo(dto.getEntryDate()); // 2. 基础信息入库 EmployeeBase base new EmployeeBase(); BeanUtils.copyProperties(dto, base); base.setEmpNo(empNo); baseMapper.insert(base); // 3. 现职信息入库 EmployeeCurrent current new EmployeeCurrent(); current.setEmpNo(empNo) .setDeptId(dto.getDeptId()) .setPostId(dto.getPostId()) .setEntryDate(dto.getEntryDate()); currentMapper.insert(current); // 4. 写入第一条版本快照 versionService.saveFirstVersion(base.getId(), dto); return base.getId(); } }代码逻辑说明方法上的Transactional(rollbackFor Exception.class)表示任何运行期异常都会回滚整个事务包括自定义 ServiceException。如果不写 rollbackForSpring 默认只在 RuntimeException 上回滚受检异常不会触发回滚这是很多人事务不生效的第一原因。第 4 步调用版本服务是为了把新增动作也当成初始版本记录下来后续每一次 update 都基于当前版本生成新版本号论文里可以据此画出档案-版本的时序图。工号生成逻辑里有一个隐性坑如果直接在事务里 SELECT MAX(emp_no) 再加一并发时会生成重复工号。稳妥做法是把当天日期加随机串或者用 Redis INCR。考虑到单人开发的人事系统并发量不高用日期加序号即可但在论文里要写明该策略的并发上限。3.3 本地启动的最小命令建库、启动、验证请求三步项目 clone 下来第一件事应该是把数据库建好而不是直接 mvn spring-boot:run。先执行建库语句和初始化 SQL再用一条 Maven 命令启动最后用 curl 验证接口是否活着。整个过程可以合并成下面三步。# 1. 初始化数据库 mysql -uroot -p123456 -e CREATE DATABASE IF NOT EXISTS hr_archive DEFAULT CHARSET utf8mb4; mysql -uroot -p123456 hr_archive doc/schema.sql # 2. 启动应用 mvn spring-boot:run -Dspring-boot.run.profilesdev # 3. 验证接口 curl -X POST http://localhost:8080/api/employee \ -H Content-Type: application/json \ -d {fullName:张三,idCard:110101199001011234,deptId:3,entryDate:2024-05-06}参数说明第一条命令里的-p123456是本地开发密码官方文档和课程设计的实现说明里建议写成-p后紧跟密码不要加空格否则 MySQL 会把-p 123456解析成未知参数。第二步的 profiles 参数指定读取 application-dev.yml该文件里必须配置数据源 URL、Driver 和 MyBatis-Plus 的 mapper 扫描路径。第三步如果返回 JSON 里的 id 字段有值说明项目已经从数据库层到 Service 层完整贯通了。4. 档案查询、数据权限与检索动态 SQL 和全文索引的取舍4.1 组合条件查询MyBatis 动态 SQL 的三种典型写法人事档案的查询条件不是固定的有时按工号查有时按姓名加部门过滤有时要按入职时间范围筛人。这类需求直接写死 SQL 不现实MyBatis 提供了if、where和foreach三个标签来解决。最典型的是姓名模糊匹配加部门 IN 条件这个场景在查询员工列表时几乎必然出现。select idselectEmployeePage resultTypecom.example.dto.EmployeePageVO SELECT b.emp_no, b.full_name, b.gender, c.dept_id, c.post_id, c.entry_date FROM employee_base b JOIN employee_current c ON b.emp_no c.emp_no where if testparam.fullName ! null and param.fullName ! AND b.full_name LIKE CONCAT(%, #{param.fullName}, %) /if if testparam.deptIdList ! null and param.deptIdList.size() 0 AND c.dept_id IN foreach collectionparam.deptIdList itemdeptId open( separator, close) #{deptId} /foreach /if if testparam.startDate ! null AND c.entry_date gt; #{param.startDate} /if if testparam.endDate ! null AND c.entry_date lt; #{param.endDate} /if /where ORDER BY c.entry_date DESC /select这个 XML 有几点值得写进论文where标签会自动去掉第一个多余的 AND不需要在条件前拼 11这是一个在代码评审里经常被挑出来的细节。日期比较的gt;是 XML 转义后的写法直接写会让 XML 解析报错除非把 SQL 放进 CDATA 块。#{param.fullName}是预编译占位符和$的区别在于#会转义字符串能挡住 SQL 注入这一点在论文的安全设计章节里是必备论据。4.2 数据权限先过滤部门再判断按钮而不是反过来很多人事系统的权限控制只做了菜单可见性登录进去后普通 HR 点击员工查询能搜到全公司的档案这在实际企业环境是重大事故。可靠的实现方式是 RBAC 加数据范围两层第一层校验角色能不能访问员工查询这个资源第二层校验角色能看哪些部门的数据。第二层常见做法是在 SQL 拼接时注入一个 dept_id 的权限过滤条件可以把当前登录人可见的部门 id 列表存进 ThreadLocal然后传给 Mapper 参数。Data public class DataScope { private ListLong deptIdList; private String dataScopeType; // ALL / DEPT / SELF } // 查询前统一构造 DataScope scope DataScopeContext.get(); QueryWrapperEmployeeCurrent wrapper new QueryWrapper(); if (DEPT.equals(scope.getDataScopeType())) { wrapper.in(dept_id, scope.getDeptIdList()); } else if (SELF.equals(scope.getDataScopeType())) { wrapper.eq(emp_no, SecurityUtils.getCurrentEmpNo()); }这段代码的核心在于 dataScopeType 与部门列表的对应关系角色 A 可以看全集团角色 B 只能看本部门角色 C 只能看自己。如果写成能看部门的人也能看全集团就失去了数据权限的分级意义。用 ThreadLocal 持有当前用户的 DataScope 对象在拦截器里初始化、在请求结束时 clear能避免线程池复用导致的数据串号。MyBatis-Plus 的 QueryWrapper 适合简单查询复杂到前面那种多表 join 时还是改回 XML 里的where更直观。4.3 检索边界什么时候用 MySQL 全文索引什么时候换独立检索引擎人事系统在 5 万条以内时前端输入姓名点查询LIKE %王% 的响应基本能压在 100ms 内用不到任何搜索引擎级别的方案。超过 10 万条再叠加姓名、身份证、部门等多条件 ORLIKE 的扫全表代价就明显了。此时第一反应应该是加 MySQL 的 FULLTEXT 索引配合 MATCH AGAINST 做自然语言模式检索而不是直接上 Elasticsearch后者对这个体量的数据是过度设计论文答辩时也很难说清为什么一个几十万数据量的系统需要独立集群。MySQL 全文索引在 InnoDB 上的最低版本要求是 5.6但 8.0 之前的全文索引对中文分词支持很差ngram 是默认分词器。建立全文索引的写法是ALTER TABLE employee_base ADD FULLTEXT INDEX ft_emp_search (full_name, emp_no) WITH PARSER ngram;对应查询语句是SELECT emp_no, full_name FROM employee_base WHERE MATCH(full_name, emp_no) AGAINST(王 IN NATURAL LANGUAGE MODE);这个方案的边界很明确ngram 的默认 token 大小是 2意味着单字王在默认配置下不会命中需要把 ngram_token_size 调成 1但这会增加索引体积。另一个坑是 MATCH AGAINST 的默认最小长度在中文场景下几乎无效要做长度过滤只能改 innodb_ft_min_token_size。如果论文里只写用全文索引实现档案检索而不交代分词配置答辩时被追问几句就会露馅。真正的结论是面向姓名的检索用 LIKE 加前缀索引就够面向描述性字段奖惩记录、培训备注的检索才值得引入全文索引。5. 用压测日志回填论文验收数据与源码交付清单5.1 用命令行压测生成响应时间数据表论文的系统测试章节最怕出现空泛的表述比如测试表明系统运行稳定响应较快。可以写一个临时用的 JMeter 或 Gatling 脚本把核心接口压出可填表格的数据。没有 JMeter 时用 bash 里的 curl 加 time 命令也能得到可用数据更轻量的是写一个 Java 单测循环执行查询接口统计平均和 TP95 响应时间。for i in $(seq 1 200); do curl -s -o /dev/null -w %{time_total}\n \ http://localhost:8080/api/employee/page?current1size10fullName张 /tmp/latency.log done awk {sum$1; if($1max) max$1} END {print avg:, sum/NR, max:, max} /tmp/latency.log这段命令把 200 次查询的真实耗时写进 latency.log再用 awk 计算平均值和最大值。注意-o /dev/null是把响应体丢弃只管响应码和耗时-w参数输出 total_time 字段。如果要在论文里写 TP95可以把 latency.log 排个序取第 190 个值。公布测试环境时建议同时交代 CPU 核心数、MySQL 版本和 JDK 版本否则数据没有可对比性。把这组数据生成表格放进论文系统性能测试章节比一句性能达标有说服力得多。5.2 审计日志转成论文的事实依据系统必须记录每一步档案变更的操作人、操作时间、变更前后摘要这些数据天然能说明系统的安全性设计。留痕方式不是只打印 log 日志而是把关键写操作同步插入到 operate_log 表然后提供一个按照操作人、时间范围、模块名三个维度筛选的查询页面。论文的安全性设计篇章可以直接引用这条表里的记录数配合版本表证明系统具备完整可追溯能力这是项目的一大加分点。5.3 源码交付目录清单答辩或提交源码时将项目按以下结构整理能够一眼看出文档同代码的对应关系。注意 src 下的 doc 目录同时放数据库脚本、接口文档、测试报告而文章正文依照此结构可以划分成需求分析、数据库设计、功能实现、系统部署与测试、总结五个部分。hr-archive/ ├── pom.xml ├── doc/ │ ├── schema.sql │ ├── data.sql │ └── api-test.md ├── src/main/java/ │ └── com/example/hr/ │ ├── controller/ │ ├── service/ │ ├── mapper/ │ ├── entity/ │ ├── dto/ │ └── config/ └── src/main/resources/ └── application-dev.yml盒外的 src 路径必须在 README 中配套标注 README 文件所需 JDK 版本及数据库账号并将完整实现代码打开打包后放在仓库的 releases 目录而不是直接压缩传网盘。把项目从只能跑升级为能答辩关键不在于新增了多少花哨功能而在于每一层的设计决定都能对应到一处代码或一张表。本文还有配套的精品资源点击获取