☰
SpringBoot学生考勤管理系统:数据库设计与核心代码实现全解析
2026/10/1 3:47:52 网站建设 项目流程

1. 项目概述与核心需求拆解

1.1 为什么“学生考勤管理系统”是毕设/课设的常青树

学生考勤管理系统算得上是Springboot方向下最经典的选题之一。我在带毕业设计和课程设计的过程中,见过大量类似的题目,比如“基于Springboot的校园教职员工考勤管理系统”、“基于Springboot的班级考勤系统”等等,基本逻辑都一样:管理学生信息、记录出勤状态、统计异常情况。这个选题火不是没有道理的,它的业务边界清晰、数据模型不复杂、功能可扩展性强,最重要的是它覆盖了Web开发几乎所有的核心知识点:CRUD、关联查询、权限控制、表单校验、报表统计。

对于学生来说,选这个题目意味着你不需要花大量时间在“搞清楚业务到底要做什么”上,而是可以把精力集中在技术栈的运用和代码质量上。你只要把Springboot、MyBatis、MySQL这套组合玩明白,再配上合理的权限设计和统计功能,基本就是一个能打85分以上的项目。

从另一个角度说,考勤管理系统也是一个“下限很低、上限很高”的题目。简单做,就是几张表的增删改查;往深了做,可以加入人脸识别签到、钉钉/企微对接、Redis缓存考勤状态、Excel导入导出、定时任务自动生成缺勤记录等。同一个题目,不同的人做出来,深度可以差出几个量级。

1.2 标题里“附源码+文档”背后的真实需求

很多人看这类标题,第一反应是“哦,又是一个毕设项目”,但实际上“附源码+文档”这六个字才是真正的关键。源码意味着项目完整可运行,不是那种只能看不能跑的半成品;文档意味着有配套的毕业设计论文或课程设计报告,里面包含需求分析、数据库设计、核心代码说明、系统测试等内容。

我见过太多学生在答辩前一周才开始找项目,拿到手发现代码缺依赖跑不起来,或者文档里画的架构图和实际代码完全对不上。源码和文档不一致是这个领域最大的坑。真正靠谱的项目,源码里的表结构、字段命名、接口路径,必须和文档里的E-R图、数据字典、接口说明完全对应。所以你拿到任何一个“附源码+文档”的项目,第一步不是急着跑起来,而是对照检查源码和文档之间的对应关系。

对于想把这个项目吃透的同学,我的建议是不要把它当成“交差用的工具”,而是当成一个“练手素材”。你有源码,有文档,最好的使用方式是把源码当成参考答案,自己从零开始搭一遍,遇到卡住的地方再去看源码是怎么解决的。这个过程走完,你对Springboot的理解会有一个质的提升,而且答辩的时候你能扛住老师的追问——因为代码里的每个细节你都真正动过手。

1.3 这类项目适合谁,以及预期的学习收益

如果你正在准备毕业设计、课程设计,或者想找一个项目来巩固Springboot的知识体系,这个考勤系统非常适合你。

具体来说,你会在整个过程中接触到以下内容:

  • Springboot的核心机制:自动配置、starter依赖、内嵌Tomcat、配置文件多环境切换。
  • 数据访问层设计:MyBatis的Mapper接口与XML映射、动态SQL、联表查询。
  • 权限与登录认证:基于Session的登录拦截、基于JWT的Token认证、密码加密存储方案。
  • 前端集成:Thymeleaf模板引擎渲染、Layui或Bootstrap搭建管理界面、Ajax异步交互。
  • 数据统计与导出:考勤汇总统计SQL的编写、POI导出Excel报表。
  • 项目交付与文档撰写:从需求分析到数据库设计,从接口文档到测试报告。

这些技能点基本覆盖了Java后端初级开发岗位的日常需求。即便你以后不打算做学生考勤方向,这套项目带来的代码能力也能直接迁移到其他的管理系统类项目上。

2. 技术选型与系统整体设计思路

2.1 为什么Springboot是这类管理系统的第一选择

