简介:这是一套基于SSM框架与JavaWeb技术开发的企业人事管理系统毕业设计资料包,面向计算机专业学生及有课程设计、毕业设计需求的学习者。系统采用JSP+MySQL+Tomcat技术栈,划分管理员、部门经理、员工三级角色,覆盖员工管理、考勤签到、请假申请、工资查询等核心人事业务模块,功能完整,适合作从入门到进阶的实战项目参考。
资源压缩包共6个文件,总大小25.46MB,包含源码zip、SQL数据库脚本、论文docx、答辩PPT及部署说明文档,对应代码工程、数据库初始化脚本、设计文档和演示文稿等交付成果,结构清晰,便于对照学习。目前已有61人学习下载。
项目源码经过严格调试,可直接导入IDE运行,并配有运行环境、部署视频教程和论文参考,能够帮助读者快速上手SSM框架开发流程,理解企业人事管理系统的数据结构与业务分层设计,是完成课程设计或毕业设计的一份完整备料。
1. 企业人事管理系统在 SSM 框架下的真正难点与交付边界
企业人事管理系统这个题目,在毕业设计里属于看起来简单、实际容易写乱的类型。表面上是员工增删改查,真正的难点在于部门调整、员工异动、考勤统计这类状态转换场景。SSM 框架(Spring + SpringMVC + MyBatis)的选型核心在于:Spring 管理事务和 Bean,MyBatis 用 XML 写贴近 SQL 的查询,SpringMVC 把 URL 和 Controller 解耦——三层的边界清晰,出问题时能按层排查,这比直接上 Spring Boot 更能把框架原理写明白。从交付角度看,源码、数据库脚本、论文、PPT 四件套要能互相印证,论文里的架构图要和代码分层对得上,答辩时老师问“这个功能在哪个文件里实现的”你必须马上指出来。下文围绕这套完整的落地方案来写,覆盖表结构设计、SSM 配置、登录认证、核心业务模块,以及论文答辩的素材整理。
2. 基于 SSM 的人事系统数据建模与表结构设计
2.1 部门-员工-考勤的核心关系拆解
人事系统的数据模型以员工为中心向外辐射。最先落地的三张主表是部门表、员工表、考勤表,它们的关系是:部门一对多员工,员工一对多考勤。很多同学在这里容易犯第一个错,把部门和员工设计成一张表,用 parent_id 递归表达层级,这在员工量小的时候没问题,但论文里写“数据库设计符合第三范式”就站不住脚。
我一般会把部门表独立出来,用 parent_id 支持树形结构(总部-分公司-部门),员工表只存 dept_id 外键。考勤表按天存一条记录,用 work_date 做唯一索引约束(emp_id + work_date),从数据库层面防止同一员工同一天重复打卡。请假单独建表,因为请假的字段(开始时间、结束时间、请假类型、审批状态)和考勤的字段差异太大,混在一起会导致大量空字段。
这套表结构在论文里对应 ER 图,画的时候要注意关系线上的标注:部门到员工是 1:N,员工到考勤是 1:N,员工到请假是 1:N,角色和用户是 N:M。ER 图不用画得太细,但要覆盖后续章节要写的每一个功能模块。
2.2 7 张核心表的字段设计与索引规划
人事管理系统最常见的合理规模是 7 张核心表,外加 1 张关联表。下面的表格可以直接复制去建库,字段名用下划线风格,和 Java 实体类的驼峰命名做好映射。
| 表名 | 核心字段 | 说明 |
|---|---|---|
| t_dept | id, dept_name, parent_id, manager_id | manager_id 存部门负责人在 t_employee 中的 id |
| t_employee | emp_no, name, gender, birth_date, phone, dept_id, position, hire_date, status, salt, password | emp_no 用唯一索引,登录密码和加密盐放这里 |
| t_attendance | emp_id, work_date, check_in_time, check_out_time, is_late, is_early_leave, status | status 区分正常/异常/补卡 |
| t_leave | emp_id, start_time, end_time, leave_type, reason, status | status 用 0/1/2 表示待审批/通过/驳回 |
| t_role | role_id, role_name, role_desc | 管理员、人事专员、普通员工 |
| t_user_role | user_id, role_id | 用户和角色的多对多关联表 |
| t_operation_log | user_id, operation, target, create_time | 记录敏感操作,写论文时是亮点 |
employee 表的建表语句里有两个细节值得注意。第一个是 emp_no 员工编号由程序生成,格式类似 EMP2024001,生成逻辑写在 Service 层而不是数据库触发器,方便在代码注释里向答辩老师解释。第二个是 status 字段,用一个整数表示在职/离职/试用期,离职员工的数据不能物理删除,否则考勤历史统计会断裂,这一点在论文的测试用例里会写到。
CREATE TABLE t_employee ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT '员工编号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender TINYINT DEFAULT 1 COMMENT '1男 0女', birth_date DATE COMMENT '出生日期', phone VARCHAR(20) COMMENT '手机号', email VARCHAR(50) COMMENT '邮箱', dept_id INT NOT NULL COMMENT '部门ID', position VARCHAR(50) COMMENT '职位', hire_date DATE COMMENT '入职日期', status TINYINT DEFAULT 1 COMMENT '1在职 0离职', salt VARCHAR(16) NOT NULL COMMENT '密码盐', password VARCHAR(64) NOT NULL COMMENT '密码MD5加盐', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_dept (dept_id), INDEX idx_status (status) ) COMMENT '员工表';索引规划上,emp_no 用唯一索引保证编号不重复,dept_id 建普通索引支撑“按部门筛选员工”的高频查询,status 的索引服务于员工列表的状态过滤。需要注意,如果后续要按入职日期范围做统计,可以再给 hire_date 加索引,一次加一个,不要提前给所有字段都加索引,不然插入性能会下降,答辩老师如果问“索引是不是越多越好”,这就是现成的回答素材。
2.3 权限模型:基于角色的操作审计怎么做
人事系统的权限不需要做到 Spring Security 那么重,用简单的 RBAC 模型就够:用户属于角色,角色绑定可访问的菜单和可执行的操作。毕业设计里最常见的权限粒度是按钮级别,也就是“管理员可以删除员工,人事专员只能编辑”,这个在拦截器里做。
操作审计是论文里讨巧的设计。在 t_operation_log 表里记录“谁在什么时间对哪个目标做了什么操作”,实现方式是在 Service 层用一个 LogUtil 工具类调用 mapper 写入日志。不要把日志逻辑写死在每个业务方法里,那样代码会非常冗余;写一个切面类用 Spring AOP 拦截标注了 @OpLog 注解的方法,是对框架特性的合理展示,答辩时能讲清楚 AOP 的概念就能得不少分。数据库脚本里要把这几张表的 INSERT 语句和表注释写全,老师导入数据库时执行无报错,这是第一印象分。
3. 用 SSM 框架跑通登录认证与 Spring 容器装配
3.1 pom.xml 依赖选型与版本对齐
SSM 的依赖版本是第一个大坑。Spring 5.x 搭配 MyBatis 3.5.x 与 mybatis-spring 2.x 是当前比较稳的组合,javax.servlet-api 如果用 4.0.1,打包时要把 scope 设为 provided,否则和 Tomcat 自带的 Servlet 实现冲突。下面是一个可以直接改用的核心依赖清单。
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.39</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.39</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.39</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies>这段依赖里有几个参数值得说明。mysql-connector-java 8.x 的驱动类是 com.mysql.cj.jdbc.Driver,和 5.x 的 com.mysql.jdbc.Driver 不一样,写错会直接 ClassNotFound。spring-jdbc 必须引入,因为事务管理器 DataSourceTransactionManager 在这个包里。mybatis-spring 是让 MyBatis 的 SqlSessionFactory 交给 Spring 容器管理的桥接包,缺少它会出现 SqlSession 无法自动注入的报错。版本号之间如果出现冲突,以 spring-webmvc 的版本为准向上对齐。
数据库连接用 Druid 连接池是常见的做法,在 pom 里额外引入 druid-spring-boot-starter 或者 druid 1.2.x 的独立包。连接池参数里 initialSize 设 5、maxActive 设 20,测试环境够用,论文里写“通过连接池复用数据库连接,降低连接创建开销”这句话要能对应到配置。
3.2 applicationContext.xml 与 spring-mvc.xml 的配置分工
SSM 最容易搞混的是两个 Spring 配置文件的分工。applicationContext.xml 管数据源、事务、Service 层和 Mapper 扫描,spring-mvc.xml 只管 Controller 层和视图解析。注意两个文件的 component-scan 扫描范围不能重叠,否则会出现事务代理失效的问题——Controller 被放进 Spring 容器而不是 SpringMVC 容器,@Transactional 就不起作用了。
<!-- applicationContext.xml --> <context:component-scan base-package="com.example.hrms.service"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/hrms?useSSL=false&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.example.hrms.entity"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>url 里有两个参数是实战中必踩的坑:useSSL=false 是避免 MySQL 8 默认的 SSL 握手警告;serverTimezone=Asia/Shanghai 是处理数据库时区导致的日期差 8 小时问题,后者的报错信息是 "The server time zone value" 开头,记住这个特征可以少走弯路。mapperLocations 指向类路径下的 mapper 目录,XML 文件不能放在 java 源码目录里,要放在 resources 下,这是很多同学首次启动时 Mapper 找不到绑定语句的原因。
spring-mvc.xml 里需要配置 DispatcherServlet 的拦截路径。web.xml 中配置 servlet-mapping 时用 / 而不是 /,/ 不会拦截 JSP,/会把 JSP 请求也交给 DispatcherServlet 处理,导致视图无法渲染。如果项目用了 RESTful 风格的路径,还需要在 spring-mvc.xml 里开启注解驱动<mvc:annotation-driven/>,否则 @RequestMapping 不生效。
3.3 MD5 加盐登录校验的完整代码路径
登录认证部分是论文里可以单独成一节的内容。明文密码直接存数据库是安全硬伤,答辩老师大概率会问“密码在数据库里怎么存的”。用 MD5 加盐做一个可解释的方案,比引入 Spring Security 更适合这个项目的定位。
public class Md5Util { public static String getSalt() { String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random random = new Random(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < 8; i++) { sb.append(chars.charAt(random.nextInt(chars.length()))); } return sb.toString(); } public static String encrypt(String password, String salt) { String base = password + salt; for (int i = 0; i < 3; i++) { base = DigestUtils.md5DigestAsHex(base.getBytes(StandardCharsets.UTF_8)); } return base; } }encrypt 方法里做了一个 3 次循环加密,即把上一次的 MD5 值继续参与下一轮摘要。参数上的考虑:每次 MD5 都是对“密码 + 盐”整体做摘要,保证相同的密码在不同盐值下产生不同的密文;循环 3 次是增加彩虹表攻击的成本,但并不把 MD5 当作高安全性方案,论文里写清楚这一点反而显得客观。salt 在用户注册时生成,存到 t_employee 表,登录时取出该用户的 salt,对输入密码做同样的 encrypt 计算再比对。
登录的 Controller 层逻辑要处理三个分支:用户不存在、密码错误、验证通过。验证通过后把员工 ID 和姓名放进 session,同时记录一条登录日志。后续拦截器检查 session 中是否有登录标记,没有就重定向到 login.jsp,实现一个轻量的登录拦截。这样一个简单的认证链路,对应了 Java 面试和毕业设计里常被追问的“Session 和 Cookie 的区别”“MD5 为什么需要加盐”等问题,属于投入产出比很高的代码模块。
4. 员工管理与考勤统计模块的 CRUD 落地
4.1 员工多条件分页查询的 Mapper 实现
员工列表是人事系统里最常用的页面,支持按姓名、部门、状态、入职日期范围四个条件过滤,后端用 MyBatis 动态 SQL 实现。分页可以用 PageHelper 插件,也可以用 LIMIT 手写,我建议手写 LIMIT,因为答辩时可以现场推导 SQL,而插件封装了过程反而不好讲。
<select id="selectEmployeePage" resultType="com.example.hrms.entity.Employee"> SELECT e.*, d.dept_name FROM t_employee e LEFT JOIN t_dept d ON e.dept_id = d.id <where> <if test="name != null and name != ''"> AND e.name LIKE CONCAT('%', #{name}, '%') </if> <if test="deptId != null"> AND e.dept_id = #{deptId} </if> <if test="status != null"> AND e.status = #{status} </if> <if test="hireDateBegin != null"> AND e.hire_date >= #{hireDateBegin} </if> <if test="hireDateEnd != null"> AND e.hire_date <= #{hireDateEnd} </if> </where> ORDER BY e.emp_no LIMIT #{offset}, #{pageSize} </select>这段动态 SQL 有几个参数要细说。<where>标签会自动去掉第一个<if>条件前的 AND,这是 MyBatis 的内置行为,不要写成 WHERE 1=1 这种老式写法。LIKE 查询用 CONCAT 拼接 %,而不是在 Java 代码里拼好再传,因为后者的写法在 MyBatis 的 ${} 场景下会有 SQL 注入风险,而 #{} 是预编译参数,可以在论文的测试章节里做一个注入用例演示。date 类型的比较用大于等于和小于等于覆盖一个完整日期范围,注意大于号在 XML 里要转义为>,否则 XML 解析报错。#{offset}在前端传 pageNum 和 pageSize 时,在 Service 层计算出 offset = (pageNum - 1) * pageSize,这个计算逻辑写在 Service 里而不是前端,保证接口只传页码。
关联查询用 LEFT JOIN 带出 dept_name,返回结果用 resultType 映射到实体类,实体类里加一个 deptName 字段但表中没有这个列。这种额外的映射字段需要 MyBatis 开启驼峰转换配置<setting name="mapUnderscoreToCamelCase" value="true"/>,否则 dept_name 映射不到 deptName。在 JSP 页面用 JSTL 的 forEach 循环渲染表格,底部分页栏显示“共 N 条,第 x/y 页”,这和课程设计里常见的分页组件逻辑一致。
4.2 考勤月统计的 SQL 写法与金额计算
考勤统计在人事系统里属于高频需求,页面一般展示“某员工某月的迟到次数、早退次数、请假天数、实际出勤天数”。最直接的做法是 SQL 按 emp_id 和 date_format 分组聚合,而不是在 Java 里启用数据循环,后者在数据量大的时候性能差距很明显。
SELECT e.emp_no, e.name, DATE_FORMAT(a.work_date, '%Y-%m') AS month, COUNT(CASE WHEN a.is_late = 1 THEN 1 END) AS late_count, COUNT(CASE WHEN a.is_early_leave = 1 THEN 1 END) AS early_leave_count, COUNT(CASE WHEN a.status = 'normal' THEN 1 END) AS normal_days, SUM(CASE WHEN a.is_late = 1 OR a.is_early_leave = 1 THEN 50 ELSE 0 END) AS penalty_amount FROM t_attendance a LEFT JOIN t_employee e ON a.emp_id = e.id WHERE a.work_date BETWEEN #{monthStart} AND #{monthEnd} GROUP BY e.emp_no, e.name, DATE_FORMAT(a.work_date, '%Y-%m') ORDER BY e.emp_no这个查询有四个关键点。第一,COUNT 配合 CASE WHEN 做条件计数,这是统计类 SQL 的常用写法,比“查出所有记录再在 Java 里 if 判断”更清晰,答辩时能展开讲。第二,penalty_amount 用 SUM(CASE WHEN) 计算迟到早退的扣款,每笔固定 50 元,这个金额参数如果希望可配置,就把它抽到系统参数表,而不是写在 SQL 字面量里。第三,BETWEEN 的边界日期要精确到当月第一天和最后一天,即 monthStart 传 "2024-06-01",monthEnd 传 "2024-06-30",传 "2024-06" 会导致隐式转换问题。第四,参数用 #{} 会把整个日期字符串当作一个参数传入,不需要额外做日期格式转换。
性能上,t_attendance 表的数据会随使用时间持续增长,SQL 的 WHERE 条件里 work_date 如果加了索引,BETWEEN 能走 range 扫描。可以在建表语句里给 work_date 单独建索引,这就是第 2 章表结构设计里预留的扩展点。如果数据量到了百万级,更好的方案是引入按月分表,但毕业设计做到这一步已经足够展示能力。
4.3 事务控制:批量导入员工时回滚的边界
批量导入员工是一个高频功能,Excel 上传后逐行解析并 INSERT。这里必须在 Service 方法上加 @Transactional,否则导入过程中遇到某行数据非法,前面已经插入的记录无法回滚,数据库里会出现半批数据。事务的边界设置为“解析完整个 Excel 后再逐条插入”,而不是“解析一行插入一行”,避免每行一个事务导致的性能浪费。
@Transactional(rollbackFor = Exception.class) public int batchImport(List<Employee> employees) { int count = 0; for (Employee emp : employees) { validate(emp); emp.setEmpNo(generateEmpNo()); String salt = Md5Util.getSalt(); emp.setSalt(salt); emp.setPassword(Md5Util.encrypt("123456", salt)); employeeMapper.insert(emp); count++; } return count; }rollbackFor = Exception.class 这个参数值得单独解释。Spring 默认只对 RuntimeException 回滚,而 Excel 解析时抛出的异常可能是 IOException 或自定义的 CheckedException,如果不声明 rollbackFor,事务不会回滚,这是事务“看似失效”的经典现场。validate 方法里做非空校验和手机号格式校验,校验失败抛出业务异常 RuntimeException,触发事务回滚。每个新员工的初始密码设为 123456 并加盐加密,首次登录后可以提示修改密码,这个逻辑在论文的“系统测试”章节可以作为一条完整用例。
生成的员工编号在 generateEmpNo 方法中用数据库当前 max(emp_no) 加 1,并发场景下可能出现重复,可以在表中用唯一索引兜底并捕获 DuplicateKeyException,在异常里生成新的编号重试一次。事务边界在 Service 层,Controller 只接收 List,这样的分层在完整源码里一目了然,评审老师打开代码能找到对应位置。
5. 论文、答辩演示与部署自查的实用技巧
5.1 论文架构与源码的对应关系
论文的目录结构不要照搬模板,要按自己的系统真实功能来写。需求分析章节写用例图,画“员工管理”“考勤管理”“请假审批”三个主用例;数据库设计章节把第 2 章的表结构表格原样放进去,附上建表 SQL;系统实现章节按模块分小节,每个模块配一个功能截图加一段核心代码,代码选最关键的部分即可。
代码截图有一个常见问题:字体太小看不清。我在处理的时候会把 IDE 字号调到 16,并把代码行号裁掉,这样打印出来仍然是清晰的。论文中引用的每一个数据表、每一个接口路径,都要保证在源码里真实存在。
5.2 答辩演示的 3 个高频提问点
演示环节建议按“登录→员工列表→新增员工→考勤月统计”这条主线走,控制在 5 分钟以内。老师经常追问的第一个问题是“SpringMVC 处理一个请求的完整流程”,回答要点是 DispatcherServlet 分发到 Controller,返回 ModelAndView,视图解析器解析到 JSP;第二个是“MyBatis 中 #{} 和 ${} 的区别”,演示时可以现场把 SQL 日志打开,展示预编译后的 SQL;第三个是“数据库表为什么这样设计”,把部门表和员工表的外键关系、考勤表的唯一索引讲清楚即可。这三个问题准备好,答辩的把握就大很多。
5.3 部署到服务器前的自查清单
项目交付时需要提供一个可运行的 WAR 包和一份启动说明。下面的表格式清单很适合直接写进 README 或 PPT 最后一页。
| 检查项 | 要求 | 常见问题 |
|---|---|---|
| JDK 版本 | 1.8 或 11 | 版本过高导致 Tomcat 启动失败 |
| MySQL 版本 | 5.7 或 8.0 | 8.0 需要配置驱动类与时区 |
| 数据库字符集 | utf8mb4 | 中文乱码的根源 |
| 连接串编码 | characterEncoding=utf8 | 与数据库字符集保持一致 |
| Tomcat 版本 | 9.0 | 对应 Servlet 4.0,和 pom 里 scope 匹配 |
| WAR 包放置 | webapps 根目录 | 部署时上下文路径会影响访问 URL |
| 端口占用 | 8080 | 被占时改 conf/server.xml 并重启 |
最后,验证部署结果的命令是curl http://localhost:8080/hrms/login,返回包含登录表单的 HTML 即视为启动成功。数据库导入时用source命令执行 SQL 脚本,执行过程中如果出现大量 ERROR 输出,优先检查脚本头部是否缺少USE hrms,这是 SQL 脚本无法导入最常见的原因。整个流程走通之后,这套源码加数据库脚本加论文加 PPT 就可以作为一个完整方案交付。
本文还有配套的精品资源,点击获取