JSP+Servlet实现实验室考勤管理系统:登录签到与数据库设计

发布时间:2026/10/4 4:27:29
JSP+Servlet实现实验室考勤管理系统:登录签到与数据库设计 简介基于JSP的实验室考勤管理系统是一个面向计算机专业毕业设计和期末大作业的完整项目适合正在准备课题答辩或需要实战练习的开发者。系统覆盖用户身份验证、考勤记录管理、数据查询与统计报表等功能采用JSPServletJDBC构建可部署在Tomcat服务器上并体现了MVC分层思想及前端交互设计。压缩包共1385个文件、约17MB包含107个JSP页面、105个Java源码、161个CSS与397个JS前端资源同时提供SQL数据库脚本、论文文档与数据文档方便对照学习。资源内源码均经过本地编译与调试运行稳定论文文档记录了从需求分析、数据库设计到功能实现与测试的全过程数据文档提供了实验数据和测试用例可直接用于功能验证与二次开发。目前已有27人学习下载适合需要完整项目参考、快速理解JSP Web开发并在此基础上扩展功能的同学。1. 实验室考勤不用再翻表格JSP 考勤管理系统到底能做什么周五晚上要把一周的实验课考勤汇总成表格翻纸质签到本翻到眼花的场景做过实验室管理的老师都不陌生。基于jsp的实验室考勤管理系统网页的设计与实现就是这类需求最常见的课程设计和毕业设计选题用 JSP Servlet 的动态网页技术搭一个能登录、能打卡、能统计的考勤网页。它能解决的不只是“把签到换到线上”而是让每次进入实验室的刷卡或点名变成一条可查询、可导出的数据库记录迟到、早退、请假都按规则自动落库。适合正在做 Java Web 课设的学生、刚学完 Servlet 想练手的人以及想用最小成本给实验室配一套考勤工具的运维老师。这套东西的难点从来不在页面写得多好看而在分层怎么分、表怎么建、重复提交怎么防这篇文章就把这些说透。2. 架构拆解JSP 考勤系统的分层模型与三张核心表2.1 JSP 在考勤系统里的分工MVC 分层与一次完整请求的数据流很多人把 JSP 考勤系统理解成“写几个 JSP 页面就够了”这是最常见的误解。JSP 本身只负责往浏览器输出 HTML真正决定系统好不好用的是它背后的控制层和数据层。在这个项目里代码分成三层JSP 页面只做展示Servlet 只做请求转发和业务调度DAO 层用 JDBC 跟 MySQL 打交道。分层最直接的好处是以后想把展示层改成前后端分离Servlet 改成返回 JSONDAO 代码可以原封不动地复用。层次技术选型职责视图层JSP JSTL渲染表格、表单、提示信息控制层Servlet接收参数、调用 DAO、决定跳转数据层DAO JDBC封装 SQL、连接数据库、返回实体一次完整的考勤请求是这样走的登录后浏览器向 Tomcat 发起/checkin请求web.xml 里的 servlet-mapping 把路径交给 CheckinServletServlet 从 request 里取参数调 AttendanceDao 查询或写入DAO 通过 JDBC 操作 MySQLServlet 拿到结果后决定 forward 回 JSP 还是 sendRedirect 到结果页。整个过程里JSP 只在最后一步参与渲染不要在 JSP 里写 JDBC 代码更不要在 JSP 里写“处理逻辑”。这套老组合到现在还没被淘汰原因很实际一个 JSP 考勤系统打 WAR 包丢进 Tomcat 就能跑不依赖 Maven 私服和复杂配置课设答辩时分层逻辑清晰评委问“你这项目怎么跑的”两分钟讲完依赖就一个 MySQL 驱动 JAR不会出现依赖冲突。注意JSP 页面里尽量用 EL 表达式和 JSTL少写% %脚本片段这样页面干净也更好维护。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table border1 cellpadding6 trth姓名/thth课程/thth签到时间/thth状态/th/tr c:forEach items${attendanceList} varatt tr td${att.userName}/td td${att.courseName}/td td${att.checkinTime}/td td${att.status 1 ? 正常 : att.status 2 ? 迟到 : 缺勤}/td /tr /c:forEach /table这里的${att.userName}是 EL 表达式直接从 request 或 session 作用域取对象属性比% %写 Java 代码安全得多。JSTL 的c:forEach负责遍历集合页面里不出现一行 Java 代码浏览器报错时也能更快定位是数据处理问题还是页面渲染问题。2.2 三张核心表怎么设计user、course、attendance 的字段与索引数据库设计决定了考勤系统的上限。常见的做法是三张表用户表lab_user存学生和老师课程表lab_course存实验课时间地点考勤表lab_attendance存每一次签到签退记录。表结构可以精简但索引和状态字段不能省。CREATE DATABASE IF NOT EXISTS lab_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE lab_db; CREATE TABLE lab_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(32) NOT NULL UNIQUE COMMENT 登录名, password CHAR(32) NOT NULL COMMENT MD5密文长度固定32位, real_name VARCHAR(32) NOT NULL COMMENT 姓名, role TINYINT NOT NULL DEFAULT 0 COMMENT 0学生 1教师 2管理员, lab_no VARCHAR(16) DEFAULT NULL COMMENT 所属实验室编号 ) ENGINEInnoDB; CREATE TABLE lab_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(64) NOT NULL COMMENT 实验课名称, teacher_name VARCHAR(32) NOT NULL COMMENT 授课教师, start_time TIME NOT NULL COMMENT 上课时间如08:30, end_time TIME DEFAULT NULL COMMENT 下课时间, lab_no VARCHAR(16) DEFAULT NULL COMMENT 上课实验室 ) ENGINEInnoDB; CREATE TABLE lab_attendance ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT 关联lab_user.id, course_id INT NOT NULL COMMENT 关联lab_course.id, att_date DATE NOT NULL COMMENT 考勤日期, checkin_time DATETIME DEFAULT NULL COMMENT 签到时间, checkout_time DATETIME DEFAULT NULL COMMENT 签退时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 0缺勤 1正常 2迟到 3请假, UNIQUE KEY uk_user_course_date (user_id, course_id, att_date), KEY idx_date (att_date) ) ENGINEInnoDB;几个关键决定解释一下。password用CHAR(32)是因为 MD5 密文固定 32 位别用 VARCHAR(255) 留多余空间。status用 TINYINT 而不是字符串枚举是为了后面统计 SQL 写起来方便SUM(status2)就能算出迟到人数。uk_user_course_date唯一索引是防重复签到的最底层防线即使代码漏了判断MySQL 也会拒绝同一人同一天同一门课的第二条记录。att_date单独建索引是因为考勤统计几乎都按日期筛选。要不要加外键我一般不加。课设阶段数据量小外键约束反而会在批量补录时卡住操作业务层的唯一索引和程序判断已经足够。但user_id和course_id必须加普通索引否则后期统计JOIN会慢。2.3 DAO 层封装把 JDBC 写成一个可复用的 BaseDao新手最容易把 JDBC 代码复制得满项目都是改一个数据库密码要搜遍全部 Servlet。合理做法是抽一个BaseDao基类连接和释放都写在这里每个业务 DAO 继承它就行。package dao; import java.sql.*; public class BaseDao { protected Connection getConn() throws SQLException { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new SQLException(找不到MySQL驱动请检查WEB-INF/lib下的jar包, e); } String url jdbc:mysql://localhost:3306/lab_db ?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/ShanghaiuseSSLfalse; return DriverManager.getConnection(url, root, 你的密码); } protected void close(Connection conn, Statement st, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (st ! null) { try { st.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }Class.forName在 MySQL 8.0 下必须写com.mysql.cj.jdbc.Driver老的com.mysql.jdbc.Driver只在 5.x 驱动里存在这一步错了直接报 ClassNotFoundException。close方法必须写很多项目跑几天后开始卡死就是因为连接没释放MySQL 默认最大连接数 151连接泄漏一次就少一个。课设阶段用DriverManager.getConnection直连是够用的不需要引入 C3P0 或 Druid 连接池少一个依赖就少一个配置项。如果你后面要部署到公网服务器再换成连接池不迟。3. 核心功能落地登录、签到、签退的 JSPServlet 代码实现3.1 登录模块Session 落地与角色跳转登录页是一个 form 表单用户名密码提交到/login这个 Servlet。这里要注意的是密码处理数据库存的是 MD5 密文Servlet 里必须先做 MD5 再比对绝不能把明文密码拼进 SQL。用WebServlet注解可以省去 web.xml 里的 mapping但如果你用的是 Tomcat 9 之前的版本建议还是写进 web.xml兼容性更好。package servlet; import dao.UserDao; import entity.User; import util.MD5Util; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password MD5Util.md5(req.getParameter(password)); User user new UserDao().findByUsernameAndPassword(username, password); if (user null) { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); return; } HttpSession session req.getSession(); session.setAttribute(user, user); resp.sendRedirect(index.jsp); } }这里有两个细节值得说。失败时用forward回到登录页这样login.jsp能通过${msg}显示错误提示成功时用sendRedirect而不是forward这是防止刷新页面重复提交登录表单的关键。如果成功用forward用户按一下 F5表单会再提交一次服务器又执行一遍查库和登录。登录后的“jsp个人信息展示页面”很简单首页右上角放当前登录人的姓名和角色直接读 session 就行% page contentTypetext/html;charsetUTF-8 % p欢迎${sessionScope.user.realName}角色 c:choose c:when test${sessionScope.user.role 0}学生/c:when c:when test${sessionScope.user.role 1}教师/c:when c:otherwise管理员/c:otherwise /c:choose /psessionScope.user是 LoginServlet 里塞进去的 User 对象EL 表达式直接取属性。这种做法比在 JSP 里写session.getAttribute(user)再强转干净得多也避免了一堆% %污染页面。3.2 签到防重查重再插入与迟到判定签到是考勤系统的核心写操作也是最容易出问题的位置。控制层只有两个任务查重和判定状态。查重必须在 DAO 层做一次数据库唯一索引再兜底一次两层都过了才插入。package servlet; import dao.AttendanceDao; import dao.CourseDao; import entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.sql.Time; WebServlet(/checkin) public class CheckinServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(); User user (User) session.getAttribute(user); if (user null) { resp.sendRedirect(login.jsp); return; } int courseId Integer.parseInt(req.getParameter(courseId)); AttendanceDao dao new AttendanceDao(); if (dao.existsToday(user.getId(), courseId)) { req.setAttribute(msg, 今天已签到请勿重复提交); req.getRequestDispatcher(index.jsp).forward(req, resp); return; } Time startTime new CourseDao().findStartTime(courseId); long diff System.currentTimeMillis() - startTime.getTime(); String status 1; // 默认正常 if (diff 10 * 60 * 1000) { // 超过10分钟算迟到 status 2; } dao.insert(user.getId(), courseId, status); resp.sendRedirect(index.jsp?msgok); } }existsToday的 SQL 是SELECT 1 FROM lab_attendance WHERE user_id? AND course_id? AND att_dateCURDATE() LIMIT 1查到一个就返回 true不需要 count(*) 数出总数。签到时间不建议用System.currentTimeMillis()直接落库常见做法是让数据库统一取时间INSERT 语句里用NOW()和CURDATE()这样即使应用服务器时钟比数据库快几分钟也不会出现时间错乱。上面代码为了演示判定逻辑用了System.currentTimeMillis()实际项目里把判断放到 DAO 层用SELECT NOW()一次取回数据库时间再比较更稳。前端 JSP 页面的签到按钮是一个隐藏课程 ID 的表单配合 c:forEach 循环课程列表生成多个签到按钮。form actioncheckin methodpost input typehidden namecourseId value${course.id} / button typesubmit签到/button /form3.3 签退与补卡边界条件也要在动手前想清楚签退和签到不同签退是更新已有记录而不是插入。核心 SQL 只有一条UPDATE lab_attendance SET checkout_time NOW() WHERE user_id ? AND course_id ? AND att_date CURDATE();执行更新前要先查一次checkout_time是否为空防止学生重复点击签退把第一次时间覆盖。早退的判定逻辑是拿checkout_time和lab_course.end_time比较如果早于下课时间就把status改成 4早退但 4 这个状态需要老师后台确认之后才计入统计不能让学生自己改状态。补卡是另一个容易忽略的场景。学生忘了签到老师手动在后台补一条记录补录时要注意如果原考勤记录已经是缺勤状态直接把它改成正常即可如果没有记录插入时att_date要指定那天不能用CURDATE()否则就补错了日期。补录之后唯一索引uk_user_course_date依然生效不会出现同一人一天多条考勤。请假状态由学生提前提交申请老师审批通过后管理员在后台把当天对应课程的状态直接置为 3。这个操作必须走单独的更新接口不能让学生通过改表单状态绕过否则就会出现“先签退再改请假”的逻辑漏洞。4. 参数与配置清单JDBC 连接串、web.xml 与 JSP 页面缓存4.1 JDBC 连接串里的 5 个必调参数一个能稳定跑起来的 JDBC 连接串参数比很多人想象的多。下面这组是我在 JSP 考勤系统里常用的标准写法String url jdbc:mysql://localhost:3306/lab_db ?useUnicodetrue characterEncodingutf8 serverTimezoneAsia/Shanghai useSSLfalse allowPublicKeyRetrievaltrue;参数推荐值作用与坑useUnicodetrue不设的话中文可能变问号characterEncodingutf8必须和 JSP、数据库三处统一serverTimezoneAsia/Shanghai不设会按服务器默认时区可能差 8 小时useSSLfalse本地开发设 false 省去证书警告allowPublicKeyRetrievaltrueMySQL 8.0 的 caching_sha2_password 插件必须加否则报 Public Key Retrieval 错误characterEncodingutf8和serverTimezoneAsia/Shanghai这两个参数是中文乱码和时间错乱的重灾区。很多项目本地跑得好好的部署到云主机上时间差 8 小时、中文变问号十有八九是连接串少了这两个参数。注意utf8和utf8mb4的区别连接串里写utf8没问题但表结构建议用utf8mb4因为utf8在 MySQL 里不支持 emoji 和生僻字学生姓名带有特殊字符时会插入失败。4.2 web.xml 的路径映射、会话时长与欢迎页用注解配置 Servlet 能省代码但 web.xml 里仍有几个必配项。欢迎页、会话超时和统一编码过滤器都建议放在这里。web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1 welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list session-config session-timeout30/session-timeout /session-config filter filter-nameencodingFilter/filter-name filter-classfilter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping /web-appsession-timeout单位是分钟默认 30 分钟。课设答辩现场经常出现的尴尬是评委看了一圈页面再回来操作Session 过期跳回登录页。解决办法是把session-timeout临时调到 60 或 120。演示完记得改回来这是很多人的血泪教训。编码过滤器写在 web.xml 里比在每个 Servlet 开头调用request.setCharacterEncoding(UTF-8)更省事。过滤器拦截/*所有 POST 请求的中文参数都会先经过转码JSP 页面顶部再加% page contentTypetext/html;charsetUTF-8 %三层字符集统一才不会乱码。4.3 JSP 页面缓存控制让考勤统计页每次加载都是新数据考勤系统有个特殊需求统计页的数据必须是最新的浏览器不能把旧页面缓存下来。默认情况下浏览器后退时会直接显示本地缓存页面学生签完到后退回统计页看到的还是签到前的数据容易误以为签到失败。在考勤统计页的 JSP 开头加三段响应头% response.setHeader(Cache-Control, no-cache, no-store, must-revalidate); response.setHeader(Pragma, no-cache); response.setDateHeader(Expires, 0); %Cache-Control是 HTTP 1.1 的标准Pragma是给老浏览器兜底的Expires设为 0 表示立即过期。这个组合能让浏览器每次进入统计页都重新请求服务器而不是读本地缓存。另一种场景是实验室门口的大屏展示页希望对签到情况实时刷新。常见做法是在 JSP 的head里加meta http-equivrefresh content60页面每 60 秒自动刷新一次适合挂在实验室门口显示“今日考勤统计”。如果要求更高可以改成前端定时器配合 Ajax 局部刷新但 JSP 课设阶段 meta refresh 已经完全够用。注意不要把meta refresh放在签到成功页否则页面加载后自动刷新会再次触发请求造成重复提交。5. 避坑排查部署 JSP 考勤系统最容易翻车的 5 个位置5.1 环境与部署类驱动、版本、连接三座大山现象 1启动 Tomcat 后访问页面报 500控制台提示 ClassNotFoundException: com.mysql.jdbc.Driver。原因MySQL 驱动 JAR 包没放进WEB-INF/lib目录或者 MySQL 8.0 驱动类名写错。很多人把驱动 JAR 放在 IDE 的 classpath 里启动不报错一打 WAR 包部署就找不到类。解决确认mysql-connector-java的 JAR 在WEB-INF/lib下并且代码里用的是com.mysql.cj.jdbc.Driver。现象 2JSP 页面里的 JSTL 标签全部报错页面上肉眼可见 c:forEach 标签。原因Tomcat 版本太新JSTL 库的包名从javax.servlet变成了jakarta.servlet或者web.xml版本声明和 Tomcat 不匹配。解决课设项目优先用 Tomcat 8.5 或 9.0这两个版本和传统 JSP 开发方式兼容最好。如果非要用 Tomcat 10就得换 Jakarta 版本的 JSTL 依赖。现象 3连接 MySQL 时提示 Access denied for user或者 Public Key Retrieval is not allowed。原因MySQL 8.0 默认认证插件是caching_sha2_password旧版驱动不支持这种认证方式。解决JDBC URL 加上allowPublicKeyRetrievaltrueuseSSLfalse。如果还不行用管理员账号执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 密码;切换认证插件。5.2 业务逻辑类重复提交、会话丢失与时间错乱现象 4在签到页面按一下 F5 刷新数据库里出现两条一模一样的签到记录。原因签到成功后使用了forward转发到 JSP 页面浏览器地址栏还是/checkin刷新时又把请求发了一遍。解决所有写操作成功后就sendRedirect让浏览器跳转到纯展示页面DAO 层加existsToday判断数据库唯一索引uk_user_course_date做最终兜底。这三层少一层都会在某次手滑时翻车。现象 5配置了 session-timeout 还是频繁掉线每次操作都被打回登录页。原因Tomcat 默认的 Session Cookie 在浏览器关闭后失效或者多次跨域跳转导致 JSESSIONID 丢失。解决登录后调用session.setMaxInactiveInterval(60 * 60)指定活动时长同时检查页面跳转是否使用了response.encodeRedirectURL。更常见的原因其实是部署后重启 Tomcat重启瞬间 Session 全部失效这是正常现象不用怀疑配置。现象 6签到时间比实际时间慢 8 小时迟到判断完全不准。原因数据库服务器配置了 UTC 时区而连接串里没有指定serverTimezone。解决JDBC URL 加serverTimezoneAsia/Shanghai同时签到 SQL 用NOW()而不是应用服务器的System.currentTimeMillis()。这两个地方都改了之后时间才真正统一。这种问题在云服务器上特别容易踩本地整机测试发现不了因为本机和数据库时钟一致。6. 进阶加固过滤器统一登录校验与图片坐标定位考勤如果项目只是在课设答辩时跑一次前面的内容已经够用。但要让系统显得“可维护、有工程素养”下面三个技巧很加分。第一个用 Filter 统一登录校验。每个 Servlet 里都写if (session.getAttribute(user) null)很啰嗦而且漏写一个就等于留了一个后门。抽一个 LoginFilter/*拦截所有请求白名单放过登录页、登录接口和静态资源目录其他请求一律检查 Session没登录就跳回 login.jsp。这个改动不算大但评委问“怎么保证没有权限的人进不来”时比现场翻 Servlet 源码有说服力得多。第二个实验室座位图点选签到。如果实验室要按固定座位考勤可以在 JSP 里放一张座位布局图点击图片时把鼠标坐标写进隐藏表单字段提交后存到考勤表的seat_x、seat_y两个字段。做法是给img绑定 onclick 事件event.offsetX和event.offsetY就是相对图片左上角的坐标。这个功能不算复杂但视觉效果好过一组下拉框演示时能明显提升印象分。第三个演示前必须造数据。统计页如果一条数据都没有再好看的图表也是白搭。答辩前手动插入一条昨天的考勤记录让页面有东西可展示。INSERT INTO lab_attendance(user_id, course_id, att_date, checkin_time, status) VALUES (1, 1, CURDATE() - INTERVAL 1 DAY, 08:31:00, 2);做这套项目时我栽过最惨的一次就是把签到按钮的typebutton写成了typesubmit结果点一下签到变成双击提交数据库靠着唯一索引保住了底线但错误提示文案写得又不够清楚害得老师和学生都以为系统坏了。后来我养成了两条习惯凡是写操作一律重定向加查重凡是时间相关一律交给数据库。希望帮到你。本文还有配套的精品资源点击获取