学生考勤管理系统这类业务——说白了就是增删改查 + 权限 + 统计——本质上不需要分布式、不需要高并发,它是一个典型的单体应用。那为什么选Springboot,而不是SSH(Struts + Spring + Hibernate)或者Spring MVC的XML配置时代?

核心原因就三个字:省配置。

在Springboot出现之前,搭建一个Spring MVC项目要写一大堆XML配置文件:web.xml配置DispatcherServlet、spring-mvc.xml配置组件扫描和视图解析器、spring-mybatis.xml配置数据源和SqlSessionFactory、还要配置事务管理。这一套流程对老开发者来说轻车熟路,但对刚刚接触企业级开发的学生来说,光是把环境跑通就可能劝退一拨人。

Springboot把这一切变成了“约定优于配置”。你只需要在pom.xml里引入spring-boot-starter-web,它会自动帮你把Tomcat、DispatcherServlet、JSON序列化组件全部拉进来;你只需要在application.yml里配好数据源三件套(URL、用户名、密码),MyBatis的SqlSessionFactory就能自动装配。这种开发体验对新手极其友好,同时背后的自动配置机制又能让你在深入理解后收获很多面试谈资。

再说生态。现在国内中小型公司的新项目,Springboot基本是默认选项。学过Springboot = 能很快融入主流技术栈,这个度对于找实习、找工作的帮助是实打实的。

2.2 技术栈组合的选型逻辑

我推荐的技术组合是:Springboot 2.x + MyBatis + MySQL + Thymeleaf + Layui,下面逐个说理由。

Springboot 2.x:为什么不建议直接上Springboot 3.x?因为3.x最低要求JDK 17,而且部分第三方组件(比如低版本的MyBatis starter、Druid连接池)的兼容性需要额外处理。对于毕业设计场景,稳定性压倒一切,2.7.x版本成熟、资料多、遇到问题搜索引擎一搜一大片,这才是“保毕业”风格的选择。

MyBatis:国内企业管理类项目中使用率极高,SQL写在XML里,便于DBA审查和优化。最重要的是MyBatis的动态SQL能力非常适合考勤统计这类需要按条件拼接查询的场景。你不用像用JPA那样去写复杂的Specification,直接在XML里判断条件即可。

MySQL:开源、免费、安装简单、资料丰富,学生机配置再差跑个MySQL 5.7或者8.0都毫无压力。

Thymeleaf:Springboot官方推荐的模板引擎,和Spring MVC的集成非常顺滑。相比JSP,Thymeleaf在HTML文件中直接写th:属性,前端开发时可以直接用浏览器打开静态页面预览效果,这一点体验好很多。

Layui:很多人可能更熟悉Bootstrap或者ElementUI,但Layui在国内管理系统后台中有一席之地,因为它自带表格、分页、弹窗、表单校验、日期选择等组件,且使用方式是“类jQuery调用”,对后端开发非常友好。你要做一个考勤管理后台,用Layui的table模块配合后端的分页接口,基本上不需要写太多前端代码就能实现一个能看的管理界面。

这个组合还有一个好处:如果你将来需要把前端技术栈升级为Vue,后端只需要把接口改成返回JSON,视图层整体换掉即可,后端的核心业务代码几乎不动。

2.3 系统模块划分与业务流程梳理

考勤管理系统看似简单,但模块划分是有讲究的。我见过很多学生的项目,把所有的Controller堆在一个包下,Service层只有一个类几百行代码怼到底,这虽然能跑,但代码非常臃肿,而且答辩时老师问“你的项目经历了哪些模块设计”你会回答得非常吃力。

合理的模块划分应该围绕角色和业务展开。本项目我建议拆成以下模块:

模块名称核心职责涉及角色
学生管理学生信息的增删改查、批量导入、学号检索管理员
教师管理教师信息的维护与账号分配管理员
课程管理课程信息、班级课程安排、授课教师绑定管理员、教师
考勤管理学生签到、教师确认、考勤状态维护教师、学生
请假管理学生提交请假申请、教师审批、状态流转学生、教师
统计报表按课程/班级/个人维度统计出勤率管理员、教师
系统管理用户登录、密码修改、权限拦截全体

