
简介基于SpringBootVue开发的学生考勤管理系统完整毕业设计项目面向计算机专业正在准备毕设的学生及需要项目实战经验的Java学习者同样适用于课程设计、期末大作业等场景。系统采用B/S架构以Java为核心技术、MySQL为后台数据库围绕管理员、教师、学生三类角色构建了学生管理、教师管理、班级信息、课程信息、签到信息、考勤信息、请假信息及考勤统计等完整功能模块业务完整性与可扩展性兼备。压缩包共包含428个文件压缩后体积约69.47MB涵盖java后端源码、vue前端页面、svg图标、sql数据库脚本、docx开发说明文档、答辩PPT、演示视频及代码注释等目录结构清晰便于按需检索。所有功能模块均经过严格调试可直接运行并作为毕业设计提交。目前已有677人学习下载适合需要快速落地考勤管理系统或希望参考SpringBoot与Vue全栈项目做二次开发的读者。1. 学生考勤管理系统手工点名太累不如把它做成一个项目一个班四五十人老师喊一圈名字要两三分钟课代表课后还要对着记录表手工统计迟到、缺勤、请假月底汇总时经常要熬夜对账。学生考勤管理系统要解决的就是这件事学生通过手机或电脑打卡系统根据课程时间自动判定状态老师随时能看到出勤率报表从“人工点名、月底对账”变成“自动记录、实时统计”。这个项目用 SpringBoot 做后端接口、Vue 做前端页面配合 MySQL 数据库是一套典型的前后端分离全栈项目交付形式是源码加数据库脚本加说明文档。适合毕业设计选题、课程设计练手也适合刚入门全栈开发的人拿来当第一个完整项目——它的业务规则清楚表结构不复杂但又足够撑起一个完整的开发流程。2. 技术选型与项目结构SpringBoot 管接口、Vue 管页面各司其职2.1 为什么选 SpringBoot Vue 而不是别的组合考勤管理系统本质是一个 CRUD 业务系统录入学生、排课、打卡、查统计核心操作就是增删改查加一些业务规则判断。对这种系统SpringBoot 几乎是当前最省事的后端选择。它内置了 Tomcat一个mvn spring-boot:run就能起服务MyBatis 或 MyBatis-Plus 把数据库操作简化到接口声明Spring Security 或 JWT 拦截器用来做登录鉴权整套生态非常成熟。同样是做这个题目用 SSM 要自己配一堆 XML用 PHP 写起来快但后续维护和答辩展示时架构感弱用 Python Django 也不是不行但对于国内大多数课程设计和毕业设计而言SpringBoot 是主流参考资料最多出问题最容易搜到答案。前端选 Vue核心原因是它适合中小型管理系统的开发节奏。基于 SpringBoot Vue 的项目现在已经成了毕业设计和中小型管理系统的主流标配。Vue 的响应式数据处理让考勤记录的实时刷新变得很简单组件化开发把登录页、打卡页、报表页拆开每个人负责一块也不容易打架。相比 React 需要自己抉择状态管理方案Vue 的上手路径更平滑而且 Element UI 或 Element Plus 这类组件库几乎把表单、表格、弹出框都封装好了做管理后台效率很高。还有一点容易被忽视前后端分离带来的部署灵活性。后端是纯接口服务前端打包成静态文件用 Nginx 托管学生端和老师端可以共用一套接口以后要加一个移动端小程序后端接口基本不用动。虽然不是每个考勤系统都需要这种扩展性但技术选型时留出这条路后期改造成本会低很多。2.2 拿到源码包后先看这三块目录一个标准的基于 SpringBoot 加 Vue 的考勤管理系统源码包通常不会把前后端混在一个工程里而是分成几块独立目录。拿到手先别急着跑把结构看清楚再动手。常见的目录划分是这样student-attendance/ ├── backend/ # SpringBoot 后端工程Maven 结构 │ ├── src/main/java │ ├── src/main/resources │ └── pom.xml ├── frontend/ # Vue 前端工程 │ ├── src/ │ ├── package.json │ └── vite.config.js 或 vue.config.js ├── sql/ # 数据库初始化脚本 │ └── attendance.sql └── doc/ # 设计文档、答辩 PPT、使用说明后端src/main/java下面一般按controller / service / mapper / entity / config分包对应接口层、业务层、持久层、实体类和配置类。前端src下常见的是views页面、router路由、api请求封装、store状态管理。sql目录里的脚本决定了你能不能跑起来先用它建库建表再启动后端再启动前端这个顺序一般不会错。建议你拿到源码后第一件事不是看代码而是先把 SQL 脚本导入数据库确保库表都在再去启动后端。很多人一上来就npm install然后报错折腾半天发现数据库根本没初始化后端一启动就报表不存在的错误白浪费一下午。2.3 环境版本搭配照着配不会翻车版本问题是这类项目第一个坑。我见过太多人卡在环境上这里给出一套稳妥的组合照着配基本不会出问题。组件推荐版本说明JDK8 或 11SpringBoot 2.7.x 用这两个版本最稳SpringBoot2.7.x不要一上来就用 3.xjavax 和 jakarta 命名空间不兼容MySQL8.05.7 也能跑但 8.0 更常见Node.js16.x 或 18 LTS太新的 20 有时装老依赖会出兼容问题Vue2.6 Element UI 或 Vue 3 Element Plus取决于源码用的哪个别混Maven3.6后端依赖管理这里重点提醒 SpringBoot 版本。很多最新教程已经用 SpringBoot 3.x但源码包的 pom.xml 如果基于 2.7 写的直接升到 3.x 会报javax.servlet找不到之类的错误因为 SpringBoot 3 把javax换成了jakarta。拿到源码先看pom.xml里的parent版本号不要自作主张升级。前端也一样Vue 2 的项目用 Element UIVue 3 的项目用 Element Plus两者 API 不通用混着用页面直接白屏。数据库方面MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver连接 URL 必须带时区参数否则启动报错。这个细节后面避坑章节会展开。3. 数据库设计六张表把考勤状态机立住3.1 核心表结构与字段释义考勤系统的业务可以拆成四个实体学生、教师、课程、考勤记录再加上请假申请一共六张表足够覆盖整个业务流程。很多源码包的表设计大同小异核心就是这几张。表结构设计的好坏直接影响后面接口的复杂度设计得好统计接口只需要一条 SQL设计得差业务层要写一堆循环去补数据。学生表是基础数据字段一般包括学号、姓名、班级、手机号。注意学号要设唯一索引因为打卡和统计都需要按学号定位学生。教师表简单一点姓名、工号、账号密码。课程表是考勤的判定依据里面必须有上课时间、下课时间、上课地点经纬度或教室编号这三个字段是做自动判定和位置校验的基础。考勤记录表是整个系统的核心建议这样设计字段类型说明idbigint 主键自增记录主键student_idbigint学生 ID关联学生表course_idbigint课程 ID关联课程表attendance_datedate上课日期精确到天check_in_timedatetime打卡时间用于判定迟到早退statustinyint出勤状态1 正常2 迟到3 早退4 缺勤5 请假locationvarchar打卡地点经纬度字符串可选字段remarkvarchar备注比如请假原因这张表上要建联合唯一索引(student_id, course_id, attendance_date)这是防止同一个人同一天同一门课重复打卡的关键。很多初学的人建表时忽略这一步后面接口层写得再复杂也挡不住数据库层面的重复数据。请假申请表字段包括学生 ID、课程 ID、请假日期、请假原因、审核状态。审核通过后数据要么把考勤记录的状态改成请假要么在统计时单独排除两种方案都行但要在系统里保持一致。3.2 出勤状态的判定规则与流转逻辑手工考勤时代老师对“迟到”的判断是看学生进教室的时间点系统做自动判定就必须把规则固化成可计算的逻辑。常见做法是以上课时间为基准提前 10 分钟内打卡算正常上课后 15 分钟内打卡算迟到超过 15 分钟算缺勤。早退的判定逻辑相对复杂因为学生离开教室的时间系统很难自动捕获除非做了签退功能否则早退状态一般靠老师手动修改。状态流转的核心代码逻辑是这样的// 判定出勤状态的核心方法 // 以服务器时间为准上课时间从课程表读取 public Integer judgeStatus(AttendanceRecord record, Course course) { LocalTime now LocalTime.now(); // 服务器当前时间 LocalTime start course.getStartTime(); // 上课时间 LocalTime end course.getEndTime(); // 下课时间 if (Duration.between(now, start).toMinutes() 15) { return 2; // 上课后15分钟内打卡迟到 } if (now.isBefore(start.plusMinutes(10))) { return 1; // 课前10分钟内打卡正常 } return 4; // 超过15分钟缺勤 }这里有个容易踩的逻辑坑判断顺序很重要。必须先把“迟到”的区间判断完再判断“正常”最后兜底算缺勤。如果把正常判断写在前面上课后 5 分钟打卡也会被算成正常因为now.isBefore(start)对上课后 5 分钟依然成立得把条件写成“上课前 10 分钟到上课时间”这个区间内才算正常。请假的状态流转稍微复杂一点。学生在请假表提交申请教师审核通过后系统要把对应课程对应日期的考勤记录状态更新为请假。这个操作放在审核接口里做不要等统计时再判断否则统计逻辑里会混入“请假申请已经通过但考勤记录还是空”的脏数据。3.3 初始化数据的写法与常见错误SQL 脚本的前半部分是建表语句后半部分是初始化数据。初始化数据至少要包含一个管理员账号和几个测试学生账号否则系统启动后连登录都进不去。密码字段千万不要明文存储要用 BCrypt 加密。SpringBoot 的spring-security-crypto依赖里自带 BCrypt 工具初始化 SQL 里可以先用预生成的密文插入或者干脆在启动类里写一个CommandLineRunner去做初始账号的创建。初始化数据最常见的错误是外键依赖顺序。学生表、课程表之间的关联属于基础数据考勤记录表引用了学生和课程所以插入顺序必须是先学生再课程最后考勤记录。很多 SQL 脚本一执行就报foreign key constraint fails就是建表时把外键约束提前加了而插入数据时引用表还是空的。解决方法是建表时先不加外键约束等数据初始化完成后再用ALTER TABLE ADD CONSTRAINT补上或者干脆不在数据库层加外键外键关系全在业务代码里维护。另一个常见错误是字符集没指定导致中文全部变成乱码。建库脚本要显式声明CREATE DATABASE attendance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;MySQL 的 utf8 是假的 utf8最多存 3 个字节遇到表情符号就报错。utf8mb4 才是真正的完整 UTF-8考勤记录里万一有学生名字生僻字多用 utf8mb4 最保险。我一般习惯把character_set_server默认值也设成 utf8mb4宁可多写一行配置也不去赌默认值。4. 后端核心实现登录、打卡、统计三个接口就够了4.1 JWT 登录接口与全局拦截器考勤系统不需要像电商平台那样复杂的权限体系角色无非两种学生和教师管理员可以归到教师角色里。JWT 是这个场景最合适的方案——无状态、不存 Session、前端拿到 token 存 localStorage请求时放进 Header 就行。先看 JWT 工具类的核心代码// JWT 生成与解析工具类 public class JwtUtil { // 密钥要足够长生产环境从配置读取不要硬编码 private static final String SECRET your-256-bit-secret-key-change-me; public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET) .parseClaimsJws(token).getBody(); } }登录接口本身没什么玄学就是把前端传来的账号密码拿到数据库比对。密码用 BCrypt 的matches方法验证比对成功返回 token 和用户基本信息。token 的有效期建议设 24 小时考勤系统一天最多登录几次没必要设太长失效后重新登录的成本也低。全局拦截器是鉴权的关键一环。在 SpringBoot 里实现HandlerInterceptor在preHandle方法里取 Header 的 token解析失败就返回 401// 拦截器除了登录接口其他接口都要带 token Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { Claims claims JwtUtil.parseToken(token.substring(7)); request.setAttribute(userId, claims.getSubject()); return true; } catch (Exception e) { response.setStatus(401); return false; } }注册拦截器时注意放行路径Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns(/api/login, /api/register); }这里的坑是路径匹配规则。/**是匹配多级路径/*只匹配一级写错了要么拦截不住接口要么把登录接口也拦了。我一般习惯接口统一用/api/前缀让拦截器只管这个前缀前端静态资源完全不受影响。4.2 打卡接口服务器时间、位置范围、防重复三件事打卡是整个系统里业务规则最密集的接口。前端提交的请求长这样POST /api/attendance/check-in参数是studentId、courseId、longitude、latitude。注意参数里故意没有时间——这是故意的因为前端设备时间不可信改个系统时间就能伪造打卡必须以后端服务器时间为准。打卡接口的核心逻辑分三步// 打卡接口核心逻辑 public Result checkIn(CheckInRequest request) { // 第一步用服务器时间 LocalDateTime now LocalDateTime.now(); // 第二步查课程取上课时间和地点 Course course courseMapper.selectById(request.getCourseId()); if (course null) { return Result.error(课程不存在); } // 第三步位置校验可选 double distance calculateDistance( request.getLatitude(), request.getLongitude(), course.getLatitude(), course.getLongitude()); if (distance 500) { return Result.error(不在课程地点范围内); } // 判定状态并写入记录 AttendanceRecord record new AttendanceRecord(); record.setStudentId(request.getStudentId()); record.setCourseId(request.getCourseId()); record.setAttendanceDate(now.toLocalDate()); record.setCheckInTime(now); record.setStatus(judgeStatus(now, course)); try { attendanceMapper.insert(record); } catch (DuplicateKeyException e) { return Result.error(今天已经打过卡了); } return Result.success(打卡成功); }位置校验用经纬度距离计算的常见做法是Haversine公式把球面距离近似成平面距离。对于教室这种小范围场景国测局坐标系和 WGS84 的偏差影响不大直接用前端navigator.geolocation拿到的经纬度和课程表里预存的教室经纬度做距离判断就行允许范围一般设 200 到 500 米太严了学生在走廊打卡会被误判。防重复打卡是这里最容易漏的点。就算数据库有唯一索引业务层也得先查一次记录是否存在因为DuplicateKeyException的异常信息不够友好直接暴露给用户显示“Duplicate entry”会很奇怪。先查再插的方式会多一次查询但换来的是明确的业务提示“今天已经打过卡了”用户体验好很多。4.3 统计接口SQL 聚合与返回结构统计接口是老师端最关心的功能。出勤率的计算方式要事先约定清楚出勤率 正常人数 迟到人数/ 应到人数缺勤和请假都不算在分母里还是请假不算出勤但也不算缺勤这里推荐的口径是请假不计入应到人数因为请假是经过审批的合理未到把请假算进缺勤会打击学生提交请假申请的积极性。按月统计出勤率的 SQL 写法-- 按月份统计每个班级的出勤率 SELECT DATE_FORMAT(a.attendance_date, %Y-%m) AS month, COUNT(DISTINCT a.student_id) AS total_students, ROUND( SUM(CASE WHEN a.status IN (1, 2) THEN 1 ELSE 0 END) / COUNT(*), 2 ) AS attendance_rate FROM attendance_record a JOIN student s ON a.student_id s.id WHERE s.class_id #{classId} GROUP BY DATE_FORMAT(a.attendance_date, %Y-%m) ORDER BY month;这段 SQL 里有几个容易被忽略的点。COUNT(DISTINCT student_id)是为了防止同一学生多条记录把总数撑大虽然唯一索引已经挡了重复打卡但统计时做一层保护不亏。出勤率算出来是小数用ROUND保留两位在表格里展示更清爽。GROUP BY 的字段和 SELECT 里的聚合字段必须一致MySQL 在ONLY_FULL_GROUP_BY模式下写法不严谨会直接报错。前端拿到的统计接口返回结构建议设计成两层上层是按时间分组的总览下层是明细列表。总览给 ECharts 画折线图用明细给表格展示用。不要把明细数据塞到图表接口里前端处理起来会多一堆无意义的过滤逻辑。接口返回统一封装成Result对象包含code、message、data三个字段。这个习惯一定要养成前后端联调时少了这个封装会非常痛苦——前端每个请求都要手动判断返回状态出错时连错误信息都拿不到。5. 做考勤系统最容易翻车的 5 个坑现象、原因、解决5.1 前后端时间对不上出勤结果全错现象学生早上 8 点 02 分上课8 点 05 分打卡系统判定为正常但课程表里写的上课时间是 8 点整。原因前端打卡时把new Date()的时间直接传给了后端而后端系统时间比真实时间慢了 3 分钟或者前端设备时间和服务器时间不一致。考勤判定依赖的是打卡时间时间基准错了整个判定全错。解决打卡接口不接收前端传的时间参数只用后端服务器的LocalDateTime.now()。如果担心服务器时间不准可以在部署时同步 NTP 时间服务。前端页面可以显示一个“当前时间”供学生参考但接口层面信后端时间。这是考勤系统设计的底线原则不要在前端和后端之间做时间校准的复杂逻辑直接后端一言堂最简单。5.2 跨域请求被浏览器拦截现象前端跑在localhost:5173Vite 默认端口后端跑在localhost:8080前端请求后端接口浏览器控制台报Access-Control-Allow-Origin错误。原因前后端分离后端口不同浏览器同源策略默认阻止跨端口请求。开发环境最常见生产环境如果前端和后端分别用不同域名也会遇到。解决开发环境用 Vite 代理在vite.config.js里配置// vite.config.js 开发环境代理配置 server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }后端也要配置 CORS二者缺一不可。开发期内用后端全放行的简单方式就够了生产环境再用 Nginx 做同域反代把/api转发到后端端口这样浏览器看起来是同源请求跨域问题从根上消失。5.3 Jackson 序列化 LocalDateTime 抛异常现象后端接口返回的数据里带LocalDateTime字段前端拿到的值是一串数字时间戳或者后端直接报InvalidDefinitionException: Java 8 date/time type not supported。原因SpringBoot 默认的 Jackson 不认 Java 8 的时间类型需要额外注册JavaTimeModule。这个问题在 SpringBoot 2.0 之后有所缓解但很多源码包从老版本升级上来时就漏了这个配置。解决在application.yml里加一行配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai再在pom.xml里确认有jackson-datatype-jsr310依赖SpringBoot 的 web 起步依赖里默认带上但如果是精简过的工程就要手动补。前端拿到的LocalDateTime字段会按yyyy-MM-dd HH:mm:ss格式返回展示时不用再做格式化处理。5.4 MySQL 8 连接报 Public Key Retrieval 错误现象后端启动时控制台报Public Key Retrieval is not allowed for user或者连接超时。原因MySQL 8 默认的caching_sha2_password认证方式要求客户端在首次连接时获取公钥而 JDBC 驱动默认禁止这个行为。同时MySQL 8 的连接 URL 必须带时区参数否则驱动会报The server time zone value错误。解决修改application.yml里的数据源 URLspring: datasource: url: jdbc:mysql://localhost:3306/attendance?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruecharacterEncodingutf8mb4 username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver三个关键参数serverTimezone必须填否则时区报错allowPublicKeyRetrievaltrue专门解决公钥获取问题useSSLfalse是本地开发省事生产环境如果要求安全连接再改回 true。characterEncodingutf8mb4防止中文乱码。这些参数组合是最常见的一套配置直接抄过去不会有坑。5.5 前端重复提交打卡请求现象学生手速快对着打卡按钮连点接口被并发请求打了两三次数据库里出现同一个人同一天同一门课的多条考勤记录。原因前端虽然做了按钮disabled状态但异步请求发出后按钮的状态重置时机不对或者学生直接绕过了前端用手工发 HTTP 请求。数据库层面唯一索引没建或者建了但索引字段不全挡不住重复数据。解决三层防线缺一不可。第一层前端在请求发出后立即把按钮置灰收到响应后再恢复第二层数据库建联合唯一索引(student_id, course_id, attendance_date)第三层后端接口在业务代码里先查记录是否存在。三层都做才能既挡住普通用户的手滑也挡住测试工具发的并发请求。数据库唯一索引是最后一道防线也是最可靠的一道只要这条索引在重复数据就进不到表里。6. 部署验证与进阶技巧让系统真正跑起来6.1 部署的最小路径源码包到手后最快跑通的路径是这样先把sql目录下的脚本导入 MySQL建好库和表然后启动后端mvn spring-boot:run或者打成 jar 包运行最后启动前端npm install后npm run dev浏览器打开本地地址。整个过程里最容易出问题的是npm install慢或者报错常见原因是 Node 版本和依赖不兼容。前端装依赖时如果看到node-sass报错直接把它换成sass新版 Node 已经不支持node-sass了。生产环境部署时后端打成 jar 包用nohup java -jar放后台跑前端npm run build产出dist目录交给 Nginx 托管再把/api路径反向代理到后端端口。这样前端请求看起来是同源的跨域问题不存在也方便以后上 HTTPS。6.2 验证清单怎么确认系统可用部署完一定要按业务路径走一遍而不是只看看页面能不能打开。推荐按这个顺序验证管理员登录系统确认能进后台创建一门课程并设置上课时间、教室位置导入几个测试学生账号用学生账号打卡确认状态判定正确——在上课前 10 分钟内打是正常上课后 15 分钟内打是迟到到统计页面确认出勤率数字和手算结果一致测试重复打卡确认被拦截最后测请假流程学生提交申请老师审核状态变成请假。这套流程走完系统才算真正可用。很多人部署完只看了登录页就开始写报告结果答辩时演示打卡功能才发现状态判定逻辑有 bug那时候改代码就来不及了。6.3 一个进阶技巧用 Excel 批量导入学生数据考勤系统的学生数据动辄几百条一条条手动录入不现实这是源码包里经常会缺的功能加上它会让整个系统完整度提升一个档次。用 EasyExcel 或者 Apache POI 做一个导入接口前端用input typefile选 Excel后端读取后校验学号是否重复批量插入数据库。代码逻辑不复杂解析每一行校验学号格式、姓名非空、班级存在通过校验的插入不通过的收集错误信息返回给前端。关键是批量插入要用 MyBatis 的insertBatch或者手动拼 SQL 一次插入多条循环单条插入几百条数据会很慢数据库连接也容易被拖垮。这个功能做完你会发现“系统导入学生数据只要点一下上传”这个点在验收和答辩时比任何炫酷页面都更能打动老师。做这个系统最深的体会是考勤系统技术难度不大难的是把业务规则想清楚——迟到怎么判定、请假怎么流转、统计口径怎么统一。把这些边界条件定义好写代码只是一天的事边界条件没想清楚改来改去才是真正耗时间的地方。希望这些经验能帮你少走点弯路项目跑通了之后你自然会明白哪些地方值得再加一层设计。本文还有配套的精品资源点击获取