☰
Spring Boot学生考勤管理系统:数据库设计、JWT鉴权与出勤统计
2026/10/6 5:15:51 网站建设 项目流程

简介:基于springboot开发的学生考勤管理系统,面向高校计算机专业毕业设计、课程设计及Web开发者,聚焦课堂签到、请假审批与考勤统计等场景。系统支持学生注册登录,管理员可维护学生、教师、班级、课程等信息,并完成签到、考勤、请假及统计业务,功能划分清晰,整体贴近管理类毕业设计的常见功能要求。资源包共426个文件,大小约9.31MB,核心是107个Java后端源码与43个前端页面,另含数据库脚本、配置文件、批处理脚本及图标图片等,内置安装、运行、构建脚本并附环境与依赖配置说明,便于读者快速完成部署与启动。已有694人学习下载,读者可获得一套完整可运行的考勤系统案例,参考多角色登录鉴权、考勤统计模块的编码思路,也能学习springboot整合前端框架的后台工程组织方式,对完成毕业设计或积累实际项目经验具有直接帮助。

1. 基于Spring Boot的学生考勤管理系统到底解决什么问题

学生考勤管理系统是Spring Boot入门到实战之间最常见的过渡项目,也是毕业设计里出现频率最高的题目之一。你拿到的这份源码加数据库脚本,解压之后一般就是一套标准的单体Web应用:前端页面、Controller接口、Service业务、Mapper数据层,再加一份MySQL初始化脚本。它的核心业务没有多复杂,就是学生选课后按课表打卡签到、签退,系统自动判定迟到早退缺勤,最后按月或按课程统计出勤率。

但真正动手跑完这套项目的人都知道,出勤率算得对不对、重复签到拦不拦得住、时间口径统一不统一,才是这套系统的真正难点。本文从数据库设计讲到JWT登录、签到回算和统计SQL,再把常见的坑一条条列出来,适合刚拿到源码还没跑起来的人,也适合想把考勤逻辑做严谨的开发者。

2. 数据模型设计与数据库初始化:考勤状态用字段钉死而不是用逻辑猜

2.1 五张核心表:从考勤流程推导表结构

考勤系统的表结构并不需要很多张表,常见的做法是围绕"谁、在哪个时间、上什么课、打没打卡"来建模。我一般会在初始化脚本里至少准备五张表:学生表、课程表、选课关系表、考勤记录表、请假申请表。再加一张系统用户表用于登录认证。

学生表负责学号和班级信息,课程表负责课程名称和起止时间,选课关系表解决学生和课程的多对多关系。考勤记录表是整套系统的核心,每条记录对应一个学生在一门课的某一天的某一个节次。请假申请表则单独存放请假申请和审批状态,因为请假是一个有状态的流程,不适合直接塞进考勤记录里。

这里有一个新手常犯的误区:想把请假直接做成考勤记录里的一个状态字段,导致请假被驳回后还要去改考勤记录。实际上请假应该先走申请审批流程,审批通过后再把对应时间的考勤记录标记为"请假"。这样统计出勤率时,请假既不进分子也不进分母,逻辑才干净。

2.2 考勤记录表与状态机:0/1/2/3/4五态

考勤记录表里最重要的字段就是status,它决定了这条记录是正常、迟到、早退还是缺勤。很多人喜欢直接在数据库里存"正常""迟到"这样的中文字符串,临时跑通没问题,但后面做统计、做图表、做接口返回时要反复转换,还容易写错。更稳妥的做法是用TINYINT存状态码,代码里用枚举或者常量去对应。

我习惯把这套状态机定成:0代表正常,1代表迟到,2代表早退,3代表缺勤,4代表请假。这样在SQL里用SUM和CASE WHEN做聚合非常顺手,比如统计缺勤人次就是SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END)。状态码本身不是录制出来的,而是根据签到时间和签退时间自动回算出来的,所以数据库字段只需要存时间,不用存"是否迟到"这种冗余判断。

状态码设计还有一个好处:以后如果业务需要区分"迟到且早退"这种组合异常,可以在代码判定逻辑里定义优先级,而不需要改表结构。判定优先级常见做法是缺勤最高,早退其次,迟到再次,全部没有异常才是正常。

2.3 建表SQL与唯一约束:防重复签到从数据库层做起

拿到源码后,第一步不是急着启动,而是先看数据库脚本能不能在本地MySQL里跑起来。下面是考勤核心表和选课关系表的建表SQL,注释里标注了关键设计点。

