☰
SpringBoot+Vue考勤系统核心逻辑与数据库设计全拆解
2026/10/3 3:58:30 网站建设 项目流程

考勤系统大概是计算机毕业设计里被做得最烂、也最被低估的一个选题。说它被做得烂,是因为每年都有大量同学从网上拿到一套SpringBoot+Vue 考勤管理平台源码,跑通界面就算完事,连考勤判定的核心逻辑在哪一行都没看过。说它被低估,是因为同一个选题,有人只做到“学生表和打卡表各一张、CRUD 糊弄过去”,有人却能理直气壮地讲清楚权限模型、考勤规则、请假审批、统计报表这条完整链路。这篇博文就是想把后面这条链路彻底讲透。

你将看到一套基于Java+MySQL的 SpringBoot+Vue 前后端分离考勤系统,从功能拆分、数据库设计、核心业务逻辑到本地部署和答辩扩展,我会把实际部署和改造这类系统时踩过的坑、验证过的方案全部摆出来。不管你是做毕业设计、课程设计,还是单纯想用这个项目练手学全栈,这套思路都能让你的项目从“能跑”变成“能讲”。

1. 为什么考勤类管理系统是毕设“安全牌”,也是最容易翻车的选题

不管你看多少选题推荐,考勤类管理系统永远稳定地出现在“适合毕设/课设”的列表里。原因很简单:技术栈覆盖均匀、业务链路完整、扩展空间充足。SpringBoot 处理后端接口和业务逻辑,Vue 做前端页面与交互,MySQL 存数据,这三样正好覆盖大多数学校对毕设的技术考核点。而且考勤不是一个“玩具需求”——它有真实的角色体系(管理员、教师、学生)、有复杂的业务规则(迟到早退缺勤怎么判定、请假怎么审批)、有数据统计需求(出勤率、缺勤次数),拿它来做项目,几乎能把课程里学的东西完整串一遍。

但正因为热门,它也最容易翻车。翻车点往往不在技术本身,而在“你拿到源码后做了什么”。网上流传的考勤系统源码版本非常多,有的数据库脚本写了一半,有的前端依赖版本老到 npm install 直接报错,有的甚至连学生签到都只是在前端写死了一条记录。如果你只是在答辩前把它跑起来、截几张图,这个项目反而会给你带来麻烦——因为老师问的每一个问题,都可能在源码对应的位置暴露出你根本没看过它。

所以这篇博文我尽量按照“拿到一套源码之后应该如何消化它”的顺序来讲:先看功能边界,再看技术结构,然后理解数据库和核心逻辑,最后把它跑起来并做个性化扩展。这个顺序其实就是你真正需要理解一个项目的顺序。

2. 功能边界拆解:一套能过答辩的考勤系统该有哪些模块

2.1 三种角色,三种完全不同的使用视角

一套合格的考勤系统,一定不是“所有人都能打卡”这么简单,而是围绕角色划分权限。最常见的角色划分是管理员、教师、学生三类,各管各的事。

角色核心职责典型页面
管理员维护基础数据、配置规则、查看全局统计学生管理、教师管理、课程管理、全局考勤统计
教师管理自己授课课程的考勤、审批请假我的课程、点名补签、请假审批、课程出勤报表
学生上课签到、提交请假、查看个人记录今日课表、在线签到、请假申请、我的考勤

这个权限模型是答辩时的重点。为什么学生看不到别人的考勤?为什么教师只能操作自己名下的课程?这类问题背后都是权限设计。常见的实现方案是 Spring Security + JWT,登录后返回角色,后端接口通过注解或拦截器校验角色,前端路由也做一层角色过滤。前后端双重校验,既能防止直接调接口越权,也能让页面不渲染无权限的入口。

2.2 核心业务模块,不是简单 CRUD

