☰
基于JSP的实验室考勤管理系统设计与实现全解析
2026/10/9 3:15:58 网站建设 项目流程

简介:这是一份基于JSP的实验室考勤管理系统完整项目资源,主要面向计算机相关专业正在完成期末大作业或毕业设计的学生,也适合需要Web项目实战练习的学习者。项目采用JSP+Servlet+JDBC技术栈,结合MySQL数据库实现用户身份验证、考勤记录管理、数据查询与统计报表等功能,并应用MVC设计模式保证代码结构清晰。资源包共1385个文件,容量约17MB,包含107个jsp页面、105个java源码、397个js脚本及161个css样式等前端资源,同时附带2个sql数据库脚本、论文文档及开发文档,从需求分析到功能实现均有完整记录。其中源码均经过本地编译调试,可直接运行,数据文档提供实验数据与测试用例,便于学习者快速上手。目前已有27人学习使用,适合作为毕业设计参考或课程实践的学习范本。

1. 从一次课程设计答辩说起:JSP 考勤系统为什么到现在还有人做

如果你接过“基于 JSP 的实验室考勤管理系统网页的设计与实现”这类题目,大概率是两种情况:要么是学校课程设计、毕业设计,要么是公司内部需要一个轻量的实验室/机房考勤工具。别小看这个题目——JSP 虽然被 Spring Boot 压得没什么存在感,但它 Servlet + JSP 的老三样架构在高校教学里依然是“必考项目”,因为它的请求-响应模型足够直白,能把 Session、Cookie、JDBC、MVC 分层这些基本功一次讲透。

这个项目要解决的实际问题很具体:实验室管理员每天要登记谁来了、谁走了、哪个时间段实验室最挤、谁长期不来。用 Excel 不是不行,但查一个人的历史记录、统计到课率、导出月度报表,Excel 能把你折腾到怀疑人生。而我接下来要讲的这套 JSP 实现方案,是一个跑得通、能答辩、能交差,甚至能直接改成公司内部用的版本。它不依赖微服务、不依赖 Redis,一个 Tomcat、一个 MySQL,部署起来半小时内能搞定。

这套方案适合谁读:正在做 JSP 课程设计的学生,以及想快速搭一个轻量考勤后台的初级工程师。我会把项目结构、核心代码、常见坑、部署细节全拆开讲,保证你照着敲能跑。下面我们从架构选型开始。

2. 先想清楚架构再动手:JSP 考勤系统的分层设计与数据库建模

2.1 为什么选 JSP + Servlet + JDBC,而不是直接上 Spring Boot

很多人在选题时纠结:都 2024 年了,学校为什么还让用 JSP?答案很现实——教学大纲没更新,以及 JSP 能让你看到 Web 底层的真实模样。Spring Boot 帮你封装了所有东西,你写完 Controller 不知道请求怎么来的;而 JSP 里一个request.getParameter()就能让你感知到 HTTP 协议的存在感。从就业角度讲,会 JSP 不代表你落后,反而说明你懂老系统的维护——很多国企、银行的老项目还在跑 JSP,能看懂它是一项实打实的吃饭技能。

架构上我推荐最经典的三层:JSP(视图层)→ Servlet(控制层)→ DAO(数据访问层)。不要用 JSP 里直接写 JDBC 的“一锅端”写法,虽然那确实能跑,但答辩时老师一问“你的 MVC 在哪”你就哑火了。分层的好处是:改数据库连接不用动页面、加一个查询功能不用复制粘贴一大段代码、出 bug 时能快速定位是 SQL 的问题还是页面渲染的问题。

目录结构我建议这样建:

src/main/java ├── com.lab.entity // 实体类:Student、AttendanceRecord ├── com.lab.dao // 数据访问:StudentDao、AttendanceDao ├── com.lab.service // 业务逻辑:AttendanceService ├── com.lab.servlet // 控制器:LoginServlet、CheckInServlet └── com.lab.util // 工具类:DBUtil、StringUtil src/main/webapp ├── index.jsp // 登录页 ├── student/ │ ├── list.jsp // 学生列表 │ ├── add.jsp // 添加学生 │ └── edit.jsp // 编辑学生 ├── attendance/ │ ├── checkin.jsp // 签到页 │ └── statistics.jsp // 统计页 └── WEB-INF/ └── web.xml // Servlet 映射配置

