简介:这是一套基于SSM框架与JSP技术开发的公司员工考勤管理系统完整源码,适用于本科毕业设计、课程设计及Java Web初学者项目实践,聚焦企业级考勤业务全流程管理。系统采用角色驱动设计,支持普通员工、部门经理和系统管理员三类用户,覆盖个人资料维护、请假/出差/差旅报销审批、日常考勤记录、出勤统计分析等核心功能模块,具备完整的权限控制与业务闭环能力。资源为29.96MB的ZIP压缩包,包含可直接运行的Java Web源码、MySQL建库建表SQL脚本、系统使用说明文档等关键文件,结构清晰、注释规范,便于快速部署与二次开发。目前已有1847人学习下载,配套文档详述各模块逻辑与数据库设计,特别适合需要理解SSM整合流程、掌握JSP前后端交互及构建真实业务系统的中初级开发者参考与复用。
1. 这不是又一个“登录+增删改查”的SSM模板,而是一套真实跑在Tomcat上的考勤业务闭环系统
很多同学拿到毕业设计选题时,第一反应是搜“SSM考勤系统源码”,结果下载一堆只有UserMapper、UserServiceImpl、UserController三层空壳的项目——连请假单状态流转都靠前端JS硬编码,数据库字段命名全用中文拼音,SQL里写死部门ID。但这个315ssm_mysql_jsp包不一样:它完整实现了「员工提交请假→部门经理审批→系统自动扣减出勤天数→月底生成请假统计报表」的业务链路,所有状态变更都经由MyBatis动态SQL控制,MySQL表结构包含leave_status TINYINT(1) DEFAULT 0 COMMENT '0待审批,1已通过,2已驳回'这类真实业务字段。它不教你怎么配Spring MVC视图解析器,而是直接让你看到/leave/apply.do接口如何校验当前用户是否为部门经理、能否审批本部门员工、是否超出年度请假额度。适合需要交付可演示、可答辩、能讲清业务逻辑的本科毕设或Java课程设计学生,也适合想快速复现SSM+JSP老式Web架构落地细节的初级后端开发者。
2. SSM三层架构如何支撑多角色考勤业务:从Spring配置到MyBatis动态SQL的落地细节
2.1 Spring MVC配置文件中隐藏的权限路由控制逻辑
该系统的spring-mvc.xml没有使用Spring Security,而是通过自定义拦截器实现角色路由控制。关键配置如下:
<!-- spring-mvc.xml --> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/admin/**"/> <bean class="com.interceptor.AdminInterceptor"/> </mvc:interceptor> <mvc:interceptor> <mvc:mapping path="/manager/**"/> <bean class="com.interceptor.ManagerInterceptor"/> </mvc:interceptor> <mvc:interceptor> <mvc:mapping path="/employee/**"/> <bean class="com.interceptor.EmployeeInterceptor"/> </mvc:interceptor> </mvc:interceptors>提示:三个拦截器类均继承
HandlerInterceptorAdapter,重写preHandle()方法。AdminInterceptor中会从Session获取userRole,若不等于"admin"则重定向至/error/403.jsp;ManagerInterceptor额外校验departmentId是否匹配当前用户所在部门——这是防止部门经理越权审批其他部门员工的核心防线。
这种基于路径前缀的拦截方式虽不如RBAC灵活,但在毕业设计场景下更易理解、调试和展示。你可以在AdminInterceptor.java第37行看到具体判断逻辑:
// AdminInterceptor.java HttpSession session = request.getSession(); User user = (User) session.getAttribute("user"); if (user == null || !"admin".equals(user.getRole())) { response.sendRedirect(request.getContextPath() + "/error/403.jsp"); return false; } return true;2.2 MyBatis映射文件中的动态SQL:处理“部门经理只能审本部门请假”这一业务约束
LeaveMapper.xml中updateLeaveStatus方法不是简单UPDATE leave SET status = #{status} WHERE id = #{id},而是嵌套了部门归属校验:
<!-- LeaveMapper.xml --> <update id="updateLeaveStatus" parameterType="map"> UPDATE t_leave SET status = #{status}, approve_time = NOW(), approver_id = #{approverId} WHERE id = #{id} AND employee_id IN ( SELECT id FROM t_employee WHERE department_id = ( SELECT department_id FROM t_employee WHERE id = #{approverId} ) ) AND status = 0 </update>2.2.1 SQL执行逻辑拆解
- 外层
WHERE id = #{id}确保操作指定请假单; - 子查询
SELECT id FROM t_employee WHERE department_id = (...)获取当前审批人所在部门的所有员工ID; AND employee_id IN (...)强制要求被审批员工必须属于审批人同部门;AND status = 0防止重复审批(状态非“待审批”时SQL影响行为为0)。
注意:此SQL依赖MySQL的
IN子查询能力,若部署到低版本MySQL(如5.5),需改写为JOIN形式。测试时可用SELECT * FROM t_leave l JOIN t_employee e ON l.employee_id = e.id WHERE e.department_id = ? AND l.status = 0验证数据一致性。
2.3 JSP页面与Servlet的数据绑定机制:如何让<form action="/leave/apply.do">真正提交到正确Controller
leave_apply.jsp中表单提交目标为/leave/apply.do,对应LeaveController.java中的方法:
@RequestMapping(value = "/leave/apply", method = RequestMethod.POST) public String applyLeave(@ModelAttribute Leave leave, HttpSession session, Model model) { User user = (User) session.getAttribute("user"); leave.setEmployeeId(user.getId()); leave.setApplyTime(new Date()); leave.setStatus(0); // 待审批 leaveService.addLeave(leave); model.addAttribute("msg", "请假申请已提交,请等待审批"); return "redirect:/employee/leave_list.jsp"; }2.3.1@ModelAttribute如何自动绑定表单字段
JSP中<input name="startTime" />会被Spring MVC自动映射为Leave.startTime属性,前提是:
Leave类中存在setStartTime(Date startTime)方法;- 表单提交时
startTime值格式为yyyy-MM-dd HH:mm:ss(JSP中用<fmt:formatDate>输出,<input type="text">需配合JS日期控件保证格式); spring-mvc.xml中已配置<mvc:annotation-driven />启用注解驱动。
若出现Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date'错误,需在Leave类上添加@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解,并确认pom.xml中spring-webmvc版本≥4.3.0(本项目使用4.3.20.RELEASE)。
3. MySQL数据库设计与初始化:从建表语句到业务索引优化
3.1 核心表结构解析:为什么t_attendance表需要复合索引(employee_id, work_date)
sql/employee_attendance.sql中创建考勤主表:
CREATE TABLE `t_attendance` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `employee_id` bigint(20) NOT NULL COMMENT '员工ID', `work_date` date NOT NULL COMMENT '工作日期', `check_in_time` time DEFAULT NULL COMMENT '上班打卡时间', `check_out_time` time DEFAULT NULL COMMENT '下班打卡时间', `status` tinyint(1) DEFAULT '0' COMMENT '0正常,1迟到,2早退,3缺勤', PRIMARY KEY (`id`), KEY `idx_emp_date` (`employee_id`,`work_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工考勤记录';3.1.1 索引选择依据
- 单独
employee_id索引无法高效支持「查询某员工近30天考勤」(需按work_date范围扫描); - 单独
work_date索引无法高效支持「查询某日所有员工考勤」(因work_date选择性低,大量重复值); - 复合索引
(employee_id, work_date)可同时满足:WHERE employee_id = ? AND work_date BETWEEN ? AND ?(员工月度考勤)WHERE employee_id = ? ORDER BY work_date DESC LIMIT 10(最近10条记录)
提示:执行
EXPLAIN SELECT * FROM t_attendance WHERE employee_id = 123 AND work_date >= '2024-01-01';应显示type=range且key=idx_emp_date,若出现type=ALL说明索引未生效,需检查字段类型是否一致(work_date必须为DATE类型,不可用VARCHAR存储)。
3.2 初始化数据脚本中的业务规则体现:t_department与t_employee的级联插入
sql/init_data.sql中部门与员工数据并非独立插入,而是通过外键约束保障一致性:
-- 先插入部门 INSERT INTO t_department (id, name, manager_id) VALUES (1, '技术部', 1001), (2, '人事部', 1002); -- 再插入员工,employee.department_id 必须存在于 t_department.id 中 INSERT INTO t_employee (id, name, department_id, role, username, password) VALUES (1001, '张三', 1, 'manager', 'zhangsan', 'e10adc3949ba59abbe56e057f20f883e'), (1002, '李四', 2, 'manager', 'lisi', 'e10adc3949ba59abbe56e057f20f883e'), (1003, '王五', 1, 'employee', 'wangwu', 'e10adc3949ba59abbe56e057f20f883e');3.2.1 密码字段为何是MD5哈希值?
password字段存储的是123456的MD5值(e10adc3949ba59abbe56e057f20f883e),而非明文。LoginController.java中校验逻辑为:
String inputPassword = DigestUtils.md5Hex(loginPassword); User dbUser = userService.findByUsername(username); if (dbUser != null && dbUser.getPassword().equals(inputPassword)) { // 登录成功 }注意:
DigestUtils.md5Hex()来自commons-codec库,pom.xml中已声明依赖。若替换为BCrypt等更安全算法,需同步修改UserService.updatePassword()中密码加密逻辑,并重新生成初始化数据。
3.3 统计类SQL:管理员页面的“员工统计”与“请假统计”如何避免N+1查询
AdminService.java中getEmployeeStatistics()方法使用单条SQL完成部门人数统计:
public List<Map<String, Object>> getEmployeeStatistics() { return sqlSessionTemplate.selectList("adminMapper.getEmployeeStatistics"); }对应AdminMapper.xml中的SQL:
<select id="getEmployeeStatistics" resultType="map"> SELECT d.name AS departmentName, COUNT(e.id) AS employeeCount, COUNT(CASE WHEN e.role = 'manager' THEN 1 END) AS managerCount, COUNT(CASE WHEN e.role = 'employee' THEN 1 END) AS staffCount FROM t_department d LEFT JOIN t_employee e ON d.id = e.department_id GROUP BY d.id, d.name </select>3.3.1 为什么不用Java代码循环查每个部门?
若采用“先查所有部门→遍历部门ID→对每个ID查员工数”方式,会产生N次数据库查询(N为部门数)。而上述SQL通过LEFT JOIN+GROUP BY一次性完成关联统计,即使部门数达100个,也仅执行1次查询。在毕业设计答辩中,可强调此设计规避了典型的N+1性能陷阱。
4. Tomcat部署与JSP编译调试:解决“页面404”、“EL表达式不解析”、“中文乱码”三大高频问题
4.1 Tomcat 8.5+环境下JSP编译路径与class文件定位
项目打包为WAR后部署到Tomcat,JSP实际编译位置为:
$CATALINA_HOME/work/Catalina/localhost/ROOT/org/apache/jsp/其中leave_005fapply_jsp.java和leave_005fapply_jsp.class即leave_apply.jsp的编译产物。
4.1.1 验证JSP是否被正确编译
启动Tomcat后访问http://localhost:8080/leave_apply.jsp,若返回404:
- 检查
webapps/ROOT/WEB-INF/web.xml中<welcome-file-list>是否包含index.jsp; - 查看
work目录下是否存在对应.java和.class文件; - 若存在
.java但无.class,说明编译失败,需检查JDK版本(本项目要求JDK 1.8,Tomcat 8.5默认兼容)。
提示:在
web.xml中添加JSP编译参数可强制重新编译:<servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> <init-param> <param-name>development</param-name> <param-value>true</param-value> </init-param> </servlet>
4.2 EL表达式失效的根因与修复:${user.name}显示为空字符串
常见原因及解决方案:
| 现象 | 原因 | 修复方式 |
|---|---|---|
${user.name}显示空白 | web.xml中<web-app>未声明version="3.0" | 将<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"后添加version="3.0" |
${user.name}显示${user.name}原样文本 | JSP页面顶部缺失<%@ page isELIgnored="false" %> | 在JSP首行添加该指令 |
${user.name}报错Property 'name' not found | User类缺少getName()方法或name字段为private但无getter | 检查User.java中public String getName(){return name;}是否存在 |
本项目web.xml已正确定义为3.0版本,因此只需确认JSP页面头部包含:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page isELIgnored="false" %>4.3 MySQL中文乱码终极排查表:从连接URL到JDBC驱动版本
当数据库存入中文后JSP页面显示??,按以下顺序逐项验证:
| 检查项 | 正确配置 | 验证命令/位置 |
|---|---|---|
| MySQL服务端字符集 | character_set_server = utf8mb4 | SHOW VARIABLES LIKE 'character_set_server'; |
| 数据库字符集 | CREATE DATABASE ... CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; | SHOW CREATE DATABASE employee_db; |
| 表字符集 | ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 | SHOW CREATE TABLE t_employee; |
| JDBC连接URL | jdbc:mysql://localhost:3306/employee_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8 | src/main/resources/jdbc.properties第3行 |
| JDBC驱动版本 | mysql-connector-java-5.1.47.jar(兼容MySQL 5.7+) | WEB-INF/lib/目录下jar包名 |
注意:
characterEncoding=utf8mb4必须与数据库实际字符集严格一致,若数据库为utf8(MySQL旧版伪UTF8),则此处需改为utf8,否则连接时抛出Unsupported charset异常。
5. 毕业设计答辩必答三问:从业务逻辑、技术选型到可扩展性的真实应对策略
5.1 “为什么用JSP而不是Vue/React?是不是技术落后?”
这不是技术先进性问题,而是教学场景适配性问题。JSP作为Java Web基础组件,其核心价值在于:
- 可追溯性:每个JSP页面的HTML、Java逻辑、EL表达式在同一文件,便于教师快速定位学生修改点;
- 零构建依赖:无需npm、webpack、node环境,Tomcat启动即运行,降低实验室机房部署复杂度;
- 教学对齐:高校Java Web课程普遍以Servlet+JSP为起点,后续再学前后端分离,符合认知梯度。
若答辩被质疑“过时”,可回应:“本系统聚焦考勤业务建模与SSM分层实践,UI层选择JSP是为了剥离前端框架干扰,集中验证后端业务逻辑完整性。如需升级,可在Controller层保持不变的前提下,将JSP替换为Thymeleaf模板,或通过REST API对接Vue前端——这正是我们课程设计中‘架构演进’模块的延伸课题。”
5.2 “请假审批流程能否支持多级审批?比如部门经理→HR→总经理”
当前系统为二级审批(员工→部门经理),扩展为三级需改动三处:
- 数据库:在
t_leave表新增hr_approver_id BIGINT、ceo_approver_id BIGINT、hr_status TINYINT、ceo_status TINYINT字段; - Service层:
LeaveService.approveLeave()方法增加状态机判断:if (currentStatus == 1 && hrApproverId != null) { // 部门经理已批,转交HR updateStatus(id, 2, hrApproverId); // 2=HR待审 } else if (currentStatus == 2 && ceoApproverId != null) { // HR已批,转交CEO updateStatus(id, 3, ceoApproverId); // 3=CEO待审 } - Controller层:
/leave/approve.do接口增加nextApproverRole参数,动态决定下一审批节点。
提示:答辩时可手绘状态流转图(待提交→部门审批→HR审批→CEO审批→完成),强调“状态字段+条件分支”是传统审批系统最稳妥的扩展方式,比引入Activiti等工作流引擎更贴合课程设计体量。
5.3 “如何证明系统真的能处理1000名员工的考勤数据?”
提供三项可验证的性能证据:
- 索引有效性验证:执行
SELECT COUNT(*) FROM t_attendance WHERE employee_id = 1001 AND work_date >= '2024-01-01';,在10万行数据下响应时间<50ms(需提前用存储过程批量插入测试数据); - 连接池配置:
applicationContext.xml中BasicDataSource设置maxActive="50",确保高并发时不会因连接耗尽导致请求超时; - 缓存预热设计:
AdminService.getDepartmentList()方法添加@Cacheable(value="departmentCache")注解(需启用Spring Cache),部门列表首次查询后永久缓存,避免每次统计都查库。
实际测试时,可用Apache Bench模拟并发:
ab -n 1000 -c 50 http://localhost:8080/employee/attendance_list.jsp?employeeId=1001观察AccessLog中平均响应时间及错误率,数据达标即可佐证系统基础负载能力。
本文还有配套的精品资源,点击获取