-- 初始化脚本,MySQL 8.x,库名:student_attendance CREATE TABLE t_student ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', student_name VARCHAR(50) NOT NULL COMMENT '姓名', class_name VARCHAR(50) DEFAULT NULL COMMENT '班级', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表'; CREATE TABLE t_course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL COMMENT '课程名', course_code VARCHAR(20) NOT NULL UNIQUE COMMENT '课程编号', week_start_time TIME NOT NULL COMMENT '上课时间 HH:mm', week_end_time TIME NOT NULL COMMENT '下课时间 HH:mm' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表'; CREATE TABLE t_student_course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, semester VARCHAR(20) NOT NULL COMMENT '学期,如2024-2025-1', UNIQUE KEY uk_student_course (student_id, course_id, semester) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选课关系表'; CREATE TABLE t_attendance_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, attendance_date DATE NOT NULL COMMENT '考勤日期', section INT 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缺勤 4请假', UNIQUE KEY uk_student_course_date_section (student_id, course_id, attendance_date, section) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考勤记录表';

这个建表脚本里最关键的是考勤记录表末尾的联合唯一约束uk_student_course_date_section。它保证同一个学生在同一门课同一天的同一个节次只能有一条考勤记录。不要小看这个约束,后面并发重复提交签到就靠它兜底。程序里就算先查再插,也会因为并发出现两条一模一样的记录,数据库的唯一约束是最后一道防线。

2.4 课表时间放哪:迟到早退阈值要可改

课表的起止时间放在t_course表里,但迟到几分钟算迟到、早退几分钟算早退,不建议写死在Java代码里,也不建议直接放到课程表里。常见做法是放到application.yml配置里,做成可调节的参数。这样学期初想调整考勤规则时,改配置重发一次服务就行,不用动表结构。

这个设计考量来自真实场景:不同学校、不同课程对迟到的容忍度不一样,有的课老师允许5分钟内不算迟到,有的课过了上课铃就算。把阈值做成配置项,后端代码只用读配置参与判定,报表和接口完全不用动。数据库里只需要存原始打卡时间,一切状态都是计算出来的,这样后期如果规则变了,历史数据还能重新回算。

时间字段的类型也值得注意,考勤日期用DATE,打卡时间用DATETIME。不要用TIMESTAMP,一是它受数据库时区影响容易出现8小时偏差,二是2038年问题虽然远但没必要埋雷。DATETIME配合Java侧的LocalDate和LocalDateTime,处理起来最顺手。

3. 项目结构和核心配置:把Spring Boot源码从解压拉到能启动

3.1 标准分层结构与包命名

源码解压之后,先别急着点启动按钮,花两分钟看清包结构。正常的学生考勤管理系统会遵循Spring Boot标准分层:controller接收请求并做参数校验,service写业务逻辑,mapper负责数据库访问,entity放实体类,config放配置类和拦截器。这种分层的价值在于,考勤规则判定逻辑可以独立写在service层,不至于散落在接口里。

常见包结构大致是这样的:com.example.attendance下面有controller、service、mapper、entity、config、common几个子包。common里放统一返回结果类Result和全局异常处理器。很多毕业设计源码会在common里放一个Result类,返回格式统一成{code, msg, data},前端处理起来非常省事。如果你的源码里没有,自己补一个也很简单。

看包结构是判断一个项目是否规范最直接的方式。如果所有Java文件都堆在一个包里,说明这个源码大概率是赶工产物,接手后要花不少时间整理。反过来,分层清晰的项目,即使接口写得啰嗦,后续扩展也容易得多。

3.2 pom.xml依赖选型:为什么是MyBatis-Plus

学生考勤这类管理系统,增删改查是绝对主体,手写大量XML映射没有必要。依赖选型上,业务代码最重的是MyBatis-Plus,单表查询连SQL都不用写,LambdaQueryWrapper就能覆盖大部分场景。下面是一份在Spring Boot 2.7.x上能直接用的依赖清单。

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这套依赖组合要注意两点。一是MyBatis-Plus和Spring Boot的版本必须匹配,Spring Boot 2.x用mybatis-plus-boot-starter没问题,但换了Spring Boot 3.x之后,必须改成mybatis-plus-spring-boot3-starter,旧包在3.x下启动会直接报找不到类。二是jjwt拆成了api、impl、jackson三个包,jackson是负责JSON序列化的,漏掉会在解析Token时报ClassCastException。版本选择上Spring Boot 2.7.x加MyBatis-Plus 3.5.x是当下最稳的组合。