如果把功能拆到页面级别,大致有下面这些:

  • 登录认证模块:账号密码登录,JWT 签发与校验,退出登录。
  • 学生信息管理:管理员维护学生的学号、姓名、班级、专业,支持 Excel 批量导入。这里值得注意,批量导入几乎是每套毕设系统都会用到的功能,用 EasyExcel 或 POI 都能做,但 EasyExcel 写起来省很多事。
  • 课程与排课管理:管理员维护课程基本信息,然后把课程分配给教师,再把学生选课关系维护好。一门课要有上课时间(星期几、第几节)、上课地点、授课教师,这些字段后面考勤判定都要用。
  • 在线签到:学生在自己课表页看到当天课程,点击签到按钮,系统根据当前时间和定位判断状态。这是整个系统的核心页面。
  • 请假申请与审批:学生提交请假申请(选课程、填原因、选时间),教师看到待审批列表,通过或驳回。审批通过后,请假时间段内的缺勤记录要自动修正为“请假”状态。
  • 考勤记录管理:教师可以对异常记录进行补签、修改状态;管理员可以看到全系统的考勤流水。
  • 统计报表:按课程、班级、时间段统计出勤率、迟到率、缺勤次数,用表格或图表展示,支持导出 Excel。

把这些模块讲清楚之后,你会发现它已经覆盖了一个完整信息管理系统该有的东西:认证授权、基础数据维护、业务流转、审批流、统计导出。这也是为什么考勤系统在开题、中期、答辩三个阶段都能拿出“正经功能”来汇报,而不是只能演示一个登录页加一个列表页。

3. 技术选型和工程结构:SpringBoot+Vue 这套黄金组合好在哪

3.1 为什么是这个组合

先说后端。SpringBoot 最核心的价值是“约定大于配置”:内嵌 Tomcat,自带起步依赖,一个spring-boot-starter-web就把 Web 环境全部拉起来,省掉了传统 SSM 项目里大量 XML 配置。对毕设来说,这意味着你可以把精力放在业务逻辑上,而不是在配置上面反复折腾。配合MyBatis Plus这样的 ORM 增强框架,单表 CRUD 可以直接用 BaseMapper 里的现成方法,开发速度非常快。

前端选 Vue 的理由更直接:组件化开发适合后台管理系统这种“页面结构类似、逻辑重复”的场景。一个学生管理页和一个教师管理页,长得差不多,抽成公共组件后改改字段就能复用。再加上 Element UI / Element Plus 这套组件库,表格、表单、弹窗、分页全是现成的,前端页面开发效率直接翻倍。我不太推荐在这个项目里硬上特别复杂的前端架构,Vue + Vue Router + Vuex/Pinia + Axios 已经足够撑起整个项目的前端部分。

MySQL 则是所有组合里最没争议的。轻量、免费、学校机房一般都有现成环境,网上教程也多,出了问题好排查。数据库选型别整花活,除非你明确知道自己在干什么,否则用 MySQL 就是最稳妥的选择。

3.2 工程目录结构,拿到源码先看这几层

一套规范的前后端分离项目,后端一定是分层的。常见结构是这样的:

backend/ src/main/java/com/xxx/attendance/ controller/ # 接口层,只做参数接收和结果封装 service/ # 业务逻辑层,考勤判定、请假审批都在这里 mapper/ # 数据访问层,对应 MyBatis 的 Mapper 接口 entity/ # 数据库实体类 config/ # 配置类,比如跨域配置、JWT 拦截器配置 common/ # 公共类,统一返回结果、异常处理、工具类 src/main/resources/ application.yml # 数据源、端口、JWT 密钥等配置 frontend/ src/ api/ # 接口请求封装,按模块拆文件 views/ # 页面组件,一个路由对应一个页面 components/ # 公共组件 router/ # 路由配置,含角色守卫 store/ # 全局状态,存用户信息和登录状态

拿到源码先别急着跑,按这个目录扫一遍,搞清楚每个包是干什么的。面试和答辩时,老师经常让你“讲讲这个项目从请求到响应的完整链路”,你能顺着 Controller → Service → Mapper → MySQL 把一条签到请求讲下来,就已经赢过大多数人了。

3.3 前后端如何配合:代理、请求封装与认证

