
简介基于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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 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里的serverTimezoneAsia/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.xJDK 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 LambdaQueryWrapperAttendanceRecord() .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里显式加上serverTimezoneAsia/Shanghai同时把Jackson的time-zone配置成GMT8。只改一处不行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表字段为NULLWHERE 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/staticSpring Boot会自动当作静态资源处理这样就不需要单独部署前端服务。部署完之后把管理员账号登录、学生签到、报表导出三个主流程各走一遍再打开数据库直接看记录基本就能放心交付了。我接手这类源码时习惯先跑通再优化第一个翻车点通常都在时区配置和版本匹配上。每次造数据验证都能抓出几个统计口径的问题这套自检流程已经成了我的固定动作希望帮到你。本文还有配套的精品资源点击获取