业务流程上的核心链路有两条:

签到链路:学生登录系统 → 进入我的课程列表 → 选择今天的课程 → 点击签到 → 系统判断签到时间是否在有效范围内 → 写入考勤记录 → 状态标记为“正常/迟到”。

请假链路:学生提交请假申请 → 附带请假类型和事由 → 教师登录后看到待审批列表 → 通过/驳回 → 状态变更 → 请假通过后考勤记录自动标记为“请假”。

这两条链路要梳理清楚,尤其是状态流转的部分,这决定了你的数据库表设计和代码实现的质量。很多初学者在这个地方容易混乱:请假和考勤是不是同一张表?请假通过后要不要再生成考勤记录?我的建议是——考勤表和请假表分开,通过请假表的状态关联考勤记录的状态,而不是在考勤表里直接改“缺勤”为“请假”,因为审计追踪需要保留原始记录。

3. 数据库设计与核心表结构解析

3.1 表结构设计与字段规范

数据库设计是整个系统中最重要的部分。考勤系统的核心表包括:学生表、教师表、课程表、授课安排表、考勤记录表、请假申请表。为了保证文章的可操作性,我把核心表的建表思路说清楚。

学生表(t_student)

核心字段:id、student_no(学号)、name、gender、class_name(班级)、phone、password、create_time、update_time。

这里的student_no一定要设置唯一索引,因为学号是学生身份的唯一标识,后续考勤关联、统计查询全靠它。password字段建议初始化为统一的默认值,比如123456,用户首次登录后再修改,这是管理系统的通用做法,也方便老师帮忙重置密码。

课程表(t_course)

核心字段:id、course_name、course_code(课程编号)、teacher_id(授课教师)、class_name(授课班级)、weeks(上课周次)、start_time、end_time。

start_time和end_time非常重要,这是判断签到是否迟到、是否有效的核心依据。比如一门课的上课时间是14:00-15:40,你可以规定上课前30分钟到上课后15分钟之内签到都算正常,超过这个窗口则标记为迟到或无法签到。时间窗口规则是考勤系统的灵魂,这个规则必须在设计阶段就想清楚。

考勤记录表(t_attendance)

核心字段:id、student_id、course_id、attendance_date、status、sign_time、source。

这里的status建议用整型枚举:1正常、2迟到、3缺勤、4请假。source字段记录签到来源,是PC端还是移动端,这个字段不是必须的,但加上它会让系统看起来更完整,答辩时也是个加分项。

请假申请表(t_leave)

核心字段:id、student_id、course_id、leave_type(事假/病假)、reason、start_date、end_date、status(0待审批/1通过/2驳回)、audit_remark、create_time。

请假表与考勤表之间的关联逻辑是:请假通过后,系统自动在考勤记录表中插入一条status为4的记录,或者由定时任务扫描缺勤记录,发现该学生在对应日期有通过状态的请假申请,则将缺勤变更为请假。前者逻辑简单但冗余度高;后者逻辑复杂但记录更干净。考虑到学生请假往往是多天的,我推荐在考勤查询时通过JOIN请假表判断状态,而不是事后回填考勤表。

3.2 为什么考勤状态用整型而不是字符串

这里聊一个很多初学者没有注意到但很关键的细节:考勤状态字段用什么类型。

我见过有人用varchar存中文,比如“正常”、“迟到”、“缺勤”。这种做法在开发初期看起来直观,但实际使用中全是坑:

  • 前端需要回显中文的时候还得手动做映射,因为数据库存的是“正常”,代码里判断的时候也要写if ("正常".equals(status)),代码里全是中文字符串。
  • 统计SQL写起来很麻烦,SUM(CASE WHEN status = '正常' THEN 1 ELSE 0 END),万一有人不小心把“正常”写成“正常 ”(带空格),统计结果直接出错。
  • 后期如果做国际化,这表基本废了。