开发环境下,前端起在 8080 端口,后端起在 8081 端口,浏览器直接跨域。最简单的解决办法是前端配代理,让前端开发服务器把/api开头的请求转发给后端。这样浏览器里看到的请求都是同源的,绕开跨域问题。后面部署阶段还有另一种合并部署方案,我会在部署章节详细说。

认证方式主流是 JWT。用户登录成功后,后端生成一个带过期时间的 token,前端存在 localStorage 或 Pinia/Vuex 里,每次请求通过 Axios 拦截器把 token 放到 Authorization 请求头。后端写一个拦截器,除了登录接口和静态资源,其余接口都校验 token,解析出当前用户 ID 和角色。这个机制不难,但它是整个权限模型的地基,建议拿到源码后第一件事就是找到这个拦截器,看它放行了哪些路径、拦截了哪些路径。

4. 数据库设计:考勤系统的地基,核心表结构和字段背后的设计逻辑

4.1 网上源码常见的“一眼假”表结构

很多流出来的考勤源码,数据库里只有两张表:一张user,一张attendance。user表里塞一个role字段区分学生和教师,attendance表里存学生ID、课程ID、签到时间、状态就完事了。这种表结构跑起来没问题,但拿给老师看,等于在脸上写着“这个系统只能做增删改查”。

一个能正经答辩的考勤系统,数据库至少要有这几张表:

-- 用户表,统一登录账号 CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role TINYINT NOT NULL COMMENT '1管理员 2教师 3学生' ); -- 学生信息表 CREATE TABLE t_student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT, student_no VARCHAR(20) NOT NULL, name VARCHAR(50) NOT NULL, gender TINYINT, class_name VARCHAR(50), major VARCHAR(50) ); -- 教师信息表 CREATE TABLE t_teacher ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT, teacher_no VARCHAR(20) NOT NULL, name VARCHAR(50) NOT NULL, department VARCHAR(50) ); -- 课程表,考勤规则所需的时间、地点参数都放在这里 CREATE TABLE t_course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, teacher_id BIGINT, semester VARCHAR(20), week_day TINYINT COMMENT '1-7 表示周几', start_time TIME, end_time TIME, address VARCHAR(100), latitude DECIMAL(10,6), longitude DECIMAL(10,6), sign_start_offset INT DEFAULT 15 COMMENT '课前多少分钟开放签到', late_offset INT DEFAULT 15 COMMENT '上课后多少分钟内算迟到' ); -- 学生选课关系表,多对多关系 CREATE TABLE t_student_course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT, course_id BIGINT ); -- 考勤记录表 CREATE TABLE t_attendance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT, course_id BIGINT, att_date DATE, sign_time DATETIME, status TINYINT COMMENT '1正常 2迟到 3早退 4缺勤 5请假', sign_type TINYINT COMMENT '1定位签到 2二维码签到 3教师补签', latitude DECIMAL(10,6), longitude DECIMAL(10,6) ); -- 请假申请表 CREATE TABLE t_leave ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT, course_id BIGINT, leave_date DATE, reason VARCHAR(255), status TINYINT DEFAULT 0 COMMENT '0待审批 1通过 2驳回', create_time DATETIME, audit_time DATETIME, audit_opinion VARCHAR(255) );

4.2 这些字段为什么要这样设计

先看t_course表。很多人会把“上课时间”设计成两个字符串字段,比如start_time = "08:00",也能用,但没法做时间比较运算。用TIME类型,LocalTime.now().isAfter(course.getStartTime())这类判断写起来非常自然。另外sign_start_offset和late_offset这两个偏移量字段是点睛之笔——不同老师对考勤宽容度不一样,有的老师课前 10 分钟开放签到,有的老师 15 分钟,有的老师允许迟到 20 分钟。把阈值做成课程上的配置而不是写死在代码里,这就是“可配置”三个字的体现,答辩时能拿出来说。

