ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SSM员工考勤管理系统实战:从JSP打卡到MySQL统计避坑指南

SSM员工考勤管理系统实战:从JSP打卡到MySQL统计避坑指南 简介这是一套基于 SSMSpringSpringMVCMyBatis、MySQL 与 JSP 构建的公司员工考勤管理系统源码面向毕业设计、课程设计等开发场景也适合正在练习 SSM 整合项目的在校生。系统按角色划分功能一般员工与部门经理可管理个人资料、浏览上班时间公告、提交请假与出差申请、处理差费报销及日常出勤系统管理员还可管理用户、部门、经理与员工并支持员工统计和请假统计覆盖企业考勤业务的主要流程。压缩包为 zip 格式整体约 29.96MB内含可运行源码、SQL 文件与说明文档导入数据库并部署后即可查看完整的前后端交互、权限控制与业务逻辑。包内文件按代码、数据库脚本、文档等分类组织便于按需查阅和二次开发。目前已有 1847 人学习或下载适合需要完成课程设计或毕业设计、梳理 SSM 分层架构或参考考勤类管理系统做扩展的开发者。1. 这个 SSM 员工考勤管理系统解决的是中小公司考勤的黑匣子问题不少中小公司到现在还在用 Excel 记考勤月底人事挨个核对上下班时间迟到早退全靠肉眼找。这个标题里的SSM 员工考勤管理系统SSM 指 Spring SpringMVC MyBatis 三件套要解决的正是这件事把打卡、请假、补卡审批和月度统计装进一个 JSP 网页里员工自己点按钮打卡主管在后台看审批月底报表自动跑出来。它适合两类人——刚学完 SSM 想做完整项目的学生以及公司想低成本替代 Excel 考勤、又不想买商业 OA 的运维或人事。拿到这套可运行源码加 SQL 文件最直接的收益是可以少走一个月的弯路不用从零搭框架先跑起来再改业务比对着教程敲代码快得多。2. SSM 三件套在考勤系统里怎么分工从请求到数据库的一条链路SSM 不是三个独立框架而是一条固定流水线浏览器发起请求SpringMVC 负责接住并按 URL 路由到对应 ControllerController 调 Service 处理业务Service 通过 MyBatis 操作 MySQL 把结果落库最终数据再返回给 JSP 页面渲染。Spring 则是中间的大管家把 Controller、Service、Mapper 这些对象的创建和依赖关系用 IoC 容器管起来你不需要手动 new配好注解就能直接用。2.1 先看工程配置web.xml 和 spring-mvc.xml 决定了系统能不能启动跑通这个项目的第一步不是改代码而是检查配置文件。SSM 项目是 war 包形态启动入口在web.xml它要干两件事注册 Spring 的上下文监听器和 SpringMVC 的前端控制器DispatcherServlet。很多新手拿到源码后 Tomcat 一启动就 404多半是这里没配对。!-- web.xml 核心片段 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring-mybatis.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-nameDispatcherServlet/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-nameDispatcherServlet/servlet-name url-pattern//url-pattern /servlet-mapping这里ContextLoaderListener加载的是 Spring 和 MyBatis 整合的配置文件DispatcherServlet单独加载 SpringMVC 的配置两者职责分开这是 SSM 的标准做法。注意url-pattern配的是反斜杠意味着所有请求都进 SpringMVC静态资源CSS、JS、图片需要额外放行否则页面样式全丢。常见做法是在 spring-mvc.xml 里加mvc:default-servlet-handler/。2.2 SSM 常用注解对照Controller、Service、Repository 别用混考勤系统里你会在源码中反复看到一组 SSM 常用注解它们的分工很明确注解用在哪一层作用考勤系统中的例子Controller表现层标注类是 SpringMVC 控制器接收请求AttendanceControllerService业务层标注类是业务逻辑实现被 Controller 调用AttendanceService的 impl 类Repository持久层标注 Mapper 接口实现由 MyBatis 生成代理EmployeeMapperAutowired任意层按类型自动注入依赖替代手动 setter在 Controller 里注入 ServiceRequestMapping方法/类上绑定 URL 与方法RequestMapping(/checkIn)一个典型的现象是新手把Repository写成Component系统也能跑但语义错了而且Repository自带的异常翻译把 SQL 异常转成 Spring 的DataAccessException就没了。在考勤系统里打卡记录写入失败时如果走了Component异常信息会裸奔到页面看起来像 500 错误页排查半天才发现是注解用错。2.3 考勤打卡的请求链路一个 Controller 方法从按下到落库为了看清这条链路我把打卡这个核心动作拆出来。前端 JSP 页面提交员工号和打卡类型Controller 接收后交给 ServiceService 组织数据后通过 MyBatis 写入attendance表。整个过程大概是这样Controller RequestMapping(/attendance) public class AttendanceController { Autowired private AttendanceService attendanceService; RequestMapping(/check) ResponseBody public String checkIn(RequestParam(empId) Integer empId, RequestParam(type) String type) { // 调用业务层业务层内部判断是上班卡还是下班卡 boolean result attendanceService.punch(empId, type); return result ? success : duplicate; } }这里RequestParam负责把 JSP 表单里的empId和type取出来ResponseBody表示直接把返回字符串写回响应体前端 JS 拿到 success 后弹提示框。业务层的punch方法里一般会做三件事先查员工是否存在再查今天是否已经打过同类型卡防止重复打卡最后insert一条记录。这个逻辑是考勤系统的地基后面第 4 章会展开补卡和审批流程。3. 考勤系统的数据库设计把打卡记录与人资统计一次做对很多开源项目的 SQL 文件是能跑但字段设计经不起推敲。考勤系统的核心不在代码而在数据库——打卡记录一天会产生大量数据月底还要按员工和月份做聚合统计表结构没设计好后面写 SQL 会非常痛苦。3.1 三张核心表员工表、考勤记录表、补卡申请表打开 SQL 文件你至少要看到这三张表缺一张这个系统都跑不完整-- 员工表存基础信息考勤统计按部门分组时会用到 CREATE TABLE employee ( id INT NOT NULL AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL COMMENT 工号, name VARCHAR(50) NOT NULL COMMENT 姓名, department VARCHAR(50) DEFAULT 未分配 COMMENT 部门, position VARCHAR(50) DEFAULT NULL COMMENT 岗位, password VARCHAR(64) NOT NULL COMMENT 登录密码建议MD5加盐, PRIMARY KEY (id), UNIQUE KEY uk_emp_no (emp_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 考勤记录表一天两行上班和下班各一条 CREATE TABLE attendance ( id INT NOT NULL AUTO_INCREMENT, emp_id INT NOT NULL COMMENT 员工ID, work_date DATE NOT NULL COMMENT 日期, type TINYINT NOT NULL COMMENT 1上班卡, 2下班卡, punch_time DATETIME NOT NULL COMMENT 打卡时间, status TINYINT DEFAULT 0 COMMENT 0正常, 1迟到, 2早退, PRIMARY KEY (id), UNIQUE KEY uk_emp_date_type (emp_id, work_date, type), KEY idx_work_date (work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 补卡申请表员工申请、主管审批审批通过后改写考勤记录 CREATE TABLE leave_request ( id INT NOT NULL AUTO_INCREMENT, emp_id INT NOT NULL, request_date DATE NOT NULL COMMENT 申请补卡的日期, reason VARCHAR(200) DEFAULT NULL COMMENT 补卡原因, status TINYINT DEFAULT 0 COMMENT 0待审批, 1通过, 2拒绝, approver_id INT DEFAULT NULL COMMENT 审批人ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上要注意三点。第一attendance表用了联合唯一索引uk_emp_date_type这能在数据库层面挡住同一个人同一天重复打同类型卡代码防重只是第一道防线这里才是兜底。第二punch_time用DATETIME而不是TIMESTAMP因为考勤数据要长期保存TIMESTAMP的范围到 2038 年就到期了你不想系统刚上线十几年就出问题。第三业务字段如status用TINYINT存数字状态不要直接存迟到早退这种中文后面统计 SQL 用CASE WHEN转换显示这样排序和筛选都方便。3.2 月度考勤统计一条 SQL 把迟到早退算清楚这套系统里最有含金量的 SQL 是月末统计。HR 要的报表不是打卡明细而是每个人的出勤天数、迟到次数、早退次数。常见做法是配合一张日历表或者用 MySQL 的日期函数生成当月日期序列然后和打卡记录左连接-- 按月统计员工的迟到早退次数 SELECT e.emp_no, e.name, COUNT(DISTINCT a.work_date) AS total_days, SUM(CASE WHEN a.type 1 AND TIME(a.punch_time) 09:00:00 THEN 1 ELSE 0 END) AS late_count, SUM(CASE WHEN a.type 2 AND TIME(a.punch_time) 18:00:00 THEN 1 ELSE 0 END) AS early_leave_count FROM employee e LEFT JOIN attendance a ON e.id a.emp_id AND DATE_FORMAT(a.work_date, %Y-%m) 2025-06 GROUP BY e.emp_no, e.name ORDER BY e.emp_no;这段 SQL 里COUNT(DISTINCT a.work_date)是关键——如果一个人一天打了好几次卡补卡、重打不加 DISTINCT 会把出勤天数算重复这就是标题热词里那个sql语句去重在考勤系统里的实际场景。TIME()函数把DATETIME截成时间部分跟上下班阈值比较就能标出迟到和早退。慢 SQL 优化的点也在这里LEFT JOIN后GROUP BY大量数据时work_date上的索引前面 DDL 里那个idx_work_date能大幅减少回表扫描否则 200 人跑一个月数据就得等好几秒。3.3 员工排班的需求边界这套基础 SQL 不支持复杂排班要提醒你的是这套代码里如果只看到employee、attendance、leave_request三张表那它就是固定上下班时间比如朝九晚六的考勤。不支持轮班、调休、跨天班晚上 22 点上班第二天 6 点下班。如果你公司有排班需求需要在员工表加一个shift_id字段再建一张排班表attendance表也要改成按排班规则判断迟到早退而不是写死 9 点和 18 点。源码的 SQL 文件通常只满足最通用的场景拿到手先确认这一点免得项目上线才发现做不了夜班考勤。4. 从登录到补卡审批JSP 展示层把考勤闭环串起来的关键代码SSM 的后端把数据和逻辑准备好了JSP 负责把流程走通。考勤系统完整的用户旅程是员工登录 → 查看今天打卡状态 → 点击上班/下班打卡 → 如果忘打卡就提交补卡申请 → 主管登录看到待审批列表 → 通过或拒绝 → 员工看到审批结果。这一章逐个环节落代码。4.1 登录与个人信息展示Session 里存员工对象登录是每个 JSP 系统的门面。SSM 里的登录 Controller 一般会把查询到的员工对象放进HttpSession所有页面通过 Session 判断是否登录。代码写法很固定RequestMapping(/login) public String login(String empNo, String password, HttpSession session, Model model) { Employee emp employeeService.login(empNo, password); if (emp ! null) { session.setAttribute(loginUser, emp); return redirect:/attendance/index; } else { model.addAttribute(errorMsg, 工号或密码错误); return login; } }登录成功后跳转的index.jsp就是员工首页它用 EL 表达式把 Session 里的员工对象渲染到页面上。很多 JSP 项目里jsp个人信息展示页面指的就是这个——把session.getAttribute(loginUser)换成${loginUser.name}、${loginUser.department}这种 EL 写法简洁且没有 scriptlet 的 Java 代码污染。注意 JSP 页面顶部要加% page contentTypetext/html;charsetUTF-8 languagejava %否则中文姓名会乱码这个坑后面章会专门讲。4.2 打卡按钮与 AJAX 刷新不用整页刷新只送一个请求考勤打卡最痛快的体验是点一下按钮页面不刷新显示打卡成功。前端用 jQuery 的$.post向后端送 AJAX 请求后端返回结果后局部更新状态。核心代码如下% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % script src${pageContext.request.contextPath}/static/js/jquery.min.js/script script function punch(type) { $.post(${pageContext.request.contextPath}/attendance/check, {empId: ${loginUser.id}, type: type}, function(result) { if (result success) { $(#msg).text(打卡成功时间 new Date().toLocaleTimeString()); } else if (result duplicate) { $(#msg).text(今天已经打过这个类型的卡了); } else { $(#msg).text(打卡失败请联系管理员); } }); } /script button onclickpunch(1)上班打卡/button button onclickpunch(2)下班打卡/button span idmsg/span这段代码里最容易忽略的是${pageContext.request.contextPath}它取的是项目部署路径。如果你把项目打成 war 包丢进 Tomcat 的 webapps 下URL 前缀是http://localhost:8080/attendance-system/不用这个变量的话换路径部署就得改一堆硬编码地址用绝对路径是血泪经验换来的习惯。4.3 补卡申请与审批流状态机字段撑起最简单的审批补卡是这个系统里最有业务感的功能。员工提交申请时带上补卡日期和原因主管看到的是待审批列表审批通过后系统要自动把那天的考勤状态改掉。这个流程不需要工作流引擎一张表的status字段就够了!-- 员工提交补卡申请的 JSP 表单 -- form action${pageContext.request.contextPath}/leave/add methodpost input typehidden nameempId value${loginUser.id} / 补卡日期input typedate namerequestDate required / 补卡原因input typetext namereason maxlength200 / button typesubmit提交申请/button /form !-- 主管审批列表只显示 status0 的记录 -- table c:forEach items${pendingList} varreq tr td${req.empName}/td td${req.requestDate}/td td${req.reason}/td td a href${pageContext.request.contextPath}/leave/approve?id${req.id}result1通过/a a href${pageContext.request.contextPath}/leave/approve?id${req.id}result2拒绝/a /td /tr /c:forEach /tableJSP 页面直接用c:forEach遍历 List这就是 JSTL 存在的意义。审批的 Controller 里接收id和result更新leave_request的状态如果通过还需要回写attendance表——把那天的记录置为正常或插入一条补卡记录。这个审批通过后回写的动作要放在同一个事务里否则会出现审批显示通过但考勤数据没变的尴尬后面避坑章节会重点提。5. 部署与联调避坑SSM MySQL JSP 最常见的 5 个翻车现场这套东西跑不起来的原因十个里有八个不在业务代码而在环境、编码和配置。以下是我见过最多的情况按现象 → 原因 → 解决写清楚。5.1 数据库连接密码含特殊字符导致启动失败现象Tomcat 启动时报Access denied for user rootlocalhost但 Navicat 用同样的密码连得好好的。原因jdbc.properties 里如果密码是Abc123符号在 properties 文件里会被解析器吞掉或截断导致实际传给 MySQL 的密码不完整。解决把特殊字符转义或者改成用urldecode方式。最简单的是换一个不含特殊字符的数据库密码或者用 IDEA 的 properties 编辑器自动转义。常见做法是写成passwordAbc\123但不同 Spring 版本解析行为有微调最稳妥还是避免特殊字符。5.2 MySQL 8.0 驱动类与 SSL 连接报错现象项目用的com.mysql.jdbc.Driver连接串里有useSSLtrue启动后控制台刷 warning个别环境直接报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因MySQL 8.0 之后的驱动类改名为com.mysql.cj.jdbc.Driver旧驱动虽然能连但时区参数必须显式指定。中文乱码的时区信息其实是 MySQL 返回的 CST 被错误解码本质是连接串缺少serverTimezone。解决jdbc.properties 里两行改掉jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/attendance?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse注意useSSLfalse不是安全妥协本地开发环境本来就不需要加密连接这样还能省掉证书配置的麻烦。5.3 JSP 页面中文乱码过滤器没配或配错顺序现象页面显示ä¸åæ这种乱码SQL 写入的中文也是乱码但数据库字段明明是 utf8mb4。原因JSP 页面编码、请求编码、响应编码三层只要有一层不对就乱。Spring 提供了CharacterEncodingFilter但很多项目要么没注册要么 filter 的 mapping 顺序排在 SpringMVC 的后面导致请求参数还没被解码就进了 Controller。解决在 web.xml 里保证这个过滤器是第一个被加载的filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingforceEncodingtrue表示不管请求和响应原来是什么编码强制用 UTF-8这是解决乱码的兜底方案。5.4 MyBatis 的 Mapper XML 没被打进 classes 目录现象项目源码里EmployeeMapper.xml明明写得没问题启动时却报Invalid bound statement (not found): com.xxx.EmployeeMapper.findByEmpNo。原因Maven 项目默认只把src/main/resources下的文件打包如果把 Mapper XML 放在了src/main/java和接口同目录编译后不会自动复制到 classesMyBatis 就找不到对应的 SQL 映射。解决在 pom.xml 里加资源配置或者把 XML 移到 resources 目录。推荐后一种目录结构更整洁build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build5.5 打卡记录重复是 SQL 层防住了但代码报错没事务现象员工连续点了两次打卡按钮页面显示打卡成功数据库里多了两条记录。原因虽然attendance表有联合唯一索引但 Controller 里插入前先查后插的两步操作没有加事务。第一次点击后事务未提交第二次点击的查询读不到未提交的数据于是又执行了 insert最终一条插入撞上唯一索引报错一条则可能因为连接被占用返回超时。用户看到的就是要么重复要么报错。解决给 Service 层的punch方法加Transactional同时前端在打卡成功后禁用按钮 2 秒双层防护。这也是考勤系统里最常见的并发场景——你永远要假设用户会连点。这个案例的价值在于让你理解数据库唯一约束是最后一道闸代码事务才是日常防线。6. 把能跑的源码改成能用的系统三个必做的进阶改造源码跑通只是起点距离真正给 HR 用还差三步。以我自己的教训来说第一次部署这套考勤系统时以为跑通就完事了结果人事用了一天就提了三个需求打卡要能看到自己这个月迟到几次、月底要能导出 Excel、员工忘了打卡要能微信通知主管。所以如果你要把它落地我建议优先做这三件事。第一Excel 导出月末报表。人事的刚需不是看网页而是把统计结果导出来放进工资表。常见做法是在统计查询的 Service 层加上 Apache POI 依赖把ListMapString, Object逐行写入HSSFWorkbook设置响应头Content-Disposition: attachment;filenameattendance_202506.xlsController 返回void直接写输出流。这个功能加一个下午就能完成但价值比花一周调样式高得多。第二数值型状态改成可读文案。数据库里status存 0、1、2JSP 页面显示时用${req.status 0 ? 待审批 : (req.status 1 ? 通过 : 拒绝)}这种三元表达式但更好的做法是在 Service 层就把状态翻译成文案封装成 VO。最近主流的做法是返回 JSON 给前端渲染但 JSP 场景下直接在 SQL 里用CASE WHEN也能解决关键别让 0/1/2 裸奔到页面上。第三一个实用验证技巧上线前造一批模拟打卡数据跑一遍月底统计核对迟到次数和手算结果是否一致。这个步骤能一次性发现时区问题、跨月边界问题、重复数据问题。我建议你写个小脚本往attendance表插 100 条随机时间的数据然后验证统计 SQL 的输出——这比上线后被 HR 拿着 Excel 来对账要舒服得多。考勤系统看似简单但它教会我最重要的一件事不要把技术难点当核心把考勤规则理清楚才算真懂。这套 SSM 源码的价值在它把框架、数据库、前端串成了一个完整闭环你改业务时能同时看到三层代码如何协作。如果你拿到手的第一天就能跑起来那第二天就值得把迟到判断的代码找出来读一遍——读懂它你就知道这套系统能怎么改成适合你公司规则的样子。希望帮到你。本文还有配套的精品资源点击获取
返回列表