用整型枚举的好处是显而易见的:存储空间小,查询比较快,代码里可以写常量定义如public static final int STATUS_NORMAL = 1;。前端展示的时候,后端返回一个Map,把整型映射为对应的中文描述,或者干脆在前端用一个字典来翻译。

这一点虽然小,但却是区分“赶工项目”和“认真项目”的标志之一。答辩的时候老师也会看你的代码风格规不规范。

3.3 统计查询中的SQL技巧

考勤系统的统计功能是硬需求,你要能回答出“某学生这个学期缺勤几次”“某门课程的平均出勤率是多少”这样的问题。

这里分享几个最核心的SQL写法。

按学生统计缺勤次数:

SELECT s.student_no, s.name, COUNT(CASE WHEN a.status = 3 THEN 1 END) AS absence_count, COUNT(CASE WHEN a.status = 2 THEN 1 END) AS late_count, COUNT(CASE WHEN a.status = 1 THEN 1 END) AS normal_count FROM t_attendance a LEFT JOIN t_student s ON a.student_id = s.id WHERE a.course_id = #{courseId} AND a.attendance_date BETWEEN #{startDate} AND #{endDate} GROUP BY s.id ORDER BY absence_count DESC

按课程统计出勤率:

SELECT c.course_name, COUNT(a.id) AS total_count, SUM(CASE WHEN a.status = 1 OR a.status = 2 THEN 1 ELSE 0 END) AS present_count, CONCAT(ROUND(SUM(CASE WHEN a.status = 1 OR a.status = 2 THEN 1 ELSE 0 END) * 100.0 / COUNT(a.id), 2), '%') AS attendance_rate FROM t_attendance a LEFT JOIN t_course c ON a.course_id = c.id GROUP BY a.course_id

编写统计SQL时要特别注意的坑:CASE WHEN的写法里,ELSE的分支别漏了;ROUND函数计算百分比时要用100.0而不是100,否则整数除法会直接把小数吃掉;多表JOIN时注意筛选条件放在WHERE还是ON里的语义差异。

这些SQL量不大,但它们是考勤系统统计模块的核心,也是答辩时老师最常追问的地方。建议准备项目的时候把这些SQL的执行结果截图存档,放到文档的测试章节里,会显得你的项目测试非常充分。

4. 核心模块实现与重点项目代码

4.1 环境准备与项目初始化流程

开始编码之前,先把环境准备好。我建议的版本组合是:JDK 1.8或JDK 11(二选一,看学校机器的情况)、Maven 3.6+、MySQL 5.7+、IntelliJ IDEA。

创建Springboot项目时,你可以通过IDEA自带的Spring Initializr去创建,选择Spring Web、Thymeleaf、MyBatis Framework、MySQL Driver这几个依赖。有了初始骨架之后,还需要在pom.xml里手动补充一些常用依赖,比如Druid连接池、Lombok、PageHelper分页插件。

Druid连接池特别推荐在大作业项目中使用,因为Druid不仅是一个连接池,还自带了监控页面,配置好之后访问/druid可以看到数据源的实时监控信息、SQL执行详情。答辩的时候现场打开Druid监控页展示SQL执行统计,老师会觉得你的项目“有章法”。

acidbase的pom.xml核心依赖配置如下:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>

这里我要特意说说PageHelper这个插件。它是国内用得极多的MyBatis分页插件,用法极其简单:在查询前调用PageHelper.startPage(pageNum, pageSize),紧跟着的查询会自动被改写成分页SQL,返回结果用PageInfo包装即可。考勤列表、学生列表这些需要分页显示的功能,有了它效率直接翻倍。

配置文件application.yml中几个容易踩坑的点:URL要加useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,注意serverTimezone不配可能会报时区错误;mybatis.mapper-locations要指向Mapper XML文件所在的目录;mybatis.configuration.map-underscore-to-camel-case=true可以让数据库字段user_name自动映射为Java属性userName,省去写别名的时间。

4.2 登录认证与权限拦截的实现

考勤系统有三种角色:管理员、教师、学生。三种角色的权限边界不同:管理员可以访问所有管理功能,教师可以管理自己授课课程的考勤,学生只能查看自己的考勤和提交请假申请。