t_attendance表里的status字段用整数而不是字符串,不是为了省存储空间,而是为了统计方便。后面算考勤率的时候,SUM(status = 1)就能直接算正常次数,一条 SQL 全搞定。如果你用字符串“正常”“迟到”,那统计 SQL 就得写一堆CASE WHEN,又丑又容易错。

还有一个细节值得提:t_attendance表里我建议冗余存一份课程名称快照(或者至少存 course_id 并关联查询)。考勤记录是不可变流水,如果课程被管理员改名或删除,历史记录里的关联会断掉。冗余存储虽然违反第三范式,但在业务流水表上做适度反范式设计,是实际项目里非常常见的做法,也可以在论文里作为一个设计思考点写出来。

t_student_course这种中间表是典型的多对多解决方案。一个学生选多门课,一门课有多个学生,如果没有中间表,你就得在课程表里存“学生ID列表”,或者在学生表里存“课程ID列表”,两种都是灾难。理解了这张表,也就理解了关系型数据库里最核心的建模思路。

5. 核心业务逻辑:考勤判定、请假审批、统计报表的实现思路

5.1 考勤判定不是“打个卡”,是一套时间窗口计算

考勤判定的核心问题是:学生点击签到的那一刻,系统怎么知道他算正常、迟到还是缺勤?答案是把当前时间和课程配置的时间窗口做比较。

假设某节课 09:00 开始,12:00 结束,配置了课前 15 分钟开放签到、迟到阈值 15 分钟。那么:

  • 08:45 - 08:59签到 → 正常
  • 09:00 - 09:14签到 → 迟到
  • 09:15及以后签到 → 缺勤
  • 11:50 - 12:00之间签到时,还可以顺便判断早退

后端伪代码大致是:

LocalTime now = LocalTime.now(); LocalTime start = course.getStartTime(); LocalTime end = course.getEndTime(); if (now.isBefore(start.minusMinutes(course.getSignStartOffset()))) { return Result.error("还没到签到时间"); } if (now.isBefore(start)) { status = 1; // 正常 } else if (now.isBefore(start.plusMinutes(course.getLateOffset()))) { status = 2; // 迟到 } else { status = 4; // 缺勤 } // 早退判断:如果现在处于下课之前 signStartOffset 分钟内 if (now.isAfter(end.minusMinutes(10)) && now.isBefore(end)) { status = 3; // 早退 }

这里最容易被忽略的是“签到时间窗口关闭”的逻辑。很多简单实现只判断了“迟到”和“缺勤”的边界,却没有处理“课程结束了还能不能签到”的问题。如果课程 12:00 结束,学生下午 3 点还能打开页面补签,那考勤就失去意义了。所以时间窗口必须做成闭合的——超出允许时间,接口直接拒绝,提示“签到时间已过”。

另外,同一门课当天不能重复签到。学生最多只能生成一条当天的考勤记录,如果已经存在,再请求要么直接返回已有记录,要么提示“今日已签到”。这两个判断逻辑简单,但特别影响系统正确性,建议拿到源码后第一个验证的就是这两个点。

5.2 定位签到:精度不用太高,但防呆必须做好

定位签到的思路是:前端通过浏览器navigator.geolocation获取当前经纬度,传给后端,后端用 Haversine 公式计算这个点和课程地点(存在t_course.latitude/longitude)之间的距离,在设定范围内(比如 200 米)就允许签到。

private double distance(double lat1, double lng1, double lat2, double lng2) { double radLat1 = Math.toRadians(lat1); double radLat2 = Math.toRadians(lat2); double a = radLat1 - radLat2; double b = Math.toRadians(lng1) - Math.toRadians(lng2); double s = 2 * Math.asin(Math.sqrt( Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2) )); return s * 6371000; // 地球半径,单位米 }

实际开发时有个坑:浏览器定位在http://localhost下能用,但部署到服务器上如果用的是http://而没上 HTTPS,浏览器会直接拒绝调用navigator.geolocation。解决办法要么给站点配 HTTPS,要么用微信 JS-SDK 里的定位接口。做毕设演示时最好提前踩一下这个点,别到现场才发现定位按钮点了没反应。调试阶段可以用浏览器 DevTools 里的 Sensors 面板手动模拟经纬度,不用真跑到教室门口测。

5.3 请假审批:状态机 + 自动修正考勤记录

请假模块的状态流转是一个典型的小型状态机。学生提交请假 → 状态为“待审批”,教师审批通过 → 状态变为“通过”,审批驳回 → 状态变为“驳回”。不允许从“驳回”直接跳到“通过”,这种状态约束用枚举类型或带状态校验的 Service 层逻辑来实现。

请假审批里最容易被忽视的是“审批通过后要修正考勤记录”。学生请假当天如果已经有缺勤记录,教师通过请假申请后,系统应该自动把该学生当天对应课程的考勤状态从“缺勤”修正为“请假”。这个动作可以放在审批通过的 Service 方法里统一处理。这个功能虽然代码不多,但在答辩里是一个非常加分的业务闭环设计——它说明你考虑到了数据联动,而不只是做一个独立的请假表单。

5.4 统计报表:SQL 聚合和前端图表

统计报表不复杂,但 SQL 写得好不好看很影响效率。按课程统计某段时间的出勤情况,可以这样写:

SELECT course_id, DATE(att_date) AS day, COUNT(*) AS total, SUM(status = 1) AS normal_count, SUM(status = 2) AS late_count, SUM(status = 3) AS early_count, SUM(status = 4) AS absent_count, SUM(status = 5) AS leave_count FROM t_attendance WHERE att_date BETWEEN #{startDate} AND #{endDate} GROUP BY course_id, DATE(att_date) ORDER BY day;

MySQL 里SUM(条件)的写法可以把“等于某状态”当成 1 和 0 来累加,一次查出全部状态分类,不用写一堆子查询。前端拿到这个结果后,用 ECharts 的折线图展示出勤率趋势、柱状图对比各课程缺勤人数,就是很好的可视化页面。导出 Excel 用 EasyExcel 直接写个监听器,把统计结果映射成实体列表导出即可。

6. 从源码到跑通:本地部署的完整链路,以及最容易卡住的三类报错

6.1 部署前先确认环境版本

一套典型的 SpringBoot 2.x + Vue 2 项目,建议的本地环境是:JDK 8 或 11、Maven 3.6+、MySQL 5.7 或 8.0、Node 14 到 16。如果是 SpringBoot 3.x + Vue 3 + Element Plus 的新版组合,JDK 要求 17,Node 要求 16 以上。这里特别提醒一句:网上很多源码是基于 SpringBoot 2.x 写的,你如果非要用 JDK 17 跑,大概率会遇到 javax 包名不兼容的问题(SpringBoot 3 才换成 jakarta)。所以拿到源码第一件事,先看pom.xml里的 parent 版本,再决定用哪个 JDK,别一上来就java -jar。

6.2 完整部署操作记录

以最常见的 SpringBoot 2.x + Vue 2 项目为例,完整跑通一套源码的流程是:

  1. 创建数据库。用 Navicat 或命令行新建一个库,比如attendance_db,然后把源码里提供的attendance_db.sql导入。很多源码的 SQL 脚本里没带CREATE DATABASE语句,需要你手动建库后再导入。
  2. 改后端配置。打开application.yml,把数据源的 URL、用户名、密码改成你自己的。MySQL 8 一定要在 URL 后面带上serverTimezone=Asia/Shanghai,否则 JDBC 驱动会报时区错误。
spring: datasource: url: jdbc:mysql://localhost:3306/attendance_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver
  1. 启动后端。在项目根目录执行mvn spring-boot:run,或者先mvn clean package -DskipTests打包成 jar,再java -jar target/xxx.jar启动。看到 “Started Application in xx seconds” 就说明后端起来了。
  2. 启动前端。进入frontend目录,先npm install。如果遇到 peer 依赖冲突,用npm install --legacy-peer-deps基本都能解决,这是 Node 高版本和旧项目依赖冲突最常见的解法。
  3. 配置前端代理。查看vue.config.js或vite.config.js,确认/api代理指向后端地址:
// vue.config.js 示例 module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } } };
  1. 执行npm run dev,浏览器打开http://localhost:8080,用源码里提供的初始账号登录,能进去、能打卡,部署就成功了。

6.3 最常见的三类报错,对应解法

第一类是数据库连不上。报Access denied for user基本就是密码错了;报Unknown database就是库没建或库名不对;报The server time zone value就是serverTimezone没配上。按上面三步逐一排查,五分钟内能定位。

第二类是端口被占用。后端默认 8080,前端也默认 8080,两个同时启动必然冲突。解决方法是把后端端口改成 8081(或任意没被占用的端口),然后前端代理指向对应端口。还有一种情况是 8080 被其他程序占了,Windows 下用netstat -ano | findstr 8080找到占用进程 PID,任务管理器里结束掉即可。

第三类是 npm install 装到一半报错。最常见原因是 Node 版本和项目依赖不兼容。老的 Vue 2 项目在 Node 18 上经常报opensslErrorStack之类的 OpenSSL 错误,要么用 nvm 切换到 Node 14 或 16,要么在package.json的 scripts 里加NODE_OPTIONS=--openssl-legacy-provider再重新跑。从经验上说,用 nvm 管理 Node 版本是最干净的方案。

7. 部署完别急着交:定位签到失效、跨域拦截、路由刷新404,三个坑的完整排查链路

7.1 跨域拦截:为什么后端配置了 CORS 还是报错

开发环境下最常见的报错是 “Access to XMLHttpRequest has been blocked by CORS policy”。网上一般的说法是“在后端加一个 CORS 配置类”,但真正麻烦的是这个配置到底有没有生效。完整的排查链路是这样的:

打开浏览器 F12 的 Network 面板,看被拦截的请求。如果是跨域请求,浏览器会先发一个 OPTIONS 预检请求。如果后端配置的 CORS 没有正确放行这个预检,就会出现拦截报错。此时先看后端有没有类似这样的配置:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } }

如果加了配置还是报错,重点检查两件事:一是项目里有没有 Spring Security,因为 Security 的过滤链优先级高于 MVC 的 CORS 配置,要在 Security 配置类里把 CORS 也放行;二是allowedOriginPatterns和allowCredentials(true)的组合是否正确,允许凭证时不能简单用allowedOrigins("*")。

不过说实话,开发环境我不建议依赖后端 CORS 配置来解决问题。直接在前端 devServer 配代理,前端请求全走同源,从根上绕过跨域,简单可靠,还不用动后端代码。后端 CORS 配置留到生产环境或前后端完全分离时再用。

7.2 路由刷新 404:尖括号里的历史模式惹的祸

后端和前端都跑通后,很多人会把前端用npm run build打包,然后把dist文件夹的内容复制到后端src/main/resources/static目录,合并成一个 jar 部署。这样做的好处是只用一台服务器、一个端口就能跑完整套系统,答辩演示特别方便。

但坑马上就来了:点击页面内部跳转没问题,一旦用户在地址栏输入子路径后按回车,或者按 F5 刷新一个非首页路径,页面就变 404 了。原因是 Vue Router 用了history模式,路由路径是前端管理的,后端静态服务器找不到对应路径的资源,自然回 404。

最简单的解决方法是后端加一个转发规则,把所有非/api开头的请求都转发到index.html:

@Controller public class PageForwardController { @RequestMapping(value = "/{path:[^\\.]*}") public String forward() { return "forward:/index.html"; } }

或者更省事,前端路由改成hash模式,URL 长这样http://localhost:8080/#/attendance,所有路径都在 hash 里,不经过后端,天然不会 404。但 hash 模式 URL 不好看,如果是正式部署推荐用 history 模式加后端转发,做毕设演示的话两者都可以接受。

7.3 考勤时间判断异常:少了 8 小时和少了 15 分钟的两种尴尬