3.3 application.yml的5个配置项

拿到源码改配置,优先看数据源、端口、时区、MyBatis-Plus和自定义的JWT密钥几个位置。很多项目启动失败,不是代码问题,而是配置里的数据库地址或密码没有改成自己的。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/student_attendance?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 attendance: jwt: secret: your-secret-key-change-me-at-least-32-characters expire-hours: 24 rule: late-minutes: 5 early-minutes: 10

数据源URL里的serverTimezone=Asia/Shanghai是必须的。如果你用的是MySQL 8.x而驱动还停留在5.x,启动时会报驱动类不存在的错误。JWT密钥这里必须写至少32个字符,HS256算法要求密钥不少于256位,太短会抛出弱密钥异常。迟到和早退阈值放在attendance.rule下面,Service里通过@ConfigurationProperties或者@Value读取即可。

3.4 启动三步与版本兼容:跑通再改业务

配置改完后,按顺序做三件事:先确认数据库脚本已经执行成功,库里有表结构;再检查本地JDK版本是否和pom.xml里java.version一致;最后启动Application主类。大多数考勤系统源码基于Spring Boot 2.x,JDK 8或11都能跑,如果你本机装了JDK 17,启动报Unable to instantiate之类的问题,先去看pom里是否依赖了过期的javax包。

我见过最典型的翻车现场是所谓"springboot版本太高":本地新建项目用了Spring Boot 3.x,把源码里的代码一拷,启动直接报错。原因是Spring Boot 3.0开始把javax.*换成了jakarta.*,依赖、注解、拦截器注册全部要跟着换。这种情况下最简单的解决方式是新建项目时把Spring Boot版本选到2.7.x,而不是强行升级代码。如果确实想用3.x,必须同步升级MyBatis-Plus到支持Spring Boot 3的版本,并检查所有javax.servlet引用是否已经改掉。

IDEA里配置启动端口不需要额外操作,server.port写在yml里,Spring Boot启动时自动读取。如果8080被占用,改这个值重启即可。项目能启动,再开始动业务逻辑。

4. 登录鉴权与签到签退:JWT拦截器加两条核心接口

4.1 JWT登录与Token校验

后台管理页面和学生的打卡页面不能裸奔,这套系统通常会区分管理员和学生两种角色。常见做法是登录接口校验用户名密码,成功后签发一个JWT Token,后续请求在Header里带上Authorization: Bearer <token>,拦截器统一校验。

JWT相对Session方案的优势在于无状态,后端不需要存登录会话,尤其在前后端分离部署时非常方便。下面是基于jjwt 0.11.5的Token工具类,生成和解析都封装好了。

@Component public class JwtUtil { @Value("${attendance.jwt.secret}") private String secret; @Value("${attendance.jwt.expire-hours:24}") private long expireHours; private SecretKey getKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public String createToken(Integer userId, String username, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expireHours * 3600_000L)) .signWith(getKey()) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder().setSigningKey(getKey()).build() .parseClaimsJws(token).getBody(); } }

密钥在配置里已经强调过至少32字符,这里再提醒一次:HS256算法对密钥长度有硬性要求,密钥不足会导致每次生成Token时直接抛WeakKeyException。过期时间设置24小时是考勤系统的常规选择,学生一天最多打几次卡,设置过短会造成频繁重新登录,设置过长又不利于账号安全。如果你做了"记住我"功能,可以把过期时间分成两档。

