简介面向中小型团队与高校实训场景的 Java 考勤管理系统完整源码包适合具备 Java Web 基础、希望掌握 Spring 风格业务开发或毕业设计复用的学习者。包内按业务模块划分覆盖员工管理、考勤打卡、请假/会议申请、固定资产与任务调度等常见功能压缩包共 2000 个文件约 55MB以 1336 个 Markdown 文档、500 个 JavaScript 文件、67 个 Java 文件及 JSON/XML 配置为主文档与配置占比高便于对照阅读、按需检索和二次开发。已有 170 人学习下载。其中包含控制器、服务、实体与工具类等完整分层主线功能覆盖考勤申请、会议与员工管理。借助该源码可快速理解考勤业务表设计、接口分层、审批流转与前后端交互方式尤其适合课程设计、项目实训或企业内部门禁考勤需求的原型搭建Java、XML、SQL 等关键文件可直接导入 IDE 运行调试Markdown 文档辅助梳理实现思路。1. 考勤管理系统源码学生项目里最容易被低估的 Java 练手资源做 Java 课程设计或者毕业设计的人十有八九会撞上考勤系统这个题目。这份源码就是一个典型的 Java Web 考勤管理系统基于 JSP Servlet MySQL 实现包含员工管理、部门管理、上下班打卡、考勤统计和月度报表导出几个模块。它不像商城项目那么绕但足够把 Java Web 开发里最核心的东西串一遍数据库设计、会话管理、请求转发、SQL 聚合统计。适合两类人一类是急着交课程设计的学生拿回去改改就能跑另一类是刚学完 Java 基础、想找一个完整项目练手的人拆开源码看一遍比看十篇教程都管用。2. 技术栈与模块拆解JSP Servlet MySQL 这条老链路为什么还在用2.1 项目形态与选型理由先说项目形态。这份源码是典型的 Java Web 三层结构JSP 负责页面展示Servlet 处理请求转发JDBC 访问 MySQL 数据库。没有引入 Spring Boot也没有用 MyBatis整条链路全是原生 API依赖只有 Tomcat 和 MySQL 驱动两个 jar 包。很多读者拿到手第一反应是这太老了。我一般会劝一句老链路恰恰是学习成本最低的链路。Spring Boot 帮你自动配置了太多东西出了问题反而不容易定位而 JSP Servlet 的每一行代码都是显式的请求怎么进来、怎么查库、怎么把结果塞进 request 再转发到页面全程没有黑匣子。选型理由就一条这份资源的定位是能跑、能改、能看懂不是生产级高并发。用原生 Servlet 的好处是依赖少只要 Tomcat 起来了就能跑不用跟 Maven 仓库里的版本冲突较劲。常见做法是登录校验写在 Filter 里业务逻辑写在 Servlet 的 doGet/doPost 里数据库访问单独抽出 DBUtil 工具类。后面章节的代码走读会按这个结构展开。2.2 数据库设计三张核心表的字段与关联考勤系统的数据库设计一般围绕三张表展开员工表、部门表、考勤记录表。员工表挂在部门表下面考勤记录表引用员工表的主键。这个资源里常见的建表 SQL 大概是这样的CREATE DATABASE attendance DEFAULT CHARACTER SET utf8mb4; USE attendance; CREATE TABLE dept ( id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL ); CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT, dept_id INT NOT NULL, emp_no VARCHAR(20) NOT NULL UNIQUE, emp_name VARCHAR(30) NOT NULL, password VARCHAR(32) NOT NULL, role TINYINT DEFAULT 1 COMMENT 1-普通员工 0-管理员, FOREIGN KEY (dept_id) REFERENCES dept(id) ); CREATE TABLE attendance_record ( id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL, work_date DATE NOT NULL, check_in TIME, check_out TIME, status VARCHAR(10) DEFAULT 正常, UNIQUE KEY uk_emp_date (emp_id, work_date), FOREIGN KEY (emp_id) REFERENCES employee(id) );这三张表就是整个系统的地基。注意两个设计细节一是 attendance_record 里加了唯一索引 (emp_id, work_date)目的是防止同一个人同一天插入两条打卡记录二是 status 字段做了冗余存储后续统计直接按状态分组不用每次用上班和下班时间现算。如果把这三张表画成关系图就是典型的部门 1 对多 员工、员工 1 对多 考勤记录。部门表是独立的员工表通过 dept_id 关联部门考勤记录表通过 emp_id 关联员工。查询员工考勤的时候只需要一次 JOIN 就能把员工姓名和部门名称带上这也是后面统计 SQL 能写短的前提。初始化数据也是常见的一个坑。很多版本自带的初始化脚本只建表不插数据登录页没有默认账号。建议手动插入一条管理员数据工号 admin、密码 admin、role 设为 0否则第一次登录会卡在账号不存在。2.3 角色权限管理员与普通员工的两套菜单逻辑权限这块原项目没有引入权限框架就是用最简单的方式employee 表里 role 字段区分管理员和普通员工登录成功之后把角色值存进 Session。管理员能访问员工管理、部门管理、考勤统计这些路径普通员工只能看到自己的打卡入口和个人考勤记录。实现上常见做法是写一个权限拦截的 Filter在请求到达 Servlet 之前先检查 Session 里的角色// AuthFilter.java 权限拦截核心逻辑 public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(false); // 未登录一律踢回登录页 if (session null || session.getAttribute(user) null) { resp.sendRedirect(request.getContextPath() /login.jsp); return; } // 已登录但角色不匹配提示无权访问 String role (String) session.getAttribute(role); String uri request.getRequestURI(); if (uri.contains(/admin/) !0.equals(role)) { request.setAttribute(msg, 没有管理员权限); request.getRequestDispatcher(/error.jsp).forward(req, resp); return; } chain.doFilter(req, resp); }这段 Filter 的逻辑说明第一层判断只解决有没有登录第二层判断解决登录了但能不能进管理页面。如果把这两层合并成一个 if就会出现普通员工直接输入管理页面地址也能进去的漏洞。参数说明Session 里存的是 user 对象和 role 字符串role 用 String 而不是 int是为了和页面 JSTL 标签里的比较写法对齐。改成 int 也行但所有比较的地方都要同步改很容易漏。另一个容易踩的点是 request.getSession(false) 里面的 false它表示拿不到 Session 时返回 null 而不是新建一个配合后面的 null 判断正好覆盖未登录场景如果误写成 getSession(true)匿名用户也会被强制创建 Session拦截逻辑就废了。3. 本地跑起来JDK、MySQL、Tomcat 三件套的配置顺序3.1 环境版本怎么选先把环境定下来再谈导入。这类老项目对版本敏感版本不匹配的翻车现场我见过太多给你一个不会踩雷的组合JDK 8不要拿 JDK 11 或更高跑老项目JSP 编译和某些 JDBC 驱动在 JDK 9 之后行为有变化MySQL 5.7 或 8.08.0 需要改驱动类名和时区参数细节见 3.3Tomcat 8.5兼容性最好Tomcat 9 对 JSP 的 EL 表达式支持有轻微差异IDE 用 Eclipse 或 IntelliJ IDEA 都行关键是确认项目是什么工程结构顺序上先装 JDK再装 MySQL最后解压 Tomcat。装完 Tomcat 先别急着往里面塞项目先启动一次确认 8080 端口能正常访问。这里有一个新手常犯的错误以为 Tomcat 一定要单独装。其实用 IDE 内置的 Tomcat 插件跑更方便控制台日志和断点调试都更顺手。版本选型的原则就一句话和项目里 lib 目录下 jar 包的编译版本对齐。你可以在项目里找到 mysql-connector-java 的 jar看它支持的最低 JDK 版本再决定装哪个 JDK。JDK 和 Tomcat 都是用前先查版本很多诡异报错都是版本不匹配引发。3.2 导入工程与初始化数据库拿到 zip 解压之后常见做法是把整个文件夹拷到 Tomcat 的 webapps 目录下直接运行但我不推荐这么干。更好的方式是导入 IDE用 IDE 内置的 Tomcat 插件跑能看到控制台日志和断点。初始化数据库的步骤固定三步# 第一步登录 MySQL mysql -uroot -p # 第二步执行建库建表脚本脚本文件一般在 db 或 sql 目录下 source /your/path/attendance.sql; # 第三步验证表是否创建成功 USE attendance; SHOW TABLES;执行完这三步数据库侧就绪。注意 source 命令后面要写文件的绝对路径别用相对路径否则会报 file not found。导入 IDE 的时候有一个常见的坑项目如果是 Eclipse 的 Dynamic Web Project 结构直接 File - Open 打开目录即可如果拿到手是一个普通文件夹没有 .project 文件要先转成 Maven 工程或者手动勾选 Web Facet否则 IDE 不认它是 Web 项目运行按钮是灰的。我一般会先看一眼工程根目录下有没有 pom.xml。有 pom.xml 就用 Maven 方式导入没有就用纯 Web 工程方式导入。两种方式对应的启动按钮不一样混淆了会在后面部署阶段报各种奇怪错误。3.3 数据源配置jdbc.properties 逐项说明老项目的数据源配置通常写在 src 目录下的 jdbc.properties 或 db.properties 里。打开之后你会看到四五行配置每一行都别乱改jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/attendance?useUnicodetruecharacterEncodingutf8 jdbc.usernameroot jdbc.password123456逐项说明jdbc.driverMySQL 5.7 用 com.mysql.jdbc.DriverMySQL 8.0 必须改成 com.mysql.cj.jdbc.Driverjdbc.urllocalhost 换成远程数据库 IP 就是连远程库characterEncodingutf8 是保中文不乱码的关键参数jdbc.username / jdbc.password按本地 MySQL 的实际账号改8.0 如果因为密码加密方式导致连接失败要在 url 后面追加 serverTimezoneAsia/Shanghai改完配置之后重启 Tomcat 让配置重新加载。验证配置是否生效的方式很简单随便打开一个需要查库的页面如果页面能正常显示数据列表说明数据源通了如果报 SQLException 或 Connection refused看 5.1 的排查思路。还有一个细节jdbc.password 明文写在 properties 文件里是课程设计项目的常态别因为这个觉得项目有问题。生产环境用加密或环境变量那是另一个话题。但要注意 properties 文件保存时编码必须是 ASCII 或 UTF-8 without BOM否则中文注释会变成乱码甚至导致解析失败。3.4 第一个冒烟测试从登录页到主界面数据库初始化完、Tomcat 启动成功之后先做一个冒烟测试打开浏览器访问 http://localhost:8080/项目名/login.jsp用默认账号登录看能不能正常跳转到主界面。如果项目名带了中文或者空格URL 里的路径会出问题建议把 webapps 下的项目文件夹重命名成纯英文比如 attendance。冒烟测试通过之后再逐个点一遍菜单员工管理、部门管理、打卡、统计。这一步主要是确认每个 Servlet 的路由映射有没有配全。很多老项目的 web.xml 里配置了一堆 servlet-mapping少配一条就会在点某个菜单时 404。我用一个笨办法快速确认路由把 web.xml 打开把每个 url-pattern 和对应 Servlet 类名抄在一张纸上然后逐个访问。抄一遍下来整个项目的请求流转关系就清楚了比反复点页面快得多。提示如果冒烟测试阶段就报 404先别怀疑代码优先检查访问路径的大小写。JSP 文件名和 Servlet 映射都是区分大小写的login.jsp 写成 Login.jsp 直接 404。4. 核心代码走读登录、打卡与统计 SQL 的三个落点4.1 登录逻辑Session 与密码校验登录是第一个值得逐行看的模块。常见写法是 LoginServlet 接收页面提交的 emp_no 和 password拿着这两个参数去查 employee 表查到就把用户对象放进 Session。// LoginServlet.java 核心校验逻辑 String empNo request.getParameter(empNo); String pwd request.getParameter(password); // 查询用户注意这里要用 PreparedStatement 而不是拼接 SQL String sql SELECT * FROM employee WHERE emp_no? AND password?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, empNo); ps.setString(2, pwd); ResultSet rs ps.executeQuery(); if (rs.next()) { // 登录成功user 对象和 role 同时入 Session request.getSession().setAttribute(user, empNo); request.getSession().setAttribute(role, rs.getString(role)); resp.sendRedirect(request.getContextPath() /index.jsp); } else { // 登录失败回到登录页并携带错误提示 request.setAttribute(msg, 工号或密码错误); request.getRequestDispatcher(/login.jsp).forward(req, resp); }逻辑说明整个登录成败就取决于 rs.next() 是否为 true。这里有两个容易忽略的细节。第一个细节是请求转发和重定向的区别。登录失败用 forward 回到 login.jsprequest 里的 msg 属性还能带上登录页可以显示工号或密码错误登录成功用 sendRedirect 跳到 index.jsp让浏览器地址栏变成 index.jsp这样刷新页面不会重复提交表单。如果把两者搞反会出现刷新一次就重复登录一次的怪现象。第二个细节是密码校验。PreparedStatement 的 ? 占位符天然防 SQL 注入老项目里如果看到字符串拼接的查询语句建议顺手改掉。employee 表里 password 字段存的是明文课程设计阶段不用纠结生产环境必须做加盐哈希处理。参数说明empNo 和 password 的取值来自 request.getParameter这两个参数名要和 login.jsp 表单里的 name 属性完全一致差一个字母就取到 null。老项目里最常见的登录失败原因不是密码错而是参数名拼写不一致。4.2 上下班打卡时间判定与去重打卡模块是考勤系统的灵魂。常见做法是同一个 PunchServlet 处理上班和下班两个动作通过页面按钮区分。时间判定逻辑是这样的// PunchServlet.java 打卡时间判定 Timestamp now new Timestamp(System.currentTimeMillis()); java.sql.Date today new java.sql.Date(now.getTime()); // 先查今天有没有打卡记录 String querySql SELECT check_in, check_out FROM attendance_record WHERE emp_id? AND work_date?; PreparedStatement ps conn.prepareStatement(querySql); ps.setInt(1, empId); ps.setDate(2, today); ResultSet rs ps.executeQuery(); if (!rs.next()) { // 今天第一次打卡记为上班时间 insertSql INSERT INTO attendance_record(emp_id, work_date, check_in) VALUES(?,?,?); } else if (rs.getTime(check_in) ! null rs.getTime(check_out) null) { // 已有上班时间但没有下班时间补下班时间 updateSql UPDATE attendance_record SET check_out? WHERE emp_id? AND work_date?; } else { // 上下班都打过了提示重复打卡 }逻辑说明这个分支结构把一天内的打卡行为拆成三种状态——没打过、打了上班没打下班、都打过了。注意 check_out 为 NULL 的判断用 rs.getTime(check_out) null而不是 rs.getString(...).equals()因为数据库 TIME 字段为空时是 NULL 而不是空字符串。这是新人最容易写错的地方写错了会导致已打过的下班卡被重复覆盖。打卡去重靠的是 2.2 里那张表的唯一索引 uk_emp_date。即使代码逻辑漏了判断数据库层也会拦住重复插入并抛 DuplicateKeyException。这个设计就是典型的前后端双保险代码判断先拦一次数据库唯一索引兜底。如果资源里的代码没有这个唯一索引建议自己加上。参数说明System.currentTimeMillis() 取的是服务器时间。如果你的应用部署在云服务器上而用户在北京时区不一致会导致打卡时间差 8 小时解决办法见 5.3。另外 empId 这个变量来自登录 Session 里的用户信息不要从前端传过来否则任何用户都能伪造别人的打卡记录。4.3 月度统计一条 SQL 出报表考勤统计页面是管理员最常用的模块。常见做法是选一个月度然后按员工分组统计迟到、早退、正常天数SELECT e.emp_no AS 工号, e.emp_name AS 姓名, COUNT(*) AS 出勤天数, SUM(CASE WHEN r.check_in 09:00:00 THEN 1 ELSE 0 END) AS 迟到天数, SUM(CASE WHEN r.check_out 18:00:00 THEN 1 ELSE 0 END) AS 早退天数 FROM attendance_record r JOIN employee e ON r.emp_id e.id WHERE DATE_FORMAT(r.work_date, %Y-%m) ? GROUP BY e.id, e.emp_no, e.emp_name;这个 SQL 的两个要点一是 DATE_FORMAT 把日期字段格式化成 YYYY-MM 和传入参数做匹配注意如果 work_date 列有索引DATE_FORMAT 会导致索引失效数据量大了之后建议改成范围查询 BETWEEN 2024-01-01 AND 2024-01-31二是 SUM(CASE WHEN ...) 这种写法把迟到和早退天数一次算完避免多次扫表。参数说明迟到阈值 09:00:00 和早退阈值 18:00:00 是写死的不同公司上下班时间不同改这两个字符串即可但格式必须是 HH:mm:ss少一位冒号都会导致比较失效。分组这里有个细节GROUP BY 后面写的是 e.id, e.emp_no, e.emp_name 三个字段。理论上按 e.id 分组就够了但因为 SELECT 里查了 emp_no 和 emp_nameMySQL 在 ONLY_FULL_GROUP_BY 模式下要求这些字段都出现在 GROUP BY 里否则直接报错。很多新手把 GROUP BY 缩写成只按 e.id 分组然后在别人的机器上能跑、自己机器上报错就是 MySQL 的 sql_mode 配置不同导致的。统计结果在页面上的展示常见做法是循环 ResultSet 拼 HTML 表格或者用 JSTL 标签遍历 List。如果资源里用的是后者你会发现代码量明显更少这也是 JSP 页面里比 scriptlet 更推荐的写法。5. 避坑排查数据库连不上、乱码、时间错位的五个现场老项目跑不起来九成问题出在环境配置而不是代码本身。我把这类考勤系统最常见的五个报错现场整理出来每个都按现象、原因、解决给出处理步骤照着排查比瞎试快得多。5.1 现象一启动 Tomcat 报 ClassNotFoundException现象第一次启动 Tomcat 或访问任意页面时控制台报 java.lang.ClassNotFoundException: com.mysql.jdbc.Driver紧接着是一大串 Caused by最后页面显示 500。报错位置通常定位在 DBUtil 工具类的静态代码块里 Class.forName(driver) 那一行。原因MySQL 驱动 jar 没有放进 WebContent/WEB-INF/lib 目录。IDE 的 build path 里加了 jar编译能通过但 Tomcat 运行时加载的是 WEB-INF/lib 下的 jar两边不一致就会出现编译过、运行炸的迷惑现象。解决分三步走。第一步确认 lib 目录下有没有 mysql-connector-java 的 jar没有就下载对应版本放进去第二步右键项目 Clean让 IDE 重新打包部署第三步检查 Project Structure 里的 Artifacts 配置确认 lib 目录被标记为 jar 目录。多数情况做完第一步就好后两步是防止 IDE 缓存捣乱。5.2 现象二页面中文全部变成问号现象登录页面正常但员工姓名、部门名称等中文数据显示成 ????或者向数据库插入中文后查出来是乱码。有些是页面显示乱码但库里正常有些是页面正常但库里是乱码两种情况原因不同。原因三个环节至少有一个字符集没对齐——数据库表字符集、JDBC 连接串、JSP 页面编码。页面乱码而库正常问题出在 JSP 编码或 response 编码库乱码则基本是 JDBC 连接串缺了 characterEncoding。解决按顺序检查先把 jdbc.url 加上 characterEncodingutf8再确认建表时用了 utf8mb4最后在 JSP 页面顶部写 % page contentTypetext/html; charsetUTF-8 %。三个地方统一成 UTF-8 之后重启乱码问题基本消失。还有一个冷门原因properties 文件保存时被 IDE 转成了 GBK 编码读出来的连接串本身就带乱码这种情况把文件重新保存为 UTF-8 即可。5.3 现象三打卡时间比本地时间慢 8 小时现象用户上午 9 点打卡数据库里 check_in 显示 01:00:00整个系统的打卡时间全部偏移 8 小时。原因服务器或 MySQL 的时区设置是 UTC而用户在东八区。MySQL 8.0 的驱动默认读取 serverTimezone 参数不指定就按 JVM 默认时区算两边不一致就出现 8 小时偏移。解决在 jdbc.url 后面追加 serverTimezoneAsia/Shanghai同时检查操作系统时区。如果项目里取时间用的是 new Date()取到的是 JVM 时区的时间还要看 Tomcat 启动的 JVM 参数有没有 -Duser.timezoneAsia/Shanghai。改完配置必须重启 Tomcat光改配置文件不重启等于白改。5.4 现象四导出报表 Excel 打开是乱码现象管理员导出月度考勤报表用 Excel 打开之后姓名列全是乱码或者干脆打不开提示格式错误。原因很多老项目导出 Excel 用的是往 response 里直接写 HTML 表格的伪 Excel 方案文件后缀是 .xls 但内容其实是 HTML。如果 response 没设置正确编码浏览器按默认编码解析中文就乱码。解决在导出 Servlet 里显式设置 response.setCharacterEncoding(UTF-8)并且给 Content-Type 加 charset。更稳妥的方案是把伪 Excel 换成真正的 Excel 导出组件但课程设计阶段先调编码就够了。注意导出的文件名如果是中文要对文件名做 URL 编码处理否则浏览器端显示文件名乱码。5.5 现象五8080 端口被占用导致启动失败现象启动 Tomcat 时控制台报 Port 8080 required by Tomcat at localhost is already in useIDE 弹窗提示端口冲突项目根本起不来。原因要么之前启动的 Tomcat 实例没关闭干净要么其他程序占用了 8080 端口。常见的是前一次异常退出IDE 里的 Tomcat 进程还挂在后台。解决先关掉 IDE 里所有 Tomcat 实例再在命令行执行 netstat -ano | findstr 8080 查占用进程的 PID然后 taskkill /PID 对应PID /F。如果端口是被别的服务固定占用也可以直接改 Tomcat 的 server.xml 里的 Connector port把 8080 改成 8081但要记得访问 URL 里的端口号同步改掉。提示排查顺序建议固定为端口、驱动、字符集、时区。这个顺序覆盖了考勤系统九成以上的启动和运行报错每次照着走一遍比东查一下西查一下省时间。6. 二次开发方向把考勤系统改造成 Spring Boot 的迁移路线6.1 从 Servlet 到 Controller先跑通再改造如果你不想停留在能跑的阶段最值得做的二次开发是把它迁移到 Spring Boot。迁移的思路不是重写而是逐层替换。先建一个 Spring Boot 工程引入 web 和 mybatis 两个 starter然后把 LoginServlet 里的逻辑原样搬进 ControllerPostMapping(/login) public String login(String empNo, String password, HttpSession session) { Employee emp employeeMapper.findByEmpNoAndPassword(empNo, password); if (emp ! null) { session.setAttribute(user, emp.getEmpNo()); session.setAttribute(role, emp.getRole()); return redirect:/index; } return redirect:/login?msgerror; }这里的 employeeMapper 是 MyBatis 的 Mapper 接口SQL 用注解直接写在方法上。迁移时注意两点一是 HttpSession 的用法不变Spring MVC 可以直接注入 HttpSession二是原来 Filter 里的权限判断改造成 Spring 的 HandlerInterceptor逻辑几乎一模一样。6.2 用接口清单验证迁移结果迁移完别急着收工。我的习惯是列一份接口清单登录、打卡、统计、导出逐个对比迁移前后的输出。从那次以后我每次拿到这类老项目都先花十分钟把数据库脚本和配置参数过一遍再启动项目。先跑通、再读码、后改造这个顺序帮我省了大量查错时间。希望帮到你。本文还有配套的精品资源点击获取