这个结构里,JSP 只负责展示,Servlet 只负责收参数和跳转,DAO 只负责和数据库打交道。你可以把它理解成餐厅分工:JSP 是端菜的服务员,Servlet 是下单的传菜员,DAO 是后厨。各自干好各自的活,出了问题也知道找谁。

2.2 数据库表设计:3 张表把考勤逻辑讲清楚

考勤系统的核心数据模型其实不复杂,三张表就够了。但很多新手会在这翻车——最常见的是把签到状态只存成一个“是否签到”的布尔值,结果想查“这个学生今天几点来的”就得加字段,想查“这个学生上周迟到了几次”就得写一堆逻辑。我的建议是:状态用枚举字符串,时间用精确时间戳。

下面是我用过多次的表结构,直接建就行:

-- 学生表 CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', major VARCHAR(50) COMMENT '专业', class_name VARCHAR(50) COMMENT '班级', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 考勤记录表 CREATE TABLE t_attendance ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL COMMENT '关联学生ID', check_date DATE NOT NULL COMMENT '考勤日期', check_in_time DATETIME COMMENT '签到时间', status VARCHAR(10) DEFAULT 'normal' COMMENT '状态: normal/late/absent', remark VARCHAR(255) COMMENT '备注', FOREIGN KEY (student_id) REFERENCES t_student(id), UNIQUE KEY uk_student_date (student_id, check_date) ); -- 用户表(管理端) CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) );

注意t_attendance表里我加了一个联合唯一索引uk_student_date (student_id, check_date),这意味着同一个学生一天只能有一条考勤记录。这个索引是防重复签到的关键,比你在 Servlet 里用if判断要可靠得多——数据库层面的约束永远比应用层的判断更靠谱。

2.3 数据源配置与 DBUtil 工具类:一个类搞定连接管理

老项目最烦的就是到处DriverManager.getConnection(),然后忘关连接导致数据库连接池爆掉。JSP 时代虽然没有 Spring 帮你管事务,但我们可以写一个工具类把获取连接和关闭资源统一封装。这里我用的是传统 JDBC 直连,因为课程设计环境下用 C3P0 或 Druid 连接池容易被老师问“你懂连接池原理吗”而卡壳。直连方案更透明,排错也容易。

package com.lab.util; import java.sql.*; public class DBUtil { private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/lab_attendance" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai" + "&useSSL=false"; private static final String USERNAME = "root"; private static final String PASSWORD = "yourpassword"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) {} } if (stmt != null) { try { stmt.close(); } catch (SQLException e) {} } if (conn != null) { try { conn.close(); } catch (SQLException e) {} } } }

这段代码里有几个细节值得注意:

  • MySQL 8.x 的驱动类名是com.mysql.cj.jdbc.Driver,如果你用的 MySQL 5.x,要写成com.mysql.jdbc.Driver,写错就会在Class.forName这行直接报ClassNotFoundException。
  • URL 里必须带serverTimezone=Asia/Shanghai,否则你往 DATETIME 字段插入时间时会报时区错误,这是 MySQL 8 之后的硬性要求。
  • useSSL=false是关掉 SSL 警告,不加的话控制台会刷一堆红字,虽然不是错误但也够烦人。

这里用线程同步的处理会更优雅,但我们课程设计不用搞那么花哨。每次请求开一个连接、用完即关,在并发量不高的场景下完全够用了。

3. 核心功能落地:登录认证、签到签退与考勤统计的代码实现

3.1 登录认证:用 Session 管理会话状态

登录是所有 Web 系统的入口,JSP 里做登录的核心逻辑就是:接收表单参数 → 查数据库验证 → 把用户信息塞进 Session → 跳转到主页。很多新手会忽略的坑是:登录成功后没有在 JSP 页面做 Session 判空,结果用户直接访问student/list.jsp也能看到数据,这等于门户大开。

我先写控制层的LoginServlet:

package com.lab.servlet; import com.lab.dao.UserDao; import com.lab.entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; @WebServlet("/login") public class LoginServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); // 基础校验:防止空值走数据库查询 if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { req.setAttribute("error", "用户名和密码不能为空"); req.getRequestDispatcher("/index.jsp").forward(req, resp); return; } User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = req.getSession(); session.setAttribute("loginUser", user); session.setMaxInactiveInterval(30 * 60); // 30分钟无操作自动过期 resp.sendRedirect(req.getContextPath() + "/attendance/checkin.jsp"); } else { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("/index.jsp").forward(req, resp); } } }