拦截器注册上,我一般会专门写一个WebConfig,统一管理哪些路径要放行、哪些路径要校验。登录接口和静态资源放行,其他/api/**路径全部过拦截器。

@Configuration public class WebConfig implements WebMvcConfigurer { private final JwtInterceptor jwtInterceptor; public WebConfig(JwtInterceptor jwtInterceptor) { this.jwtInterceptor = jwtInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/error"); } }

拦截器里做的事情只有一个:从Header里取出Token,调用JwtUtil解析,把userId和role放到request attribute里供Controller使用。解析失败统一抛401异常,由全局异常处理器返回未授权提示。注意放行路径里别漏了/error,否则认证失败时Spring Boot的默认错误页面会被拦截器二次拦截,出现奇怪的循环跳转。

4.2 签到、签退接口与判重逻辑

签到的核心业务是:学生选择一门课,提交当天第几节课的签到请求。Service层要做两件事,一是友好提示判重,二是兜底防止并发重复写入。MyBatis-Plus的LambdaQueryWrapper可以用来做前置查询,但真正的防线在数据库唯一约束。

@Override @Transactional(rollbackFor = Exception.class) public void checkin(Integer studentId, Integer courseId, LocalDate date, Integer section) { Long count = attendanceMapper.selectCount(new LambdaQueryWrapper<AttendanceRecord>() .eq(AttendanceRecord::getStudentId, studentId) .eq(AttendanceRecord::getCourseId, courseId) .eq(AttendanceRecord::getDate, date) .eq(AttendanceRecord::getSection, section)); if (count != null && count > 0) { throw new BusinessException("该课程这个时段已签到,请勿重复提交"); } AttendanceRecord record = new AttendanceRecord(); record.setStudentId(studentId); record.setCourseId(courseId); record.setAttendanceDate(date); record.setSection(section); record.setCheckinTime(LocalDateTime.now()); record.setStatus(0); try { attendanceMapper.insert(record); } catch (DuplicateKeyException e) { throw new BusinessException("签到失败:该时段已存在打卡记录"); } }

这段代码的逻辑顺序是有讲究的。先查一次是为了给用户一个友好的中文提示,而不是让用户直接看到数据库异常。但先查后插在并发下不安全,两个请求同时通过第一步查询后,第二个插入会撞上唯一约束,所以必须捕获DuplicateKeyException兜底。@Transactional保证插入失败时整个方法回滚,不会留下半截数据。签退接口与签到对称,区别在于它只更新checkout_time和status,不新增记录。

4.3 迟到、早退、缺勤是如何回算的

签到和签退的时间都拿到后,状态判定才有依据。判定规则在Service里单独抽一个方法,避免散落在接口逻辑里。课程表的起止时间和配置里的迟到早退阈值决定最终状态。

private Integer calcStatus(AttendanceRecord record, Course course, AttendanceRuleConfig rule) { LocalTime checkin = record.getCheckinTime().toLocalTime(); LocalTime checkout = record.getCheckoutTime().toLocalTime(); LocalTime start = course.getWeekStartTime(); LocalTime end = course.getWeekEndTime(); boolean late = checkin.isAfter(start.plusMinutes(rule.getLateMinutes())); boolean early = checkout.isBefore(end.minusMinutes(rule.getEarlyMinutes())); if (late && early) { return 2; // 又迟到又早退,按更严重的早退记录 } if (late) { return 1; } if (early) { return 2; } return 0; }

这个方法的参数需要解释一下:rule.getLateMinutes()是迟到容忍分钟数,比如配置5,表示上课时间后5分钟内打卡不算迟到。earlyMinutes同理,是早退容忍分钟数。这里最容易算错的是isAfter和isBefore的边界,精确等于上课时间不算迟到,所以用isAfter而不是isAfter加等于判断。很多考勤源码在这里用一个>=和一个>混在一起,造成边界时段判错。

缺勤状态回算不放在签到签退接口里,而是由一个定时任务每天凌晨统一处理:把已经过了下课时间、但当天没有签到记录的学生标记为缺勤。这样设计的原因是缺勤需要等课程结束才能确认,学生卡在最后一分钟签退也算到勤。

4.4 请假审核通过后如何抵消考勤

请假流程是考勤系统里最容易被忽略的一环。学生请假提交后,管理员审核无非通过或驳回两种结果。审核通过后,需要把该学生请假时间段对应的考勤记录插入一条,状态置为4。如果原本已经有签到记录,也要把状态改成4,否则学生请假了又按时打卡,统计时会出现一个人既算出勤又算请假的情况。

请假抵消考勤的时机要卡在审核通过那一刻,不要在提交请假时就写状态。因为请假可能被驳回,提前改动考勤记录会造成数据脏写。一条请假单可能覆盖多个连续节次,所以Service层需要根据请假单的起始节次和结束节次循环生成多条考勤记录。这里的节次编号必须和t_attendance_record里的section字段对齐,否则会出现审核通过了但考勤记录没匹配上的奇怪现象。

请假和考勤状态的关系,反映在统计上就是出勤率的分母问题。请假不参与出勤率计算,这是和很多人直觉不一样的地方:一个学生请假十天,他的出勤率分母应该相应减少,而不是拿实际出勤除以总课时。这个口径如果不对,期末导出报表时一定会被老师质疑。

5. 考勤统计与五个常见问题排查:出勤率为什么总和Excel对不上

5.1 出勤率统计SQL:分母去掉请假

考勤统计是这套系统最有价值的部分,也是出错率最高的部分。按学生维度统计出勤率,最常见的SQL是先按学生分组,再用CASE WHEN分别统计正常、迟到、早退、缺勤和请假人次。下面是核心查询,注释里标注了统计口径。

SELECT s.student_no, s.student_name, COUNT(*) AS total_records, SUM(CASE WHEN a.status IN (0, 1, 2) THEN 1 ELSE 0 END) AS actual_present, SUM(CASE WHEN a.status = 3 THEN 1 ELSE 0 END) AS absent_count, SUM(CASE WHEN a.status = 4 THEN 1 ELSE 0 END) AS leave_count, ROUND( SUM(CASE WHEN a.status IN (0, 1, 2) THEN 1 ELSE 0 END) / (COUNT(*) - SUM(CASE WHEN a.status = 4 THEN 1 ELSE 0 END)) * 100, 2 ) AS attendance_rate FROM t_student s LEFT JOIN t_attendance_record a ON a.student_id = s.id WHERE a.attendance_date BETWEEN #{startDate} AND #{endDate} GROUP BY s.id, s.student_no, s.student_name ORDER BY absent_count DESC;

这条SQL里最关键的是出勤率的分母:COUNT(*)减掉请假人次。因为请假的学生不来上课是有正当理由的,不应该拉低出勤率。分子是正常、迟到、早退三项之和,迟到和早退虽然算到勤,但报表里应该单列出来,方便老师掌握课堂纪律情况。如果只给一个出勤率数字而不暴露迟到早退明细,这套系统的说服力会大打折扣。

5.2 按课程和按日期的报表怎么组织

按学生、按课程、按日期是三张最常见的报表。按课程统计时,GROUP BY的粒度是课程和日期,因为同一门课不同日期出勤情况差异可能很大。按日期统计时,通常要给出当天应到人数、实到人数、缺席名单三个信息,实到人数不要直接复用状态统计,因为应到人数是选课人数,实到人数是实际打卡人数,两者差距就是缺勤加请假的人数。

接口设计上,这三个报表都可以走同一个查询方法,传入不同的分组字段和筛选条件。如果追求效率,可以加一层汇总缓存,但学生考勤系统的数据量通常不大,没必要为了几万行数据引入Redis,直接查数据库完全够用。报表接口统一走/api/attendance/report/**,按学生、课程、日期三个子路径区分,前端拿到的都是统一的Result格式。

5.3 坑一:数据库时区导致签到日期差8小时

现象:学生晚上打卡,第二天查看报表,签到日期显示的是后一天。或者反过来,上午打卡被记成前一天。原因几乎都是数据库连接的时区设置不对,MySQL默认时区是UTC,而Java应用在Asia/Shanghai,两个时间一转换,DATETIME和LocalDateTime就错位了。

解决:在数据源URL里显式加上serverTimezone=Asia/Shanghai,同时把Jackson的time-zone配置成GMT+8。只改一处不行,JDBC连接时区负责数据库读写,Jackson时区负责前端JSON展示,两层都要统一。这个坑在Linux服务器上尤其隐蔽,本地开发时区对的,一部署就出问题,因为服务器系统时区可能不是东八区。

5.4 坑二:并发重复签到,先查后插拦不住

现象:学生连点两次签到按钮,生成了两条考勤记录。原因:前端按钮没有做防重复提交,后端用"先查再插"的方式判重,两个请求同时通过了查询这一步,然后各自完成插入。解决:数据库唯一约束兜底,这是最根本的防线。程序里保留前置查询用于友好提示,但插入时捕获DuplicateKeyException,把数据库异常翻译成业务提示。后端接口里也可以加一个简单的前端幂等方案,比如每张签到卡带上请求时间戳,但真正可靠的手段还是唯一约束加异常捕获。

5.5 坑三:LEFT JOIN和INNER JOIN统计口径差很多

现象:同一条出勤率SQL,换一个JOIN方式,数字就变了。原因:INNER JOIN会把没有考勤记录的学生过滤掉,而LEFT JOIN能把选了课但一次都没打卡的学生保留下来,并让出勤记录字段为NULL。出勤率统计必须在分组时处理NULL,否则COUNT(*)会把没有记录的学生也算进去,造成分母虚高。

解决:统计出勤率先确定查询主体。如果统计对象是"选了课的所有学生",必须用LEFT JOIN,并在COUNT里用COUNT(a.id)而不是COUNT(*)。注意SQL里我只在WHERE a.attendance_date上加了筛选,如果学生这一时间段完全没有考勤记录,a表字段为NULL,WHERE a.attendance_date BETWEEN会把这一行过滤掉。要展示缺勤0次的学生,筛选条件应该放到ON子句里,或者保留缺省记录。

5.6 坑四:Spring Boot版本太高,MyBatis-Plus不兼容

现象:项目启动时控制台报ClassNotFoundException: javax.servlet.*,或者Mapper扫描时报Invalid bound statement。原因:本地新建了个Spring Boot 3.x项目,把旧源码直接拷进来,javax包已经改名jakarta,旧MyBatis-Plus在3.x环境里根本加载不了。解决:最稳妥的是把项目切回Spring Boot 2.7.x,改动最小。如果确实要留在3.x,需要把MyBatis-Plus依赖换成mybatis-plus-spring-boot3-starter,并全局搜索javax.servlet改成jakarta.servlet。网上有人推荐直接升级不加改动的,实测基本都会翻车。

5.7 坑五:分页插件没注册,查询返回全表数据

现象:调分页接口时,Page对象返回了总条数,但实际查出来的records是全表数据,total也不对。原因:MyBatis-Plus的分页插件不是默认生效的,需要手动注册MybatisPlusInterceptor,不注册时selectPage方法会退化成普通查询。

解决:在config包里新增一个配置类,注册分页拦截器。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

这个配置类在毕业设计源码里经常缺失,因为我见过不少项目把分页当全表查询在用。注册之后,PageHelper式的分页才真正生效,前端表格的页容量和控制条才有意义。另外分页插件的DbType要和你实际数据库一致,MySQL和PostgreSQL的分页方言不一样,配错会生成不兼容的SQL。

6. 部署后自检与进阶:10分钟造数验证签到分流

项目跑起来后,不要急着写业务,先用造数脚本验证签到、签退和统计逻辑对不对。我习惯写一个临时SQL,往考勤记录表里插一批模拟数据,覆盖正常、迟到、早退、缺勤、请假五种状态,再手工计算一名学生的出勤率,和报表接口返回的数字比对。对不上就说明报表SQL或者状态回算有问题,这时候暴露问题成本最低。

-- 临时验证:给1号学生在一门课上造五天的考勤 INSERT INTO t_attendance_record (student_id, course_id, attendance_date, section, checkin_time, checkout_time, status) VALUES (1, 1, '2025-04-01', 1, '2025-04-01 07:55:00', '2025-04-01 09:40:00', 0), (1, 1, '2025-04-02', 1, '2025-04-01 08:10:00', '2025-04-01 09:40:00', 1), (1, 1, '2025-04-03', 1, '2025-04-01 07:55:00', '2025-04-01 09:00:00', 2), (1, 1, '2025-04-04', 1, NULL, NULL, 3), (1, 1, '2025-04-05', 1, NULL, NULL, 4);

这五条数据分别对应正常、迟到、早退、缺勤、请假。把这五天的考勤手动算一下,出勤率分母是4天(请假不算),分子是3天(迟到早退算到勤),结果应该是75%。如果报表接口返回的不是75%,先查SQL里的CASE WHEN条件,再看状态回算的优先级是不是把迟到和早退的排序搞反了。手工核对是排查黑匣子的最直接手段。

进阶技巧方面,建议加一个@Scheduled定时任务,每天凌晨自动把过课未签到的人标记为缺勤,否则学生漏了打卡就一直停在待定状态,报表永远不会准确。定时任务记得在启动类上加@EnableScheduling,这是Spring Boot里最容易漏的注解。

@Scheduled(cron = "0 0 1 * * ?") public void autoMarkAbsent() { // 找出昨天课程已结束但没有签到时间的记录,批量改成缺勤 attendanceMapper.markAbsentByDate(LocalDate.now().minusDays(1)); }

前端页面如果也想省事,常见做法是把Vue或静态页面打包出来的dist目录整个放进src/main/resources/static,Spring Boot会自动当作静态资源处理,这样就不需要单独部署前端服务。部署完之后,把管理员账号登录、学生签到、报表导出三个主流程各走一遍,再打开数据库直接看记录,基本就能放心交付了。

我接手这类源码时习惯先跑通再优化,第一个翻车点通常都在时区配置和版本匹配上。每次造数据验证都能抓出几个统计口径的问题,这套自检流程已经成了我的固定动作,希望帮到你。

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

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

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

立即咨询