SSM+JSP员工考勤管理系统:从环境搭建到避坑实战

发布时间:2026/10/10 3:04:28
SSM+JSP员工考勤管理系统:从环境搭建到避坑实战
简介这是一份基于 SSMSpringSpringMVCMyBatis与 JSP 技术栈的员工考勤管理系统面向高校毕业设计、课程设计及 Java Web 初学者可帮助读者快速搭建并理解企业级分层开发的项目骨架。系统按一般员工、部门经理、系统管理员三种角色划分权限覆盖个人资料、上班时间公告、请假、出差、差费报销、考勤及日常出勤等核心模块管理员还可进行系统用户、部门、员工管理以及员工/请假统计业务链路完整。压缩包为 zip 格式约 29.96MB内含可运行源码、SQL 数据库文件与配套说明文档可直接部署运行。已有 1847 人学习浏览适合作为项目实战参考、答辩演示或课程作业的完整方案。通过源码与 SQL 对照可掌握 SSM 整合配置、JSP 页面开发、角色权限控制及考勤数据统计等关键技能。1. 这套公司员工考勤管理系统为什么还值得花时间跑一遍手里拿到一份名为“315ssm_mysql_jsp 公司员工考勤管理系统”的压缩包时第一反应可能是都什么年代了还在用 SSM JSP 写考勤但你打开源码会发现这类项目恰恰是理解 Java Web 服务端开发最完整的样本。Spring 管对象、SpringMVC 管路由、MyBatis 管数据库JSP 从服务器渲染页面整条链路没有任何黑匣子断点能一路从浏览器打到 MySQL。对于刚入行的开发者或是需要快速搭一套内部管理系统的中小公司这个技术组合落地成本极低部署只需要一个 Tomcat、一个 MySQL 实例不需要引入微服务体系。这套系统解决的是行政和人事最头疼的日常事务员工签到签退、请假审批、加班登记、月末考勤统计。相比拿 Excel 手工登记它能自动计算迟到早退、按部门汇总出勤率数据留痕可追溯。适合拿来学习的人是刚学完 SSM 理论但没写过完整项目的在校生适合拿来用的人是公司还没有专用 OA、想先低成本跑起来的小团队。这篇笔记会从环境搭建开始带你把数据库脚本、后端逻辑、前端页面整条链路跑通并讲清楚最容易翻车的几个地方。2. SSM 框架搭建三个组件怎么分工配置从哪开始写2.1 Spring、SpringMVC、MyBatis 在考勤系统里各管什么SSM 是三个开源框架的缩写组合在这个考勤系统里分工非常清晰。Spring 是容器负责创建和管理 Service、Mapper 这些对象同时承担事务控制SpringMVC 是 Web 层框架所有以 .do 或 /api 开头的请求都先进 DispatcherServlet再由它分发到 Controller 的某个方法MyBatis 负责数据库访问把 Java 方法调用映射成 SQL 语句。三层各司其职所以你把项目拆开看目录结构一眼就能看出请求从 JSP 页面发出后经过了 Controller、Service、Mapper最后落到 MySQL。选择这套组合而不是 Spring Boot不是因为它更新而是因为它更透明。Spring Boot 的自动配置省事但出了问题不好排查底层逻辑。SSM 的配置全部显式写在 XML 里数据源连的是哪个库、Mapper 扫描哪个包、视图解析器怎么拼接 JSP 路径全部一眼可见。对新手来说这个透明度本身就是最好的学习材料。考勤系统没有高并发、没有分布式事务SSM 的轻量和可控恰好匹配需求。2.2 本地跑通的最小配置pom.xml、web.xml、Spring 配置文件从压缩包里解压后先看目录结构。典型布局是 src/main/java 放 Java 源码src/main/resources 放配置文件src/main/webapp 放 JSP 页面sql 目录放数据库脚本doc 目录放说明文档。我一般会把源码导入 IDE 后先改三个配置文件再导入 SQL最后启动 Tomcat。第一步是确认 pom.xml 里的依赖完整核心依赖如下dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.20/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.10/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies这里的关键是 mybatis-spring 这个桥接包它负责把 MyBatis 的 SqlSessionFactory 交给 Spring 容器管理。没有它MyBatis 和 Spring 就是两套独立体系事务和 Mapper 扫描都会失效。版本上 spring-webmvc 和 mybatis-spring 要匹配如果 Spring 版本太新而桥接包太旧启动时会直接报 BeanCreationException。2.3 数据源与 Mapper 扫描三个必调的参数配置的核心在 spring-mvc.xml 里一个文件管三件事开启注解驱动、配置数据源、配置视图解析器。我习惯把它拆开写便于排查。最小可运行版本如下context:component-scan base-packagecom.company.attendance/ mvc:annotation-driven/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/attendance?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value123456/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.company.attendance.dao/ /bean bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean三个必调参数逐个说。第一个是数据源 url这里的 serverTimezoneAsia/Shanghai 必须加MySQL 8.0 驱动默认使用 UTC 时区不加的话打卡时间和服务器本地时间会差 8 小时后面所有考勤判定全部错位。第二个是 mapperLocations配置的是 MyBatis XML 映射文件的路径路径写错启动不报错但一调用 Mapper 方法就报 Invalid bound statement。第三个是 basePackage它告诉 Spring 去哪个包扫描接口生成代理实现类这个包名必须和你的 DAO 接口所在包完全一致。视图解析器的 prefix、suffix 决定 Controller 返回的字符串怎么拼接成实际 JSP 路径如果你把 JSP 放在 webapp 根目录而不是 WEB-INF 下这里也要对应修改。3. 数据库设计考勤系统的表结构为什么这样拆3.1 员工表、部门表、考勤记录表核心表关系先理清打开 sql 目录下的脚本文件能看到一个完整的考勤数据库设计。设计思路上遵循第三范式把员工信息和考勤行为分开存避免冗余。核心是四张表部门表、员工表、考勤记录表、请假申请表。部门表和员工表是一对多关系员工表和考勤记录表是一对多关系员工表和请假申请表也是一对多关系。逻辑上很直观一个部门有多个员工一个员工每天有多条考勤记录。表结构设计直接决定后面统计 SQL 的复杂度。我一般会先检查员工表是否包含部门外键考勤记录表是否包含员工外键索引是否建立在关联字段上。一个常见毛病是表建好了但没加索引数据量到几万条后月底统计报表直接卡死。建表脚本核心部分如下CREATE TABLE dept ( id INT NOT NULL AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL COMMENT 部门名称, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT部门表; CREATE TABLE employee ( id INT NOT NULL AUTO_INCREMENT, dept_id INT NOT NULL COMMENT 所属部门ID, emp_no VARCHAR(20) NOT NULL COMMENT 工号, name VARCHAR(30) NOT NULL COMMENT 姓名, password VARCHAR(64) NOT NULL COMMENT 登录密码, position VARCHAR(50) DEFAULT NULL COMMENT 岗位, PRIMARY KEY (id), UNIQUE KEY uk_emp_no (emp_no), KEY idx_dept_id (dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工表;注意几个细节。emp_no 工号加了唯一索引这是员工登录的账号不允许重复dept_id 加普通索引因为按部门统计是高频查询密码字段长度设为 64是为了兼容 MD5 或 SHA-256 摘要结果如果只设 32 位后续想升级加密算法就麻烦了。字符集用 utf8mb4 而不是 utf8因为 utf8 在 MySQL 里存不了生僻字和部分表情符号员工姓名场景虽然概率低但一次乱码就够折腾了。3.2 打卡状态位一天的考勤记录怎么存储考勤记录表是整个系统的核心设计它的时候要回答一个问题一个员工一天需要几条记录。常见方案是两条一条上班打卡、一条下班打卡用 type 字段区分。但也有一行搞定的方案就是下面这种设计每条记录对应员工某一天签退时间允许为空下班前再更新同一行。两种方案各有取舍两行方案更灵活可以支持一天多次打卡的班次单行方案统计更方便一条记录就是一个员工一天的完整考勤状态。CREATE TABLE attendance ( id INT NOT NULL AUTO_INCREMENT, emp_id INT NOT NULL COMMENT 员工ID, work_date DATE NOT NULL COMMENT 上班日期, clock_in_time DATETIME DEFAULT NULL COMMENT 上班打卡时间, clock_out_time DATETIME DEFAULT NULL COMMENT 下班打卡时间, status TINYINT DEFAULT 0 COMMENT 0正常 1迟到 2早退 3迟到早退 4缺卡, PRIMARY KEY (id), UNIQUE KEY uk_emp_date (emp_id, work_date), KEY idx_work_date (work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤记录表;这里的关键设计是唯一索引 uk_emp_date它保证同一员工同一天只有一条考勤记录。第一次打卡执行 INSERT第二次打卡执行 UPDATE不会出现重复记录。status 字段是冗余设计它不靠数据库计算而是由后端在打卡时判断后写入虽然多占一个字节但月末统计时直接 WHERE status 0 就能找出所有异常考勤不用每次现算。这个取舍对考勤这种高频写、低频复杂查的场景非常划算。3.3 SQL 脚本导入命令行三步走拿到 sql 文件后我习惯直接用命令行导入不通过 IDE 的可视化工具因为脚本里可能包含存储过程或事务语句某些图形化工具会半路报错。导入前先确认 MySQL 服务已启动然后依次执行建库、选库、导入三步。mysql -u root -p -e CREATE DATABASE IF NOT EXISTS attendance DEFAULT CHARSET utf8mb4; mysql -u root -p attendance attendance.sql mysql -u root -p -e USE attendance; SHOW TABLES;第一条命令创建数据库指定 utf8mb4 字符集避免脚本里建表语句没写 DEFAULT CHARSET 导致乱码第二条命令把脚本里的建表和初始化数据全部导入第三条命令验证是否出现 dept、employee 等表。如果导入时报 Unknown database 错误说明第一条命令执行失败检查 root 密码和 MySQL 服务状态。如果报语法错误用记事本打开 sql 文件检查文件编码是不是 UTF-8从 Windows 下拷贝的脚本经常是 GBK 编码在 Linux 上执行就会乱码报错。4. 核心业务实现签到、签退、请假审批的代码调用链4.1 登录后签到Controller 到 Service 到 Mapper 的完整流程考勤系统最核心的操作就是签到。用户登录后进入首页点击签到按钮前端发起请求到后端后端先查今天有没有记录没有就插入有就更新签退时间。下面是 Controller 层的签到接口它接收当前登录员工的 ID调用 Service 完成打卡逻辑Controller RequestMapping(/attendance) public class AttendanceController { Autowired private AttendanceService attendanceService; RequestMapping(/sign) ResponseBody public MapString, Object sign(HttpSession session) { Employee emp (Employee) session.getAttribute(loginUser); MapString, Object result new HashMap(); if (emp null) { result.put(code, 401); result.put(msg, 未登录); return result; } try { attendanceService.sign(emp.getId()); result.put(code, 200); result.put(msg, 打卡成功); } catch (Exception e) { result.put(code, 500); result.put(msg, 打卡失败 e.getMessage()); } return result; } }这段代码里有两个容易被忽略的点。第一个是登录用户从 session 里取而不是从表单里再传一次防止伪造请求替别人打卡。第二个是返回类型用 Map 配合 ResponseBody这比返回 ModelAndView 更简洁前端拿到 JSON 后自行提示成功或失败。实际项目里我给 session 里的用户对象加了个超时时间30 分钟无操作自动失效考勤这种敏感操作必须保证会话有效。4.2 迟到早退判定后端计算的边界条件签到签退的核心在 Service 层这里要做两件事判断是签到还是签退计算是否迟到早退。常见做法是在 application.properties 或 XML 里配置上班时间和下班时间Java 代码按时间字符串比较。这里有个容易踩坑的点不要用 String 的 compareTo 直接比较时间要用 LocalTime 解析后再比否则格式不统一会出现 8:5 这种脏数据导致比较错误。public void sign(Integer empId) { LocalDate today LocalDate.now(); Attendance record attendanceDao.findByEmpIdAndDate(empId, today); LocalTime now LocalTime.now(); LocalTime workStart LocalTime.parse(09:00:00); LocalTime workEnd LocalTime.parse(18:00:00); if (record null) { record new Attendance(); record.setEmpId(empId); record.setWorkDate(today); record.setClockInTime(LocalDateTime.now()); int status now.isAfter(workStart) ? 1 : 0; record.setStatus(status); attendanceDao.insert(record); } else { record.setClockOutTime(LocalDateTime.now()); if (now.isBefore(workEnd)) { record.setStatus(record.getStatus() 2); } attendanceDao.update(record); } }边界条件要单独说。9:00:00 整打卡算不算迟到各公司定义不同如果要求 9 点前必须到用 isAfter 就是迟到如果 9 点整还可以要用 isBefore 配合等号判断。同样18:00:00 整打卡算准时还是早退要按公司制度来。我把这两个判断抽成了独立方法放到 config 里方便调整。还有跨天情况如果下班时间是凌晨 2 点LocalTime.parse 和 LocalDate.now 组合就会出问题需要引入班次表概念但基础版考勤系统很少涉及。4.3 请假审批与月度统计SQL 聚合怎么写请假模块相对独立核心是审批流。员工提交请假申请上级登录后看到待审批列表点击通过或驳回。数据上只需要在请假表里加一个 status 字段0 待审批、1 已通过、2 已驳回。审批通过后考勤统计时要把请假天数从应出勤天数里扣除。这里要注意事务审批操作和扣减考勤状态要放在同一个事务里否则会出现审批通过了但统计还显示缺勤的数据问题。SELECT e.emp_no, e.name, d.dept_name, COUNT(DISTINCT a.work_date) AS actual_days, SUM(CASE WHEN a.status IN (1, 3) THEN 1 ELSE 0 END) AS late_days, SUM(CASE WHEN a.status IN (2, 3) THEN 1 ELSE 0 END) AS leave_early_days, SUM(CASE WHEN a.status 4 THEN 1 ELSE 0 END) AS miss_days FROM employee e LEFT JOIN dept d ON e.dept_id d.id LEFT JOIN attendance a ON e.id a.emp_id WHERE a.work_date BETWEEN #{startDate} AND #{endDate} GROUP BY e.id, e.emp_no, e.name, d.dept_name;这条 SQL 是月末统计的主力。使用 LEFT JOIN 而不是 INNER JOIN是为了把没打卡的员工也统计出来否则没考勤记录的人直接消失。SUM(CASE WHEN ...) 是条件聚合的标准写法比多次查询再在 Java 里拼装高效得多。GROUP BY 后面跟着的字段要和 SELECT 里非聚合字段严格一致MySQL 5.7 之后默认开启了 ONLY_FULL_GROUP_BY不一致直接报语法错误。实际跑这条 SQL 之前我一般会在 MySQL 客户端先执行一遍 EXPLAIN看看有没有走索引如果 type 列出现 ALL 就说明全表扫描了数据量大时要优化索引。5. 避坑排查SSM 考勤系统最常见的五个翻车现场5.1 现象本地启动 Tomcat 后访问页面 404原因可能有两个方向。一个是项目没有正确部署到 webapps 目录IDE 里显示启动了但访问路径不对另一个是 SpringMVC 的前端控制器 url-pattern 配置把 JSP 请求也拦截了导致视图解析器找不到页面。排查时先看 Tomcat 控制台日志里项目是否 successful started再在浏览器地址栏试访问 /项目名/ 根路径。我遇到最多的场景是 web.xml 里 DispatcherServlet 的 url-pattern 配置成了 /*这会把所有请求包括 JSP 都拦下来正确做法是配成 *.do 或 /。5.2 现象数据库里记录的时间和本地时间差 8 小时这个基本可以断定是 JDBC 连接串的时区问题。MySQL 8.0 驱动默认连接时区是 UTC如果代码里用 LocalDateTime.now() 写入时间会直接取本机时间但查询时驱动又按 UTC 转换一进一出就差了 8 小时。解决方式就是在数据源 url 里显式声明 serverTimezoneAsia/Shanghai。注意这个参数在连接串中要放在末尾前面用 连接多个参数XML 里 符号要转义成 忘了转义 Spring 解析 XML 时会直接报错。5.3 现象Mapper 接口方法调用报 BindingException说 Invalid bound statement原因是 MyBatis 找不到对应的 XML 映射文件。常见情况有三种XML 文件没有放在 mapperLocations 指定的目录下XML 文件里的 namespace 写错了包名DAO 接口方法名和 XML 里语句的 id 不一致。排查方法很直接打开编译后的 target/classes 目录看 mapper 目录下有没有对应的 XML 文件没有就说明资源没有打进 classes需要在 pom.xml 里配置资源过滤。5.4 现象打卡成功后数据库里没有记录但页面提示成功这是典型的 Spring 事务没有生效。SSM 整合时需要在 spring 配置文件里配置事务管理器 DataSourceTransactionManager并在 Service 实现类上标注 Transactional。如果这两个条件不满足Service 方法查完数据库会自动提交但一旦后续有异常回滚数据就丢了而 Controller 层被 try-catch 捕获后依然返回成功提示。排查时在 Service 方法里加一个日志输出查看有没有进入事务代理最简单的方法是确认配置文件中有 txManager 这个 bean 且注解驱动开启。5.5 现象修改 JSP 页面后刷新不生效还是旧页面Tomcat 对 JSP 有缓存机制开发时修改页面后需要清理 work 目录下的缓存。有些 IDE 的 Tomcat 插件是热部署模式但 JSP 缓存依然存在。解决方式是停掉 Tomcat删除 CATALINA_HOME/work 目录下的内容再重启。另外如果使用 IDEA 的 Tomcat 集成部署确认 Deployment 里是 war exploded 模式而不是 war 包模式后者每次改 JSP 都要重新打包。6. 进阶路线跑通之后怎么改出更适合生产的样子6.1 从 JSP 到前后端分离给考勤系统留一条升级路基础版考勤系统用的是 JSP 服务端渲染页面嵌在 Java 代码里前端改动要重启 Tomcat不灵活但胜在简单。如果想升级成前后端分离不需要推倒重来。Controller 层本来就是返回 JSON只需要把原来返回 ModelAndView 的方法全部改成 ResponseBody再把 JSP 页面替换成静态 HTML 加 Vue 或原生 JS。视图解析器可以保留但原来的 /WEB-INF/jsp/ 路径已经用不上。我做过一次这样的迁移两天时间就把登录、打卡、统计三个核心页面全部切换完成效果立竿见影页面交互响应更快前端也终于可以用现代构建工具调试。6.2 给考勤记录加一层 Redis 缓存如果考勤系统承担整个公司几百人的打卡压力月底统计频繁查询数据库会吃力。我一般会在统计报表接口前加一层 Redis 缓存key 用统计参数的 MD5 值value 存统计结果 JSON设置 5 分钟过期。打卡后主动删除对应缓存这样同一天的统计结果不会出现脏数据。引入 Redis 后用 Spring Data Redis 模板操作但要注意序列化方式默认 JDK 序列化在 Redis 客户端里看到的是乱码配置时用 GenericJackson2JsonRedisSerializer 替换。这一步做完统计接口的响应时间通常能从几百毫秒降到几十毫秒。6.3 跑通后的自测清单与交接文档我把这个系统交付给别人之前一般会跑一遍自己写的自测清单新建一个部门、新建员工、用该员工登录、签到、签退、提交请假申请、用管理员账号审批、查看月度统计报表。整个流程走完后确认数据写入正确再检查异常分支重复打卡会不会更新同一行未审批的请假会不会计入缺勤删除部门时关联员工如何处理。确认无误后把配置文件里的数据库密码改掉把初始化账号信息写进交付文档。想起一次交付经历某公司拿去用了两周月底算考勤时发现统计数据和手工台账对不上排查后定位是员工跨天加班到凌晨 2 点打卡时间记到了第二天被算成了缺卡。后来我在打卡逻辑里加了一个规则22:00 之后的打卡自动归属到前一天的工作日记录才算把这个问题解决。这类业务规则是需求文档里永远不会写清楚的只能在实际使用中不断补漏。考勤系统看起来简单真正跑到业务里才知道边界条件有多磨人希望这篇笔记能帮你少走一段弯路。本文还有配套的精品资源点击获取