ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于SSM+layui的课堂考勤系统设计与实现

基于SSM+layui的课堂考勤系统设计与实现 简介这是一套基于Java SSM框架的学生课堂考勤管理系统完整项目覆盖辅导员、管理员、教师、学生四种角色包含学生管理、班级管理、签到管理、请假审批等核心业务适合Java Web学习者或毕业设计参考。压缩包共含1802个文件其中JSP页面、Java类、XML配置与SQL脚本构成项目主体JS、CSS及layui前端资源支撑交互界面同时包含大量gif演示图与png截图便于对照理解运行效果包体总大小33.32MB。目前已有765人下载学习项目支持Tomcat 7/8/9及MySql 5.7环境附带环境配置说明与使用文档可直接导入IDEA或Eclipse运行。整体代码分层清晰涵盖SSM整合、Maven构建、layui渲染等常见技术点是练习企业级Java Web开发流程和考勤业务逻辑的不错选择。1. 考勤系统为什么还需要 SSM layui 来重写课堂考勤看起来是简单的“点名-记录-统计”但真正落到辅导员、任课教师、学生三方协同场景里问题会复杂得多辅导员要管多个班级的请假审批任课教师要发起签到并限定时间窗口学生既要在限定范围内完成签到又要随时查看自己的出勤记录。如果只用 Excel 或微信群接龙数据分散、权限混乱、统计滞后。SSM layui 的组合在校园信息化项目里仍然是性价比极高的选择——SSM 负责事务一致性严谨的业务后端layui 负责不需要前端工程化的后台管理界面JSP 承担服务端渲染整个链路清晰且易维护。这套系统适合两类人一是需要快速上线课堂考勤流程的院校或培训机构二是想从零过一遍 SSM 全家桶 多角色权限设计的 Java 学习者。下面从数据模型、签到逻辑、请假审批到部署排错完整拆一遍这个项目。2. 基于角色的权限矩阵与数据库模型设计2.1 四角色登录后的路由逻辑这套系统定义了四种角色管理员、辅导员、教师、学生。登录接口不做复杂的三方授权直接用用户名密码查询user表通过role字段判断跳转地址。这种方案在中小型系统中比 Spring Security 的过滤器链更轻量而且便于在 JSP 端用自定义标签或 EL 表达式控制页面元素显隐。核心登录校验写在拦截器里继承HandlerInterceptor在preHandle中放行登录接口和静态资源其余请求统一校验 Session 中的userId。角色路由用 switch 表达式做跳转代码逻辑如下// LoginController.java 登录成功后路由 switch (user.getRole()) { case 1: // 管理员 return redirect:/admin/index; case 2: // 辅导员 return redirect:/counselor/index; case 3: // 教师 return redirect:/teacher/index; case 4: // 学生 return redirect:/student/index; default: return redirect:/login?errorrole; }参数说明role字段用数字枚举1 为管理员、2 为辅导员、3 为教师、4 为学生。这里用数字而不是字符串一方面减少数据库存储开销另一方面在 JSP 页面中做角色判断时user.role 3比字符串 equals 更直观。拦截器层面的权限控制属于粗粒度过滤如果要做按钮级的权限校验需要在 JSP 里再处理一次。2.2 核心表结构与状态机设计数据库设计直接决定签到、请假流程能否顺畅流转。项目采用 MySQL 5.7字符集使用 utf8mb4避免签到备注或学生姓名中出现生僻字时乱码。核心表分为四组账号与班级、课程与签到、请假与审批、辅导员与班级关联。以签到主表和请假主表为例-- 签到记录表 CREATE TABLE sign_record ( id int(11) NOT NULL AUTO_INCREMENT, course_id int(11) NOT NULL COMMENT 课程ID, student_id int(11) NOT NULL COMMENT 学生ID, teacher_id int(11) NOT NULL COMMENT 教师ID, sign_code varchar(10) NOT NULL COMMENT 签到码, sign_time datetime DEFAULT NULL COMMENT 签到时间, status tinyint(4) DEFAULT 0 COMMENT 0未签到 1已签到 2迟到, PRIMARY KEY (id), UNIQUE KEY uk_course_student (course_id, student_id, sign_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 请假申请表 CREATE TABLE leave_apply ( id int(11) NOT NULL AUTO_INCREMENT, student_id int(11) NOT NULL, course_id int(11) DEFAULT NULL, reason varchar(500) DEFAULT NULL, start_time datetime DEFAULT NULL, end_time datetime DEFAULT NULL, status tinyint(4) DEFAULT 0 COMMENT 0待审批 1通过 2驳回, apply_time datetime DEFAULT NULL, approve_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时给(course_id, student_id, sign_code)加了联合唯一索引这是防止学生重复签到的最关键约束。如果同一个学生在同一节课用同一个签到码提交两次数据库会因为唯一索引冲突直接拒绝第二次写入比起 Java 代码层的同步锁更可靠。请假表的status字段是典型的审批状态机从待审批流转到通过或驳回驳回后学生可以修改申请再次提交此时生成新的记录而不是修改原纪录保留完整审批历史。2.3 辅导员与班级的关联建模辅导员不是直接关联学生的而是通过班级间接管理。系统中辅导员表和管理员表、教师表一样都是用户表的外键扩展但辅导员需要额外关联多个班级。这里采用单独的关系表而不是在班级表中存counselor_id字段原因是一个辅导员可能同时带多个班级——如果将来学校调整带班关系只更新关系表即可不影响班级表和用户表数据。CREATE TABLE counselor_class ( id int(11) NOT NULL AUTO_INCREMENT, counselor_id int(11) NOT NULL, class_id int(11) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;查询辅导员名下的学生列表时需要三表关联辅导员班级关系表 → 班级表 → 学生表。Mybatis 中可以用嵌套 resultMap 一次性查出来也可以用两条 SQL 分步查——先查班级 ID 列表再查学生列表。建议数据量小时用嵌套 Select 标签数据量大时改为分步查询避免 Mybatis 的 N1 问题。3. 教师端动态签到码的生成与校验实现3.1 为什么不用二维码而用动态码教室场景下二维码签到需要学生打开摄像头扫码手机配置较低的设备识别速度慢而且网络环境差时二维码图片加载不出来。动态码方案是教师在 Web 端发起签到时后端生成一个 6 位数字验证码学生输入即可完成签到。6 位数字从 000000 到 999999 共有 100 万种组合单节课的学生数量通常不超过 100 人暴力破解需要同时满足“知道课程 ID 在签到时间窗口内 签到码正确”三个条件安全性足够。动态码生成逻辑放在SignService中使用ConcurrentHashMap缓存当前有效的签到码并记录过期时间// SignService.java 签到码生成与校验 Service public class SignService { // key 为 courseIdvalue 为签到码和过期时间 private final MapInteger, SignCodeHolder codeCache new ConcurrentHashMap(); // 过期时间单位毫秒默认10分钟 private static final long EXPIRE_MILLIS 10 * 60 * 1000; public String startSign(Integer courseId, Integer teacherId) { // 同一门课同时只能有一个签到码防止教师误操作重复发起 SignCodeHolder holder codeCache.get(courseId); if (holder ! null !isExpired(holder)) { return holder.getCode(); // 已有未过期签到码直接复用 } String code String.format(%06d, new Random().nextInt(1000000)); codeCache.put(courseId, new SignCodeHolder(code, System.currentTimeMillis() EXPIRE_MILLIS)); return code; } public SignResult checkSign(Integer courseId, String inputCode, Integer studentId) { SignCodeHolder holder codeCache.get(courseId); if (holder null || isExpired(holder)) { return SignResult.fail(签到尚未发起或已过期); } if (!holder.getCode().equals(inputCode)) { return SignResult.fail(签到码错误); } // 调用 mapper 写入签到记录 return signServiceMapper.insertSignRecord(courseId, studentId, inputCode); } private boolean isExpired(SignCodeHolder holder) { return System.currentTimeMillis() holder.getExpireTime(); } }参数说明EXPIRE_MILLIS设置 10 分钟这个值需要根据课程时长和点名耗时调整一般建议 5-15 分钟。签到码缓存存放在 JVM 内存中服务重启后缓存丢失此时教师需要重新发起签到项目文档中没有引入 Redis所以这是合理取舍。Random().nextInt(1000000)生成的是 0-999999 的整数%06d格式化保证不足 6 位时前面补零学生端看到的是完整 6 位数字。并发场景下需要注意ConcurrentHashMap的原子性问题两个教师同时为同一门课发起签到时get和put不是原子操作可能出现各自生成签到码互相覆盖。更稳妥的做法是用putIfAbsent或者对 courseId 做 synchronized 锁public synchronized String startSign(Integer courseId, Integer teacherId) { // 同步方法保证同一时刻只有一个线程执行这段逻辑 }在单机部署的校园场景中直接在方法上加synchronized足够支撑几百人并发比引入分布式锁简单很多。3.2 迟到判定与签到时间容差签到不是简单的“点击按钮写入记录”还要区分正点和迟到。实现方式是记录教师发起签到的时间将签到窗口切分为两个阶段前 5 分钟内签到记为“正常”第 6-10 分钟签到记为“迟到”10 分钟后签到码过期。这种时间窗口逻辑放在 service 层而不是 SQL 层因为要复用过期时间的常量。// 状态判定逻辑 long elapsed System.currentTimeMillis() - holder.getStartTime(); if (elapsed 5 * 60 * 1000L) { status 1; // 正常签到 } else if (elapsed 10 * 60 * 1000L) { status 2; // 迟到 } else { return SignResult.fail(签到已截止); }JSP 端用 layui 的table组件渲染签到列表时通过templet自定义列模板将 status 字段的 0、1、2 映射为“未签到、正常、迟到”三种标签。layui 的表格渲染代码可以写成// studentSignList.jsp 中 layui table 列配置 { field: status, title: 签到状态, width: 120, templet: function(d) { if (d.status 1) return span classlayui-badge layui-bg-green正常/span; if (d.status 2) return span classlayui-badge layui-bg-orange迟到/span; return span classlayui-badge layui-bg-gray未签到/span; }}templet是 layui table 的模板函数接收当前行数据对象d返回 HTML 字符串。这里用 layui 内置的徽章样式渲染状态比纯文本展示更直观前端代码也不需要额外引入图标库。4. 学生请假申请与辅导员审批的完整链路4.1 学生端请假的消息流学生端的请假流程不是简单地提交一张表单而是要处理“正在进行的课堂签到和请假申请之间的冲突”。比如学生提交请假时系统需要判断请求的起止时间是否覆盖了某节正在进行的课程如果覆盖且学生尚未签到则自动将该时段标记为“已请假”不参与未签到统计。学生提交请假的 Controller 代码// StudentController.java 提交请假申请 PostMapping(/student/leave/add) public String addLeave(LeaveApply leaveApply, HttpSession session) { Integer studentId (Integer) session.getAttribute(userId); leaveApply.setStudentId(studentId); leaveApply.setStatus(0); // 待审批 leaveApply.setApplyTime(new Date()); // 冲突检测判断请假时间段内是否有未完成的签到 ListInteger conflictCourseIds leaveService.findConflictCourseIds( studentId, leaveApply.getStartTime(), leaveApply.getEndTime() ); if (conflictCourseIds ! null !conflictCourseIds.isEmpty()) { // 存在冲突课程记录自动关联 for (Integer courseId : conflictCourseIds) { leaveService.createLeaveMapping(leaveApply.getId(), courseId); } } leaveService.insertLeaveApply(leaveApply); return redirect:/student/leave; }findConflictCourseIds的 SQL 要根据课程表和签到表联查找出学生的选课记录中时间与请假区间重叠且该学生尚未签到的课程。这里注意日期比较要用 MySQL 的STR_TO_DATE或直接传入 Date 类型参数避免字符串比较导致的边界歧义。4.2 辅导员审批与状态回写辅导员的审批操作就是更新leave_apply表的status字段但审批通过后需要做一次级联更新将该学生对应课程的未签到记录标记为“请假”。如果直接在 approve 方法里写死一组 update 语句后续需求调整会很难维护。项目做法是定义一个LeaveStatusMachine接口把状态流转封装成枚举// LeaveStatusMachine.java 请假状态机 public enum LeaveStatus { PENDING(0) { Override public LeaveStatus approve() { return APPROVED; } Override public LeaveStatus reject() { return REJECTED; } }, APPROVED(1) { Override public LeaveStatus approve() { return this; } Override public LeaveStatus reject() { return this; } }, REJECTED(2) { Override public LeaveStatus approve() { return this; } Override public LeaveStatus reject() { return this; } }; private final int code; LeaveStatus(int code) { this.code code; } public int getCode() { return code; } public abstract LeaveStatus approve(); public abstract LeaveStatus reject(); }这个状态机的设计意义在于审批操作只能从PENDING流转为APPROVED或REJECTED已经审批过的记录不能二次审批通过枚举方法调用直接避免if-else的判断遗漏。审批通过后Mybatis 的updateLeaveApproved语句使用多表联合更新UPDATE leave_apply l SET l.status 1, l.approve_time NOW() WHERE l.id #{leaveId} AND l.status 0;更新返回影响行数如果为 0说明该条记录已经被其他管理员处理过Controller 中直接提示“该申请已被处理”避免重复审批覆盖数据。4.3 辅导员视角的签到汇总辅导员端的“签到列表”功能是将学生出勤数据按课程分组展示。这里的数据来自三方关联签到记录表 学生表 课程表。为了提升列表页响应速度项目中可以用 Mybatis 的延迟加载分页查询时先查学生基础信息再按需加载签到记录。分页使用 PageHelper 插件配置在 Mybatis 的 mybatis-config.xml 中plugins plugin interceptorcom.github.pagehelper.PageInterceptor property namehelperDialect valuemysql/ property namereasonable valuetrue/ /plugin /pluginsreasonable参数设置为true后当页码小于 1 时自动返回第一页页码大于总页数时自动返回最后一页防止用户恶意构造页码参数导致查询越界。5. 部署排错与 layui 前端工程化注意点5.1 Tomcat 与 JDK 版本的匹配陷阱这个项目要求 JDK 1.8 Tomcat 7/8/9实际部署中踩得最多的坑是 Tomcat 版本和 JDK 版本不匹配。Tomcat 9 要求 JDK 8 及以上Tomcat 7 在 JDK 8 下运行没问题但如果本机装了 JDK 11Tomcat 7 会因为缺少javax.annotation相关类直接启动失败。推荐组合是 JDK 8 Tomcat 8.5稳定且兼容性最好。部署时需要注意项目中的web.xml头声明SSM 项目通常使用 Servlet 3.0 规范Tomcat 7 及以下版本需要确认web.xml版本是否符合web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 /web-app如果启动时报Unsupported major.minor version 52.0说明 class 文件版本是 Java 8 编译的而当前 JRE 版本低于 8检查 catalina.bat 中的JAVA_HOME是否指向了旧版本 JDK。5.2 layui 静态资源路径与日期组件配置layui 是纯前端组件库项目中通常放在webapp/static/layui目录。JSP 页面引入时要注意路径SpringMVC 拦截器如果设置了/为拦截路径必须放行静态资源否则浏览器报 404mvc:resources mapping/static/** location/static//layui 的日期控件在考勤管理中有典型应用——请假时间段选择。layui 的laydate组件默认日期选择范围没有限制但如果限制最早只能选择今天需要设定min参数// 请假页面日期控件初始化 layui.use(laydate, function() { var laydate layui.laydate; laydate.render({ elem: #startTime, type: datetime, min: new Date().toLocaleDateString(), // 最早只能选今天 format: yyyy-MM-dd HH:mm:ss }); });min参数如果直接传new Date()可能导致年-月-日的日期分隔符不一致建议先转成toLocaleDateString()再传入。另一个高频问题layui 渲染完成后如果通过 Ajax 动态添加了表格行需要重新调用table.render()或者利用table.reload()更新数据。5.3 签到并发写入的性能优化技巧多人同时签到时数据库层面的唯一索引已经兜底防重但大量并发插入可能导致 InnoDB 锁竞争。可以把插入操作改为批量插入前端提交签到码后由教师端统一提交整个班级的签到名单后端一次性插入多条记录// 批量签到减少数据库连接次数 public int batchSign(ListSignRecord records) { return signMapper.batchInsert(records); }SQL 使用 Mybatis 的foreach标签拼接批量插入注意 MySQL 单条 SQL 允许的最大包大小可通过max_allowed_packet参数调整默认 4M 通常够用。这里对应教师端“签到管理”页面的一个功能教师可以手动勾选已到场的同学批量标记签到而不需要学生逐个操作适合机房或合班上课的场景。5.4 MySQL 5.7 的 SQL 模式检查项目部署时报Expression #1 of SELECT list is not in GROUP BY clause错误是因为 MySQL 5.7 默认开启了ONLY_FULL_GROUP_BY模式。Mybatis 中如果写了group by course_id而 select 列表中含有非聚合字段就会报错。解决方案有两种一种是修改 SQL 保证所有查询字段都在 group by 或聚合函数中另一种是调整 MySQL 的 sql_mode在 my.ini 中[mysqld] sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION建议优先调整 SQL 写法而不是关闭严格模式——项目里已有的多条统计 SQL 如果全部改成聚合函数的写法改动成本大那么临时去掉ONLY_FULL_GROUP_BY是快速上线的最优解但要在交付文档中注明这个环境配置风险。5.5 JSP 页面改动了不生效的处理项目中出现“JSP 改了不生效”的问题多半不是代码错误而是 IDEA 的编译输出目录和 Tomcat 的实际部署目录不一致。IDEA 中 Artifacts 默认输出到target/classes但 Tomcat 运行的是out/artifacts下的副本修改 JSP 后需要执行 Build → Rebuild Project。如果 JSP 的修改只涉及 HTML 和 JSTL 标签可以不重启 Tomcat 直接刷新页面但如果修改了 Java 类或配置文件必须重启才会生效。排查时先看 Tomcat 控制台是否有重编译日志再确认浏览器缓存——很多“改了不生效”其实是浏览器本地缓存了旧 JSP 渲染结果CtrlF5 强制刷新即可。最后检查 JSP 文件头有没有加% page contentTypetext/html;charsetUTF-8 languagejava %编码错误会导致页面中文乱码看起来像是代码没改对。本文还有配套的精品资源点击获取
返回列表