
简介这是一套基于SSM框架开发的Java Web学生课堂考勤管理系统面向高校计算机专业初学者与Java Web课程设计实践者解决多角色协同管理课堂签到、请假审批与数据统计的实际教学管理需求。资源包共1802个文件涵盖422个界面图标gif/png、244个前端交互脚本js、142个样式文件css、88个服务端页面jsp、57个核心业务类java及114个配置与编译产物xml/class完整呈现JSPLayui前端与SpringSpringMVCMyBatis后端的典型分层结构压缩包大小33.32MB。已有766人学习下载适合用于课程设计参考、毕业设计原型或SSM整合实战训练。读者可直接导入IDEA或Eclipse运行含完整数据库SQL脚本、四角色权限控制逻辑、签到/请假全流程业务代码及TomcatMySQL部署说明代码结构清晰、注释规范便于理解MVC职责划分与前后端交互细节。1. 学生课堂考勤管理系统一个真实跑在Tomcat上的SSMJSP老项目不是Demo能直接对接教务排课表这不是一个“Hello World”级别的Java Web练手项目而是一套在真实高职院校信息中心部署过、支撑3个二级学院、27个班级、近1200名学生日常考勤的轻量级生产系统。它用最朴素的技术栈——SpringSpringMVCMyBatisSSM打底JSP做视图层Layui写前端交互MySQL存数据不碰Spring Boot、不搞前后端分离、不加Redis缓存但恰恰因为“土”它能在老旧机房的Windows Server 2012 Tomcat 8.5 MySQL 5.7环境下稳定运行三年零故障。如果你正被毕业设计卡在“功能完整但技术陈旧”的矛盾里或者需要一套可快速二次开发、能塞进教务系统API接口、带完整角色权限链路的Java Web基座这个项目就是你该拆开的第一份源码包。它不炫技但每行代码都带着教室签到、请假审批、辅导员复核的真实业务逻辑——比如学生点击“开始签到”时后端会校验当前课程是否在课表时间窗内、该班是否已开启考勤、同一节课是否重复签到比如教师端导出的Excel考勤报表字段顺序和教务处Excel模板完全对齐。新手能照着跑通熟手能一眼看出SSM各层职责切分是否干净、Layui表格渲染有没有内存泄漏隐患、MyBatis动态SQL是否做了防注入处理。2. 从源码结构到模块职责看清SSM三层如何协作完成一次“学生签到”这个项目不是靠IDEA一键生成的Maven骨架而是典型的手动分层SSM工程目录结构清晰得像教科书——但恰恰是这种“教科书式”结构暴露了真实项目中常被忽略的耦合点。我们先从src/main/java下的Controller层切入再一层层剥开Service和Mapper最后落到JSP页面如何与Layui组件联动。2.1 Controller层四个角色控制器的命名规律与职责边界你看到的TeacherBjController.class、ManageController.class、RecordController.class、TeacherController.class、SignController.class表面看是类名重复如两个TeacherBjController.class实则是编译产物——源码里对应的是TeacherBjController.java教师-班级管理、ManageController.java管理员总控、RecordController.java考勤记录查询、TeacherController.java教师个人事务、SignController.java核心签到流程。每个Controller只做三件事接收HTTP请求参数、调用Service层方法、返回ModelAndView或JSON响应。例如SignController.java中的studentSign()方法RequestMapping(value /studentSign, method RequestMethod.POST) ResponseBody public MapString, Object studentSign(HttpServletRequest request, HttpServletResponse response) { String stuId (String) request.getSession().getAttribute(stuId); String classId request.getParameter(classId); String courseId request.getParameter(courseId); // 校验参数非空 if (StringUtils.isEmpty(stuId) || StringUtils.isEmpty(classId) || StringUtils.isEmpty(courseId)) { return ResultUtil.error(参数缺失); } // 调用Service执行签到逻辑 int result signService.studentSign(stuId, classId, courseId); if (result 1) { return ResultUtil.success(签到成功); } else if (result -1) { return ResultUtil.error(该课程未开启考勤); } else if (result -2) { return ResultUtil.error(您已签到请勿重复操作); } else { return ResultUtil.error(签到失败请重试); } }注意这里没有用RequestParam注解而是手动从request取参——这是JSP时代遗留的兼容性写法好处是参数类型转换失败时不会抛400错误坏处是丢失了Spring MVC的自动类型绑定能力。如果你要升级建议逐步替换为RequestBody接收JSON对象但必须同步改掉所有JSP表单的enctypeapplication/x-www-form-urlencoded。2.2 Service层事务边界与业务校验的落地位置SignService.java是整个考勤流程的中枢。它用Transactional标注在实现类上而非接口确保studentSign()方法内所有数据库操作要么全成功、要么全回滚。关键逻辑不在SQL里而在Java判断中Transactional public int studentSign(String stuId, String classId, String courseId) { // 1. 检查课程是否在有效考勤时段内对比当前时间与课表start/end Course course courseMapper.selectByCourseId(courseId); if (course null) return -1; LocalDateTime now LocalDateTime.now(); if (now.isBefore(course.getStartTime()) || now.isAfter(course.getEndTime())) { return -1; // 课还没开始或已结束 } // 2. 检查该班该课是否已开启考勤status1 SignConfig config signConfigMapper.selectByClassAndCourse(classId, courseId); if (config null || config.getStatus() ! 1) { return -1; } // 3. 检查学生是否已签到防止重复提交 SignRecord exist signRecordMapper.selectByStuAndClassAndCourse(stuId, classId, courseId); if (exist ! null) { return -2; } // 4. 插入新签到记录 SignRecord record new SignRecord(); record.setStuId(stuId); record.setClassId(classId); record.setCourseId(courseId); record.setSignTime(now); record.setStatus(1); // 1正常签到 signRecordMapper.insert(record); return 1; }这段代码揭示了SSM项目中最容易被忽视的“业务校验前置”原则数据库约束如唯一索引只能防住并发插入冲突但无法校验“课还没开始”“考勤未开启”这类业务规则。这些必须在Service层用Java逻辑判断否则前端会收到模糊的SQL异常。2.3 Mapper层MyBatis动态SQL与防注入的实操细节SignRecordMapper.xml里的SQL不是简单CRUD而是带条件拼接的动态查询。以selectByStuAndClassAndCourse为例select idselectByStuAndClassAndCourse resultTypecom.example.entity.SignRecord SELECT * FROM sign_record WHERE stu_id #{stuId} AND class_id #{classId} AND course_id #{courseId} AND status 1 /select注意两点第一参数全部用#{}而非${}这是防SQL注入的生命线第二status 1硬编码在SQL里而不是传参——因为“已签到”状态值是业务常量不应由前端控制。再看更复杂的selectSignList教师端查看本班考勤列表它支持按日期、状态、学生姓名多条件筛选select idselectSignList resultTypecom.example.entity.SignRecord SELECT s.*, st.stu_name, c.class_name, co.course_name FROM sign_record s LEFT JOIN student st ON s.stu_id st.stu_id LEFT JOIN class c ON s.class_id c.class_id LEFT JOIN course co ON s.course_id co.course_id WHERE 11 if teststartDate ! null and startDate ! AND s.sign_time #{startDate} /if if testendDate ! null and endDate ! AND s.sign_time #{endDate} /if if teststatus ! null AND s.status #{status} /if if teststuName ! null and stuName ! AND st.stu_name LIKE CONCAT(%, #{stuName}, %) /if ORDER BY s.sign_time DESC /select这里if标签是MyBatis动态SQL的核心CONCAT(%, #{stuName}, %)比%#{stuName}%更安全避免MySQL版本兼容问题且LIKE查询已建好联合索引idx_stu_name_status_sign_time——这些细节在schema.sql里有明确注释但新手常忽略索引存在导致查询慢。2.4 JSPLayui前端如何把SSM数据变成可操作的考勤表格/WEB-INF/jsp/teacher/sign_list.jsp是教师端考勤列表页。它没用Vue或React而是纯JSP脚本Layui table组件。关键在于>table idsignTable lay-filtersignTable/table script typetext/javascript layui.use([table, form], function(){ var table layui.table; var form layui.form; table.render({ elem: #signTable, url: ${pageContext.request.contextPath}/record/list, // 对应RecordController.list() page: true, cols: [[ {field:stu_name, title:学生姓名, width:120}, {field:class_name, title:班级, width:150}, {field:course_name, title:课程, width:180}, {field:sign_time, title:签到时间, width:180, sort: true}, {field:status, title:状态, width:100, templet: #statusTpl}, {title:操作, width:160, align:center, toolbar: #barDemo} ]] }); }); /script !-- 状态列模板 -- script typetext/html idstatusTpl {{# if(d.status 1){ }} span stylecolor:#5FB878;已签到/span {{# } else if(d.status 0){ }} span stylecolor:#FFB800;请假中/span {{# } else { }} span stylecolor:#FF5722;缺勤/span {{# } }} /script这里templet: #statusTpl是Layui的模板引擎把数字状态转成带颜色的文字toolbar: #barDemo则定义右侧操作按钮如导出Excel、查看详情。所有数据都来自后端Controller返回的JSONJSP本身只负责静态结构——这正是JSP作为View层的正确用法不写Java逻辑只做展示。3. 数据库设计与初始化MySQL 5.7下必须踩的四个坑项目用MySQL 5.7但schema.sql脚本里藏着几个不写明就跑不通的隐性依赖。我当年在Mac上用Homebrew装的MySQL 8.0死活启动不了降级到5.7.44才解决根本原因就在这几处。3.1 字符集与排序规则utf8mb4才是真UTF-8schema.sql开头声明CREATE DATABASE attendance_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE attendance_system;注意不是utf8MySQL的utf8实际是utf8mb3最多存3字节字符不支持emoji和部分生僻汉字而utf8mb4才是真正的UTF-8。如果你用Navicat新建数据库时选了默认utf8后续插入学生姓名含“䶮”yǎn字就会报错Incorrect string value。解决方案只有两个要么删库重建要么执行ALTER DATABASE attendance_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;再逐个表执行ALTER TABLE xxx CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。3.2 时间字段类型datetime vs timestamp的时区陷阱所有含时间的表如sign_record、course都用datetime而非timestampCREATE TABLE sign_record ( id bigint(20) NOT NULL AUTO_INCREMENT, stu_id varchar(20) NOT NULL COMMENT 学生ID, class_id varchar(20) NOT NULL COMMENT 班级ID, course_id varchar(20) NOT NULL COMMENT 课程ID, sign_time datetime NOT NULL COMMENT 签到时间, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1-已签到0-请假-1-缺勤, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为什么不用timestamp因为timestamp会自动转换时区存UTC读本地时区而考勤时间必须绝对精确——教师在北京设置的“9:00-10:30”课表不能因服务器在纽约就变成“22:00-23:30”。datetime存的是字面值不涉及时区转换这才是教务系统的刚需。3.3 外键约束MySQL 5.7默认关闭必须手动启用schema.sql里所有外键如student.class_id引用class.class_id都写了FOREIGN KEY但MySQL 5.7默认foreign_key_checksOFF。如果初始化时没开插入数据会成功但外键形同虚设删班级时学生记录不会级联删除导致脏数据。验证方法执行SELECT foreign_key_checks;结果为0即关闭。开启命令SET FOREIGN_KEY_CHECKS 1; -- 或永久生效在my.cnf的[mysqld]段加一行 -- foreign_key_checks ON3.4 初始化数据admin用户密码是明文必须改init_data.sql里有一条INSERT INTO user VALUES (admin, admin, 管理员, 1, 2020-01-01 00:00:00);密码admin是明文生产环境必须改。SSM项目没集成Shiro或Spring Security密码校验在LoginController.java里硬编码if (admin.equals(username) admin.equals(password)) { // 登录成功 }血泪经验第一次部署到学校测试服务器被扫端口的爬虫爆破出admin账号幸好没开放外网。正确做法是用BCryptPasswordEncoder加密密码存入数据库登录时用passwordEncoder.matches(inputPassword, dbPassword)校验。补丁代码已整理好文末提供。提示init_data.sql里还埋了一个坑——role字段值是数字1管理员2教师3学生4辅导员但JSP页面显示时直接用c:when test${user.role 1}管理员/c:when没做枚举映射。如果后期要加角色必须同步改所有JSP和Controller建议抽成RoleEnum类。4. 部署与运行Tomcat 8.5 JDK 1.8的黄金组合及避坑指南这套系统在Tomcat 7.x也能跑但推荐8.5——因为JDK 1.8的Optional、Stream等特性在Tomcat 8.5的Servlet 3.1容器里支持最稳。我试过Tomcat 9.0.80结果Layui的laydate日期组件JS报错根源是Tomcat 9默认禁用了allowLinking导致JSP里link relstylesheet href${pageContext.request.contextPath}/static/layui/css/layui.css路径解析失败。4.1 JDK 1.8配置别信“其他版本理论上也可以”项目pom.xml里明确写着properties java.version1.8/java.version maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties如果你强行用JDK 17编译MyBatis的SelectProvider注解会报Method not found因为JDK 17移除了javax.xml.bind包而MyBatis 3.4.6本项目所用依赖它。解决方案只有两个降级JDK或升级MyBatis到3.5.10并排除jaxb-api依赖。但后者要改pom.xml二十多处不如直接用JDK 1.8省事。4.2 Tomcat启动context.xml里必须配JNDI数据源conf/context.xml不是摆设它定义了JNDI数据源spring-dao.xml里通过jee:jndi-lookup引用!-- conf/context.xml -- Resource namejdbc/attendance authContainer typejavax.sql.DataSource maxTotal100 maxIdle30 minIdle10 usernameroot password123456 driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/attendance_system?useUnicodetrueamp;characterEncodingutf8mb4amp;serverTimezoneAsia/Shanghai/注意serverTimezoneAsia/Shanghai——这是MySQL 5.7连接URL的强制要求漏了会报The server time zone value XXX is unrecognized。另外com.mysql.cj.jdbc.Driver是MySQL 8.0驱动类名但本项目用的是mysql-connector-java-5.1.47.jar适配5.7所以实际应写com.mysql.jdbc.Driver。翻车现场我曾把驱动jar放错lib目录层级Tomcat启动时报ClassNotFoundException查日志发现catalina.out里有Caused by: java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver立刻换回5.1.47版本。4.3 Maven依赖SSM版本锁死别乱升级pom.xml里SSM版本是精心匹配的dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version4.3.29.RELEASE/version !-- Spring 4.3.x 最终版 -- /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version1.3.2/version !-- MyBatis 3.4.6 Spring 4.3 兼容版 -- /dependency如果把Spring升级到5.xRequestMapping的params属性会失效如params!delete导致删除功能404如果把MyBatis升级到3.5.xforeach标签的collection属性必须从list改成param1所有XML都要重写。后悔药我备份了原始pom.xml每次想“优化”前先git checkout pom.xml。4.4 避坑 / 常见问题 / 排查 / 注意现象1登录后跳转404地址栏显示http://localhost:8080/WEB-INF/jsp/login.jsp原因JSP路径写错web.xml里welcome-file-list没配index.jsp或Controller返回的ModelAndView视图名没加前缀。解决检查LoginController.java中return new ModelAndView(login);确认spring-mvc.xml里InternalResourceViewResolver的prefix/WEB-INF/jsp/和suffix.jsp配置正确且login.jsp确实在/WEB-INF/jsp/目录下。现象2Layui表格显示“数据接口请求异常”浏览器F12看Network返回500原因RecordController.list()方法里PageHelper.startPage()分页插件没生效或signRecordMapper.selectSignList()SQL里if条件拼错导致语法错误。解决先注释掉PageHelper.startPage()直接查全表看是否报错再检查mybatis-config.xml是否启用了分页插件plugin interceptorcom.github.pagehelper.PageInterceptor/最后用Navicat执行selectSignList对应的SQL看能否跑通。现象3学生签到成功但教师端查不到记录数据库sign_record表里sign_time是0000-00-00 00:00:00原因MySQL 5.7严格模式STRICT_TRANS_TABLES开启时datetime字段插入空字符串会转成零值而MyBatis的LocalDateTime类型没配置DateTimeFormat注解。解决在SignRecord.java的signTime字段上加DateTimeFormat(patternyyyy-MM-dd HH:mm:ss)并在spring-mvc.xml里启用mvc:annotation-driven /或关掉MySQL严格模式不推荐。现象4修改密码后再次登录提示“密码错误”但数据库里密码字段已更新原因密码是明文存储但LoginController.java里校验逻辑写成了if (user.getPassword().equals(MD5Util.md5(password)))而初始化数据是明文admin没走MD5。解决统一用BCrypt加密。在UserService.java里加BCryptPasswordEncoder passwordEncoder new BCryptPasswordEncoder();注册时user.setPassword(passwordEncoder.encode(inputPassword));登录时passwordEncoder.matches(inputPassword, user.getPassword())。现象5Tomcat启动后访问http://localhost:8080显示404但http://localhost:8080/attendance_system能进原因项目没设为ROOT应用pom.xml里finalName是attendance_systemTomcat部署时生成的war包名就是attendance_system.war。解决两种方案① 把pom.xml的finalName改成ROOT重新打包② 在Tomcatconf/server.xml里Host节点下加Context path docBaseattendance_system /指向解压后的目录。5. 安全加固与二次开发给老项目装上现代防护盾这个项目没做任何安全加固直接上线等于裸奔。但好消息是SSM架构下加防护非常轻量——不需要重构只要在现有Spring配置里插几行Bean定义就能挡住90%的常见攻击。我把它拆成三个层次基础防护防SQL注入/XSS、权限强化RBAC细粒度控制、审计留痕谁在何时干了什么。5.1 防SQL注入不只是#{}还要加MyBatis拦截器#{}能防大部分注入但遇到ORDER BY动态字段如按sign_time或stu_name排序就必须用${}这就开了后门。解决方案是写一个MyBatis拦截器白名单校验排序字段Component Intercepts(Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class})) public class SqlInjectionInterceptor implements Interceptor { private static final SetString SAFE_ORDER_BY_FIELDS Set.of(sign_time, stu_name, class_name, course_name); Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler (StatementHandler) invocation.getTarget(); BoundSql boundSql statementHandler.getBoundSql(); String sql boundSql.getSql(); // 检查ORDER BY后是否为白名单字段 if (sql.toLowerCase().contains(order by)) { Pattern pattern Pattern.compile(order\\sby\\s(\\w), Pattern.CASE_INSENSITIVE); Matcher matcher pattern.matcher(sql); if (matcher.find()) { String field matcher.group(1); if (!SAFE_ORDER_BY_FIELDS.contains(field)) { throw new RuntimeException(非法排序字段 field); } } } return invocation.proceed(); } }然后在mybatis-config.xml里注册plugins plugin interceptorcom.example.interceptor.SqlInjectionInterceptor/ /plugins这样当有人在URL里传?orderBysign_time; DROP TABLE user--时拦截器会直接抛异常比数据库报错更早掐断攻击。5.2 XSS防护JSP输出必须fn:escapeXml()Layui模板也要过滤JSP里所有${}输出都要套fn:escapeXml()比如学生姓名% taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions % td${fn:escapeXml(student.stuName)}/tdLayui的templet模板也不能幸免。上面的statusTpl要改成script typetext/html idstatusTpl {{# if(d.status 1){ }} span stylecolor:#5FB878;{{ layui.util.escape(d.stu_name) }}/span {{# } }} /scriptlayui.util.escape()是Layui内置的XSS过滤函数比前端自己写正则更可靠。玄学时刻有次我忘了给d.stu_name加escape学生在姓名里输img srcx onerroralert(1)结果教师端表格一渲染就弹窗——这就是没过滤的代价。5.3 RBAC权限升级从硬编码角色ID到数据库驱动的权限表原项目权限靠user.role字段1/2/3/4硬编码判断扩展性差。我加了三张表sys_role角色、sys_permission权限、sys_role_permission关联表名字段说明sys_rolerole_id,role_name,description如“辅导员”、“任课教师”sys_permissionperm_id,perm_code,perm_name,url_pattern如sign:manage,/teacher/sign/*sys_role_permissionrole_id,perm_id角色-权限多对多然后写一个PermissionInterceptor在preHandle()里查当前用户角色拥有的所有url_pattern匹配请求路径public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String uri request.getRequestURI(); String contextPath request.getContextPath(); String path uri.substring(contextPath.length()); // 去掉上下文路径 // 查询用户权限列表 ListString perms permissionService.getUserPermissions((String) request.getSession().getAttribute(username)); // 检查path是否被任一权限pattern匹配 for (String pattern : perms) { if (pathMatcher.match(pattern, path)) { return true; // 放行 } } response.sendError(HttpServletResponse.SC_FORBIDDEN, 无权限访问); return false; }pathMatcher用Spring的AntPathMatcher支持/teacher/**、/admin/user/*等通配符。这样新增一个“教务员”角色只需在后台页面勾选权限不用改一行Java代码。5.4 操作审计记录每一次关键操作的“谁、何时、干了什么”考勤系统最怕纠纷——学生说“我签到了”教师说“没记录”。我在SignService.java的studentSign()方法里加了审计日志Autowired private AuditLogService auditLogService; Transactional public int studentSign(String stuId, String classId, String courseId) { // ...原有逻辑... // 记录审计日志 AuditLog log new AuditLog(); log.setOperator(stuId); log.setOperation(student_sign); log.setTarget(sign_record); log.setDetail(String.format(班级%s课程%s, classId, courseId)); log.setCreateTime(LocalDateTime.now()); auditLogService.insert(log); return 1; }AuditLog表结构很简单CREATE TABLE audit_log ( id bigint(20) NOT NULL AUTO_INCREMENT, operator varchar(20) NOT NULL COMMENT 操作人ID, operation varchar(50) NOT NULL COMMENT 操作类型, target varchar(50) NOT NULL COMMENT 操作目标表, detail text COMMENT 详情, create_time datetime NOT NULL COMMENT 创建时间, PRIMARY KEY (id), KEY idx_operator_time (operator,create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;有了这张表查“张三在2024-05-20 08:59:30签到”只需SELECT * FROM audit_log WHERE operator2021001 AND operationstudent_sign ORDER BY create_time DESC LIMIT 1——这就是教务处要的“证据链”。从那以后我每次接手老Java Web项目都强制走一遍这四步① 检查所有% %输出是否加escapeXml② 扫描所有${}动态SQL补拦截器③ 把硬编码角色ID换成RBAC表④ 给增删改操作加审计日志。不是为了炫技而是让系统在没人盯着的时候依然能守住底线。希望帮到你。本文还有配套的精品资源点击获取