有次我帮人看一个考勤系统,学生明明早上 8 点 55 分签的到,系统判定却是迟到。排查了大半天,最后发现问题是数据库连接串里的时区没配好。MySQL 服务器默认时区是东八区,但 JDBC 连接时如果没指定serverTimezone,驱动会用 JVM 默认时区去解释时间,两端时区不一致,读出来就错乱。这里的现象和解决方法在部署章节已经说过:MySQL 8 必须带serverTimezone=Asia/Shanghai。

还有一个很容易被忽略的边界情况:考勤接口里如果直接拿系统时间LocalTime.now()去和课程时间比较,而课程时间是数据库里读出来的TIME类型,两者理论上都是服务器时间,没问题。但有些源码为了“图省事”,把课程开始时间写成了一个全局配置,比如“允许签到时间 08:45”,而不是从课程表里读。一旦课程时间调整,这个写死的配置就成坑了。所以拿到源码后,重点检查考勤判定逻辑到底是从课程表读参数,还是用了写死的值,这也是你真正读懂这套系统的关键一步。

8. 从能跑到能讲:对源码做个性化改造,让答辩不再心虚

8.1 为什么一定要改造

网上同质化太严重了。同一个项目源码,可能班里有七八个同学都用它在做毕设,导师眼睛雪亮,一眼就能认出来。单纯的“跑通”只能保证不挂,但不能保证拿高分。真正的加分项是你对源码做了一两个有业务意义的改造,并且能清楚地讲出“为什么这么做”。这个改造不需要多宏大的规模,但要有完整的故事线:解决了什么问题、方案是什么、效果怎么样。

8.2 低成本高回报的四个扩展方向

扩展一:动态二维码签到。普通二维码签到用一张静态二维码,学生扫一扫就完成签到,截图流传后很容易代签。改造方案是把二维码内容设计成“课程ID + 当前时间戳 + 随机数”,30 秒或 1 分钟刷新一次,过期作废。学生每次打开签到时候,前端向后端请求一个新的二维码内容。这个改造代码量不大,但能完整讲出一个“防止代签”的业务故事。

扩展二:人脸识别签到。调用现成的云平台人脸识别 API(百度的 AI 开放平台、腾讯云的人脸识别),学生在签到时拍照上传,后端做人脸比对,确认是本人后才允许签到。这个方向适合想往 AI 方向靠一点的选题,而且云平台 API 一般都有免费额度,毕设量级完全够用。写论文时可以把“传统定位签到 + 人脸验证”做成双层校验,作为系统的差异化亮点。

扩展三:数据可视化大屏。用 ECharts 做一块“考勤总览”大屏页面,展示今日到课率、各学院缺勤排行、近 30 天考勤趋势。这算锦上添花的功能,但视觉效果非常加分。很多同学不会做,你做了就比别人多一个可以演示的页面。

扩展四:消息通知。请假审批通过或驳回后,系统给对应学生发送站内信或邮件通知。用 Spring 自带的ApplicationEventPublisher做事件解耦即可,顺便还能写一个异步处理的小案例。这个功能涉及事件驱动和异步,工作量不大,但论文里可以讲的东西很多。

8.3 改造怎么落地,怎么讲

选定扩展点之后,不要直接闷头写代码。先把改造涉及的数据表变更、接口设计、流程变化写成一个简短的设计说明,再动手实现。答辩时不要只说“我加了人脸识别”,要给一个完整的叙事:考勤场景里存在代签问题 → 单纯的定位签到无法防止代签 → 我引入人脸识别 + 定位双重校验 → 实现效果和准确率如何。这个“问题 - 方案 - 验证”的叙事结构,比堆砌一万字的功能介绍都管用。

我自己带过不少用这类系统做毕设的同学,最大的体会是:你对项目的理解深度,直接决定答辩时的自信程度。哪怕是网上拿到的源码,只要你能把考勤判定那几行逻辑亲手改一遍、把数据库表关系自己画出来、把报表 SQL 读明白,这个项目就已经变成你自己的了。希望这篇拆解能帮你少走一点弯路,把一套平平无奇的考勤系统,做成一份拿得出手的作品。

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

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

立即咨询