实现角色权限有两种常用方案:Spring Security和HandlerInterceptor拦截器。这里我推荐用拦截器方案,原因很简单——Spring Security功能强大但学习曲线陡峭,配置不当还会把你绕晕;拦截器逻辑简单清晰,自己写一遍反而更能理解HTTP请求流转的流程。

核心实现思路是这样的:

  1. 定义一个AuthInterceptor,实现HandlerInterceptor接口。
  2. 在preHandle方法中,从Session中取出当前登录用户信息。
  3. 判断用户是否为空,为空则重定向到登录页面。
  4. 进一步判断当前请求的路径是否匹配当前用户角色的访问权限。

权限判断这块,可以在每个Controller方法上用自定义注解标记所需的角色,也可以简单地在拦截器里判断路径前缀。对于学生考勤这种角色明确的项目,拦截器里通过路径前缀来控制就足够了:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect("/login"); return false; } String uri = request.getRequestURI(); if (uri.startsWith("/admin") && !"ADMIN".equals(loginUser.getRole())) { response.sendRedirect("/unauthorized"); return false; } if (uri.startsWith("/teacher") && !"TEACHER".equals(loginUser.getRole()) && !"ADMIN".equals(loginUser.getRole())) { response.sendRedirect("/unauthorized"); return false; } return true; }

密码存储一定要用BCrypt加密,不要用MD5。MD5已经被彩虹表打得体无完肤了,而且Spring Security的crypto模块中自带BCryptPasswordEncoder,直接调用即可。Springboot项目引入spring-boot-starter-security后默认会开启安全拦截,反而增加复杂度,我建议不引入这个starter,只单独引入spring-security-crypto这个轻量组件用于密码加密,这样既不会干扰项目,也实现了安全存储。

4.3 学生签到的核心逻辑实现

签到是考勤系统的灵魂功能。核心流程是:学生点击签到按钮,后端判断当前时间是否在签到有效窗口内,然后插入一条考勤记录。

首先需要从登录用户的Session中取出学生ID:

Student student = (Student) session.getAttribute("loginUser"); Long studentId = student.getId(); Long courseId = Long.valueOf(request.getParameter("courseId")); Date now = new Date();

然后是签到逻辑的核心部分,判断是否迟到:

Course course = courseService.getById(courseId); Date startTime = course.getStartTime(); Date endTime = course.getEndTime(); String status; if (now.before(startTime)) { // 上课前签到,但系统窗口尚未开启 return error("签到尚未开始"); } else if (now.after(startTime) && now.before(DateUtil.addMinutes(startTime, 15))) { status = "1"; // 正常 } else if (now.after(DateUtil.addMinutes(startTime, 15)) && now.before(endTime)) { status = "2"; // 迟到 } else { return error("已超过签到时间"); }

这里有个细节值得解释:为什么上课前不能签到?从业务上说,老师还没开始上课,学生都还没到教室,签到的意义不大。从技术上说,设置签到有效窗口可以有效避免学生在家就把考勤签了的情况。当然,如果你希望更灵活,可以放宽为“上课前30分钟即可签到”,具体看你系统的需求文档怎么写。

签到前还要做去重判断,防止同一学生同一课程同一日期重复签到:

AttendanceExample example = new AttendanceExample(); example.createCriteria() .andStudentIdEqualTo(studentId) .andCourseIdEqualTo(courseId) .andAttendanceDateEqualTo(DateUtil.today()); long count = attendanceMapper.countByExample(example); if (count > 0) { return error("您今日已签到"); }

这套逻辑写完之后,签到的状态流转就清晰了。学生每次签到,系统都会记录签到时间和状态,而统计报表直接查这些记录即可。

4.4 请假审批的状态流转

请假模块考察的是状态机的设计能力。学生提交请假后,数据进入t_leave表,status为0(待审批)。教师端展示待审批列表,点击同意或驳回时,将status更新为1或2。

教师审批的代码逻辑大致如下:

@Transactional public void auditLeave(Long leaveId, Integer auditStatus, String auditRemark) { Leave leave = leaveMapper.selectById(leaveId); if (leave == null) { throw new RuntimeException("请假记录不存在"); } if (!leave.getStatus().equals(0)) { throw new RuntimeException("该申请已处理"); } leave.setStatus(auditStatus); leave.setAuditRemark(auditRemark); leave.setAuditTime(new Date()); leaveMapper.updateById(leave); // 审批通过后,处理关联的考勤记录 if (auditStatus.equals(1)) { attendanceService.markAsLeave(leave.getStudentId(), leave.getCourseId(), leave.getStartDate(), leave.getEndDate()); } }

这里一定要加@Transactional事务注解,因为审批操作涉及更新请假表、更新考勤表两步操作,如果第二步失败而第一步成功,会导致数据不一致。

markAsLeave方法的职责是查询该学生在这个课程、这段时间范围内的考勤记录,如果存在缺勤记录,将其更新为请假状态;如果没有缺勤记录,就插入一条请假状态的考勤记录。这一步是在保证考勤统计的完整性。

我自己在写这段代码时踩过一个坑:在auditLeave方法内部调用attendanceService.markAsLeave时,如果LeaveMapper和AttendanceMapper操作的是同一个数据库连接,事务是可以正确生效的;但如果用了不同数据源或者没有配置事务管理器,就可能出现主表更新成功、关联表更新失败的情况,最终统计数据对不上。排查这个问题的思路就是检查事务配置和Mapper的代理对象是否纳入Spring容器管理。

4.5 前端页面实现与Ajax交互

前端部分用Thymeleaf + Layui来组合。

Thymeleaf负责页面渲染,比如学生列表页student-list.html:

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>学生管理</title> <link rel="stylesheet" th:href="@{/layui/css/layui.css}"> </head> <body> <table id="studentTable" lay-filter="studentTable"></table> <script th:src="@{/layui/layui.js}"></script> <script> layui.use(['table', 'layer'], function() { var table = layui.table; table.render({ elem: '#studentTable', url: '/admin/student/list', page: true, cols: [[ {field: 'studentNo', title: '学号', width: 120}, {field: 'name', title: '姓名', width: 100}, {field: 'className', title: '班级', width: 150}, {field: 'phone', title: '联系电话', width: 150}, {width: 180, title: '操作', templet: '#barDemo'} ]] }); }); </script> </body> </html>

后端对应接口返回Layui规定的JSON格式,必须包含code、msg、count、data四个字段:

@GetMapping("/admin/student/list") @ResponseBody public LayuiResult studentList(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer limit, String keyword) { PageHelper.startPage(page, limit); List<Student> students = studentService.queryWithKeyword(keyword); PageInfo<Student> pageInfo = new PageInfo<>(students); LayuiResult result = new LayuiResult(); result.setCode(0); result.setMsg("查询成功"); result.setCount(pageInfo.getTotal()); result.setData(pageInfo.getList()); return result; }

这种前后端联调的方式,对不擅长写前端的后端开发人员非常友好。你不需要掌握复杂的Vue状态管理,只需要理解HTTP请求和JSON数据结构即可。

5. 常见问题与排查技巧实录

5.1 环境与依赖类问题

Could not find resource com/xxx/mapper/StudentMapper.xml

这个错误几乎是必踩的。原因是Maven默认不编译src/main/java目录下的XML文件,所以Mapper XML放在Java包目录下时不会被导出到target目录。解决方案是在pom.xml的build节点中配置resources:

<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources>

当然,更标准的做法是把Mapper XML放在src/main/resources/mapper目录下,然后通过mybatis.mapper-locations=classpath:mapper/*.xml来指定。我更推荐后者,因为更符合Maven的目录约定,且不会出现上述编译问题。

Whitelabel Error Page 404

出现这个页面的原因有几种。最常见的是静态资源路径配置不对,比如访问/admin/student时,Controller里没有对应的@GetMapping映射。排查思路是:先看控制台日志是否打印了对应的请求映射,如果打印了但还是404,可能是静态资源被拦截了;如果压根没打印,说明Controller没有被扫描到。Controller扫描问题,检查启动类上的@SpringBootApplication注解所在包是否比Controller所在的包层级高。

数据库中文乱码

工程配置文件中JDBC URL里漏了characterEncoding=utf8,数据库表本身的字符集也不是utf8mb4。解决方法是连接串加上参数,同时建库时指定字符集:CREATE DATABASE attendance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;。

5.2 业务逻辑类问题

学生签到时提示“已签到”,但考勤记录表中查不到记录。这种情况通常是Session中存储的学生ID出了问题,比如Session中存的不是当前登录的学生,而是上一轮测试残留的用户信息。排查时先打印Session中的用户ID,再检查登录逻辑是否正确更新了Session。

考勤统计报表中出勤率超过100%。这个问题几乎都是因为JOIN出现了数据膨胀,比如考勤表关联了多条请假记录,导致COUNT数量重复统计。排查思路是去掉统计SQL中的多余JOIN条件,给COUNT子查询加DISTINCT,或者使用子查询替代JOIN。

请假审批通过后,考勤记录没有自动更新为“请假”。先检查事务是否生效,再检查markAsLeave方法的日期比较逻辑,看是否因为日期格式化问题导致状态更新条件没有命中。

5.3 文档撰写与答辩准备的建议

这个项目附带的文档一般包括:需求分析、系统设计、数据库设计、核心代码说明、系统测试、总结。这里分享几个让文档加分的细节:

数据库设计章节:E-R图必须画,推荐用draw.io在线绘制,画完导出PNG放到Word里。数据字典表格列清楚字段名、类型、说明、是否主键、是否外键,有条理的数据字典非常加印象分。

核心代码说明章节:不要贴大段完整代码,而是贴关键代码片段并配上文字解释。比如贴签到判断逻辑的代码,然后解释时间窗口规则的设计思路,这样老师一眼就能看出你理解自己在写什么。

测试章节:不要只写“测试通过”四个字,要写测试用例设计。比如正常签到测试、迟到签到测试、重复签到测试、请假审批通过后考勤变更测试,每个用例写明输入、预期结果、实际结果、是否通过。有了这些内容,文档的测试章节就非常丰富且真实。

答辩时被问到最多的问题就集中在几个地方:考勤状态为什么用整型存、时间窗口规则是怎么定的、跨天课程怎么处理、统计SQL的准确率如何保证。这些问题在你动手写过代码之后基本都能从容回答。

5.4 如何从“能跑”到“答辩能讲”

很多同学的项目处于“能跑”状态,但一旦老师问“你这里为什么要加这个索引”“你的统计报表的SQL执行效率怎么样”,就语塞了。这里分享一个简单有效的准备方式:给项目中的每个类、每个方法,准备一个“为什么要这样写”的解释。

举几个高频问题的应对思路作为参考:

老师问:“为什么使用MyBatis而不是MyBatis Plus?”你可以回答:“本项目主要用了XML形式的Mapper,因为考勤统计涉及多表联查和动态SQL,XML写起来更直观,而且SQL的可读性和优化空间更大。MyBatis Plus虽然适合单表CRUD,但复杂的统计查询最终还是要写SQL,所以我选择了原生MyBatis。”

老师问:“考勤记录为什么要单独建表,不能直接写在学生表里?”你可以回答:“因为一个学生对应多门课程、多次考勤,这是一对多的关系,如果直接冗余在学生表里,数据会出现大量重复,也不符合数据库第三范式。单独的考勤表可以按课程、按日期灵活查询。”

老师问:“如果同一个学生同时提交了两个请假申请,都通过了,会有什么问题?”你可以说:“我在审批时通过status != 0判断是否已处理,防止重复审批;而在考勤更新时,会先删除该时间段内现有的请假状态考勤记录,再插入新的记录,保证数据幂等。”

这样的问答准备多了,你就不会紧张,真正理解了项目的每个角落,答辩就变成了一个分享的过程,而不是被动挨打的过程。

6. 源码二次开发的可扩展方向

6.1 从考勤到更多业务场景的扩展

如果你不满足于把这套系统当成一个毕业设计交差,而是希望它后续能变成一个有实际价值的工具,这里有几种低成本高价值的扩展方向:

接入钉钉/企微通知:考勤统计完成后,通过钉钉机器人Webhook推送迟到、缺勤名单给辅导员。实现难度不高,只需要在项目里引入一个HTTP客户端,调用Webhook地址发送JSON消息即可。这个功能一旦上线,系统的实用性直接提升一个档次。

二维码签到:教师端生成当次课程专属二维码,学生扫码完成签到。后端只需要把签到接口改造成接收课程ID和学生ID即可,二维码可以用自带的工具生成,不需要依赖第三方。

人脸识别签到:这个方向比较重,需要接入OpenCV或者第三方人脸识别SDK,但如果你把这种功能作为扩展写进论文的“系统展望”或者“后期规划”章节,会让论文的高度显得不一样。

Excel导入导出:管理员手动录入学生信息太麻烦,很多老师手里本来就有班级花名册Excel,支持上传Excel解析并批量导入学生数据,同时考勤统计结果可以导出为Excel报表。引入POI依赖,代码量不大但实用性极高。

我最推荐的是Excel导入导出和二维码签到,因为这两个功能实现成本低,但演示效果很好。答辩现场给老师展示一下“一键导入100条学生记录”,这个画面感比单纯展示增删改查强得多。

6.2 从一个项目到一套完整的前后端分离方案

如果你打算在这套考勤系统的基础上学习更多主流技术,可以考虑逐步演进:

  • 把前端从Thymeleaf+Layui替换为Vue3 + Element Plus,后端不变,只增加JSON返回接口。
  • 引入Redis缓存课程信息和考勤配置,减少数据库的查询压力。
  • 把单体重构为按模块拆分的多模块工程,这个对你理解Maven多模块开发有帮助。
  • 用Docker部署整个系统,数据库、后端、前端各跑一个容器。

这些演进路线不是毕业设计阶段必须做的,但如果你有长期学习和上线的打算,它们比重新做一个新项目更有价值。因为你已经对业务足够熟悉,每一次技术升级都只需要关注技术本身的变化,而不用分心去理解业务。

6.3 代码质量与协作习惯的养成

最后聊聊代码习惯。源码这块,我强烈建议你从一开始就做好几件事:

  • Git管理:哪怕是你一个人开发也要用Git。每一次功能完成就提交一次,commit信息写清楚,比如“feat: 新增学生批量导入功能”、“fix: 修复签到时间窗口判断逻辑”。这个习惯以后进入团队工作会非常受益。
  • 统一异常处理:用@RestControllerAdvice注解做全局异常捕获,统一返回错误格式。这个看似不是核心功能,但答辩时老师问你“你的系统是怎么返回错误信息的”,你就可以自豪地说“我们有全局异常处理机制”。
  • 日志规范:使用Slf4j记录关键操作日志,比如登录、签到、审批这种敏感操作,记录操作人、操作时间、操作内容。这既是数据审计的需要,也是系统健壮性的体现。

这些习惯看起来是“额外的要求”,但它们才是真正让你区别于其他做毕业设计的人的东西。大部分同学的代码能跑,但不规范;如果你能做到既正常跑、又代码整洁规范,那么整个项目交付给你的不仅仅是学分,还有可以写进简历的项目经验。

我在实际带项目的过程中最深的体会是:学生考勤管理系统这样的经典项目,最大的价值不在于“它是一个考勤系统”,而在于它是一个完整的、覆盖全栈的、可以反复练习和改造的练习场。源码和文档是起点,不是终点。把别人的代码跑通,再改成自己的项目,再优化成更好的设计,这个过程走完之后,你收获的远不止一个“毕设通过”的结果。

如果你选择用它作为毕业设计,希望这篇拆解能帮你少踩几个坑;如果你只是把它当作练手项目,那我建议你重点去看数据库设计和统计SQL的部分,这两个地方是考勤系统的核心内容,也是你以后在做报表类功能时能复用最多的知识。

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

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

立即咨询