逻辑说明:先拿到表单提交的username和password,做一次空值过滤——这一步是为了防止恶意请求直接打到 DAO 层。然后调用UserDao查数据库,查到了就创建 Session 并把用户对象存进去,setMaxInactiveInterval(30 * 60)表示 30 分钟没操作就自动失效。查不到就返回登录页并把错误信息带到页面上。

带登录校验的页面,我在 JSP 最上面加一段判空逻辑:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <% User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/index.jsp"); return; } %>

这是 JSP 时代常见的防未授权访问手法,相当于页面级的拦截器。如果你有多个页面都要做这个校验,复制粘贴也行,但如果页面多到 10 个以上,建议写一个AuthFilter实现javax.servlet.Filter,在web.xml里拦/*路径,排除index.jsp和/login即可。

3.2 签到与签退:一条 SQL 完成 upsert 防重复

签到功能的难点不在“插入一条记录”,而在“同一个学生同一天不能重复签到”。前面建表时我加了唯一索引uk_student_date,现在配合INSERT ... ON DUPLICATE KEY UPDATE,一条语句就能把“首次签到插入、重复签到更新”这个逻辑搞定。

public boolean checkIn(int studentId) { String sql = "INSERT INTO t_attendance (student_id, check_date, check_in_time, status) " + "VALUES (?, CURDATE(), NOW(), 'normal') " + "ON DUPLICATE KEY UPDATE " // 如果当天已有记录,则更新时间 + "check_in_time = NOW()"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, studentId); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } }

这段代码里我用了CURDATE()取当天日期、NOW()取当前时间,所以 Java 端不用传时间参数,减少了一步参数绑定的出错机会。ON DUPLICATE KEY UPDATE的作用是:当唯一索引冲突(即学生当天已经有一条记录)时,不再插入新行,而是把已存在那行的check_in_time更新为当前时间。

逻辑说明:这叫“幂等更新”,无论你点击多少次签到按钮,数据库里每个学生每天始终只有一条记录。如果你用的是先SELECT再INSERT的老办法,在高并发或多标签页场景下会出现重复记录,到时候只能手动清数据库,相当狼狈。

签退功能同理,把check_in_time换成check_out_time字段即可。如果你的需求里有“迟到判定”,就在更新后比较时间:如果签到时间晚于实验室规定开门时间(比如 08:30),自动把状态改为late,这个判断可以在 Servlet 里用LocalTime.now().isAfter(LocalTime.of(8, 30))实现。

3.3 考勤统计:月份报表的 SQL 怎么写

统计功能是实验室老师最常用的功能——他要看的是“这个月学生出勤率是多少”“谁经常缺勤”,而不是一条条原始记录。我的统计页面分两个维度:按学生汇总和按日期汇总。

按学生统计月度出勤的核心 SQL:

SELECT s.student_no, s.name, COUNT(a.id) AS total_days, -- 出勤总天数 SUM(CASE WHEN a.status = 'normal' THEN 1 ELSE 0 END) AS normal_days, SUM(CASE WHEN a.status = 'late' THEN 1 ELSE 0 END) AS late_days, SUM(CASE WHEN a.status = 'absent' THEN 1 ELSE 0 END) AS absent_days FROM t_student s LEFT JOIN t_attendance a ON a.student_id = s.id AND DATE_FORMAT(a.check_date, '%Y-%m') = ? WHERE s.id NOT IN (...) GROUP BY s.id, s.student_no, s.name

这里有个关键点:LEFT JOIN的条件里我用了DATE_FORMAT(a.check_date, '%Y-%m') = ?,把月份筛选放在了 ON 子句里而不是 WHERE 子句里。这是因为如果放在 WHERE 里,LEFT JOIN会退化成INNER JOIN——那些没有考勤记录的学生(全勤缺席)会被过滤掉,这不是我们想要的。放在 ON 里,即使某学生这个月一次都没来,也会显示在结果中,只是统计数字都是 0。

这个坑我踩过一次——当时统计结果里少了好几个人,排查半天才发现是 WHERE 和 ON 的语义差导致,后来把这个经验记在笔记本上再也没犯过。统计结果在前端用 JSTL 的c:forEach循环渲染成表格,再用高亮的颜色标记出勤率低于 80% 的学生,老师一眼就能看出问题。

4. 部署与运行:Tomcat 配置、MySQL 初始化与 war 包发布全流程

4.1 从零开始的环境搭建:JDK + Tomcat + MySQL 的版本搭配

JSP 项目对环境版本非常敏感,版本配不齐,跑不起来是常态。我推荐一套经过验证的搭配:JDK 8 + Tomcat 8.5 + MySQL 5.7。如果你想用更新的,JDK 11 + Tomcat 9 + MySQL 8.0 也可以,但注意驱动版本必须对应——MySQL 8 配mysql-connector-java-8.0.x.jar,MySQL 5.7 配mysql-connector-java-5.1.x.jar,混用会报各种奇怪错。

下载 Tomcat 后解压到纯英文路径(不要有中文和空格),然后配置环境变量JAVA_HOME指向 JDK 安装目录。启动 Tomcat 验证安装:双击bin/startup.bat(Windows),浏览器访问http://localhost:8080,看到 Tomcat 首页就说明环境没问题。如果你看到的是 404 或连接失败,先检查端口占用——netstat -ano | findstr 8080找出占用进程,或者改 Tomcat 的conf/server.xml里端口号。

4.2 用 Eclipse 或 IDEA 导入并部署 war 包

以 IDEA 为例(Eclipse 流程类似):直接File → Open选择解压后的项目文件夹,如果是 Maven 项目 IDEA 会提示导入依赖,如果是传统项目没有 Maven,你需要手动把WEB-INF/lib下的 jar 包(包括mysql-connector-java-x.x.jar、jstl.jar、standard.jar)确认放好。

部署到 Tomcat 有两条路:

  1. 开发调试用:IDEA 配置好 Tomcat Server,Deployment里添加war exploded,Debug 模式启动,代码修改后热部署直接生效。
  2. 交付用:Build → Build Artifacts → Build生成 war 包,把xxx.war拷贝到 Tomcat 的webapps目录下,启动 Tomcat 自动解压部署。

第二个方式在最终交付时更干净——把一个 war 包丢给答辩老师,他只要放到 Tomcat 里就能跑。项目里如果有配置文件(比如数据库密码)要记得在 war 包里,不要放在外部,不然换机器就找不到配置。

4.3 MySQL 初始化一条龙:建库脚本、编码与数据预置

数据库初始化只需要两样东西:建库建表 SQL 和测试数据。很多人在这步会遇到“中文乱码”问题,根源是你建的数据库字符集不对。建库时显式指定:

CREATE DATABASE lab_attendance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

utf8mb4是 UTF-8 的超集,能存 Emoji 和特殊字符,比裸utf8更保险。表结构的 SQL 文件建议用source命令导入。

我还会顺手插入几条测试学生数据和管理员账号:

INSERT INTO t_user (username, password, real_name) VALUES ('admin', 'admin123', '实验室管理员'); -- 注意:正式项目中密码必须存 MD5,这里仅为演示方便

预置测试数据的意义在于,你部署完成后可以直接登录测试签到功能,不用先从页面一个个添加学生。以“先看到效果”为目标去调程序,比从头到尾手点快得多。

5. 避坑指南:JSP 考勤系统开发和部署中的 5 个易翻车点

5.1 现象一:JSP 页面能打开,但所有请求提交后 404

这个问题是新手重灾区。原因几乎都指向同一个:Servlet 的映射路径和表单的提交路径不一致。比如你@WebServlet("/checkin"),但页面里写的是<form action="doCheckin" method="post">,Tomcat 找不到doCheckin这个映射,自然报 404。

解决:统一路径规则。我习惯所有 Servlet 映射都用@WebServlet("/xxx")的注解方式,表单的 action 前面加${pageContext.request.contextPath},例如<form action="${pageContext.request.contextPath}/checkin">。这样不管项目部署在什么上下文路径下,请求都能拼对地址。

5.2 现象二:前端页面中文正常,但管理员登录后从数据库读出的姓名是问号

现象是:页面直接写死的中文没问题,但凡是数据库里读出来的中文全是??。

原因有两个:一是数据库中表的字符集不是 UTF-8,二是 JDBC 连接串里没指定字符集。缺了characterEncoding=utf8,JDBC 驱动会按平台默认编码把字节流解码,中文就坏了。

解决:确保三个地方一致。数据库建库语句用utf8mb4,JDBC URL 里带characterEncoding=utf8,JSP 页面顶部声明contentType="text/html;charset=UTF-8"。三处配齐,中文问题基本消除。我的经验是先改 JDBC 连接串,因为这是最常见的漏网之鱼。

5.3 现象三:签到重复点击,出现多条考勤记录

这个坑我前面提到过,但这里单独再强调一次。现象是学生连点两次签到按钮,数据库出现了两条同一天记录。

原因:你用的是SELECT 查询存在 → INSERT 插入两步逻辑,如果第二次点击发生在第一次查询和插入之间(时间差极小,但确实存在),第二次查询查不到记录,就会再插入一条。

解决:不要依赖应用层逻辑做唯一性判断,直接在数据库表上加联合唯一索引UNIQUE KEY uk_student_date (student_id, check_date),代码里用INSERT ... ON DUPLICATE KEY UPDATE。这是我最早踩过的一个真坑,当年被导师要求在并发测试下做压测,结果一压就翻车,加完索引立刻治好。

5.4 现象四:Tomcat 启动时端口被占用,报Port 8080 already in use

这不算代码问题,但十个人里至少有四个遇到过。可能是你开着多个 Tomcat 实例,也可能是之前崩掉的进程没杀干净,还可能被别的开发工具占用了端口。

解决:命令行执行netstat -ano | findstr 8080,看到 LISTENING 行末尾的 PID,然后taskkill /F /PID <PID>强制结束。如果你预期这个端口属于其他服务,那就干脆改 Tomcat 默认端口——打开conf/server.xml,把<Connector port="8080" ...>改成8081,改完重启即可。

5.5 现象五:部署到云服务器或新电脑上,数据库连接超时、页面转半天

本地跑得好好的,部署到另一台机器就出问题,而且控制台报Access denied for user 'root'@'...'。这是 MySQL 用户权限问题——你的root@localhost权限只允许本机访问,从远程连过去 MySQL 会拒绝。

解决:登录本地 MySQL 后执行授权:

GRANT ALL PRIVILEGES ON lab_attendance.* TO 'root'@'%' IDENTIFIED BY '你的密码'; FLUSH PRIVILEGES;

这个问题有个细节:@'%'表示允许任意 IP 访问,安全性稍弱。如果你用的是云数据库,建议只放行你服务器的 IP 或内网网段,别偷懒全开。我还遇到过一种玄学情况:明明代码没改,换台电脑连数据库就慢,结果是 MySQL 配置文件里bind-address被设置成127.0.0.1导致的,改掉就行。

6. 进阶用法:给 JSP 考勤系统加一点“看得见的价值”

当你能把上面的登录、签到、统计、部署跑通之后,这套系统已经是一个能交差的完整项目了。但如果你想在答辩或工作中多拿几分,我建议加一个功能:用前端图表把考勤数据可视化。

具体做法是:统计页面的 JSP 里加一个<canvas>元素,后端写一个StatisticsApiServlet,把月度出勤数据以 JSON 格式输出,前端用原生 JavaScript 绘制柱状图。很多人一想到图表就拉 ECharts 进来,但课程设计场景里原生 Canvas 反而更能展示基本功——老师问“你 ECharts 不熟,改成原生能实现吗”,你答“能”,这个印象分会加不少。

一个简单的月度出勤统计接口:

@WebServlet("/api/attendance/stats") public class StatsApiServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { AttendanceDao dao = new AttendanceDao(); List<Map<String, Object>> list = dao.getMonthlyStats(); resp.setContentType("application/json;charset=UTF-8"); // 手动拼 JSON 或用 Gson 序列化 resp.getWriter().write(buildJson(list)); } }

前端在 JSP 里用fetch拉接口,渲染条形图或折线图。这个功能把“统计数据”从静态表格升级成了动态图表,整个系统的完成度立刻不一样。另一个值得加的小功能是“迟到阈值可配置”——把实验室的签到截止时间存到t_config表里,管理员在页面上能改,而不是每次改代码。

我在多个课程设计项目里反复见到的一个通病是:项目功能齐全,但页面样式停留在 2008 年。JSP 页面套个 Bootstrap 5 的 CDN 也就几分钟的事,布局加上卡片、表格加上斑马纹,答辩的第一印象会比白底黑字好很多。记住,老师不一定有时间逐一试你的功能,但他一定能一眼看出你有没有用心。

最后我自己的习惯是:做完项目后,写一个README.md放进项目根目录,记录部署步骤、默认账号、数据库初始化顺序。这花不了十分钟,但三个月后你自己回来看这个项目,会觉得“幸好当时写了”。希望今天这套从架构到部署再到进阶的思路,能帮你少走几步弯路,把一个 JSP 考勤系统做得既扎实又拿得出手。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询