如果你正打算做课堂考勤这个课题,或者已经被老师分配了这个题目,你先别急着去网上找代码直接改。这个题目看着经典,但越经典的题目越容易做成一锅粥:功能东拼西凑、前后端接口对不上、论文和代码说的是两套东西。我这两年陆续带过几个用 SSM + Vue 做课堂考勤的毕设项目,踩过不少坑,也总结出了一套还算靠谱的落地路径。这篇就把完整思路、表结构、核心签到逻辑、Vue 端配置、论文写法、联调部署一次说清楚,适合本科毕设、课程设计,也适合想快速搭出一套前后端分离考勤系统的人。
1. 内容整体设计与选题思路
1.1 课堂考勤系统到底要做哪些事
先别动代码,把需求盘清楚。课堂考勤系统的核心角色一般是三类:管理员、教师、学生。
- 管理员:管理教师信息、学生信息、课程信息,生成统计报表,系统日志查看。
- 教师:创建课程、发起一次签到、查看某个班某次课的到课情况、期末导出出勤率。
- 学生:查看自己已选课程、进行课堂签到、提交请假申请、查看个人考勤记录。
功能上最常见的做法是:教师端开启签到,生成一个签到码或二维码;学生输入签到码或扫码完成签到;后端记录签到时间、签到方式、地理位置(可选)等,并计算是否迟到。在此基础上,还可以扩展请假审批、补签申请、考勤统计图表、邮件通知、课表管理等功能。毕设项目不建议贪多,核心闭环做好,再挑两个扩展点做亮,就足够支撑论文的“创新点”章节了。
1.2 为什么是 SSM + Vue 而不直接上 Spring Boot
先说结论:从纯技术角度看,Spring Boot 确实更省事,自动配置、内嵌 Tomcat,几分钟能跑起来。但毕业设计是学术评价的一部分,很多院校的评分标准里明确要求展现“对各层框架的理解”。SSM(Spring + Spring MVC + MyBatis)的优势在于:
- 分层非常清楚,Controller、Service、Mapper 各司其职,论文中画架构图、数据流图都更好画。
- 配置文件是显式的,你对数据库连接、事务管理、Mapper 扫描都看得见摸得着,答辩被问“讲讲这个配置是干嘛的”也不容易慌。
- 参考资源多,网上 SSM 整合案例、报错解决方案比某些偏门框架丰富得多。
Vue 作为前端框架,原因是更直白的:前后端分离是当前主流,Vue 生态成熟,Vue Router、Vuex/Pinia 都有大量实践文章。而且 Vue 对新手友好,比 React 的学习曲线稍缓,热重载也快,页面改完立马能看到效果。
这里补充一点,如果你导师不限制技术栈,Spring Boot + Vue 做起来更顺手,论文里一样能写“Spring 家族技术”。但如果你用的就是 SSM,请务必把 web.xml、spring-mvc.xml、spring-mybatis.xml 这几份配置吃透,答辩几乎必问。
1.3 前后端分离架构的边界划分
SSM + Vue 的项目天然是前后端分离的:后端只提供 RESTful 接口,返回 JSON;前端用 Vue 组件渲染页面,通过 Axios 发异步请求。
在设计阶段就要把边界划好,不然联调的时候会互相甩锅。我的习惯是:
- 后端接口统一以 /api 开头,例如 /api/user/login、/api/attendance/start。
- 所有返回数据统一用 { code, message, data } 这种结构包装。
- 状态码约定清楚:200 成功、401 未登录、403 无权限、500 服务器异常。
- 前端不做业务逻辑判断,只负责把后端返回的 code 翻译成用户能看懂的语言。
这样约定之后,前后端可以并行开发。后端用 Postman 测接口,前端用 Mock 数据先画页面,最后联调时只要关注字段对没对上,省掉大量 Debug 时间。
2. 后端 SSM 框架整合与核心业务实现
2.1 SSM 整合的核心配置
SSM 整合的关键就三份 XML 配置加一个 web.xml,但很多人在这块卡住。先说 maven 依赖,pom.xml 里除了 spring-webmvc、mybatis、mybatis-spring、mysql-connector-java,务必加上 druid 连接池和 jackson-databind,不然返回 JSON 会报错。
spring-mybatis.xml 里主要配数据源、SqlSessionFactory、Mapper 扫描器和事务管理器。一个比较常规且不会出错的写法如下:
<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="typeAliasesPackage" value="com.example.entity"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.dao"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>spring-mvc.xml 里核心是开启注解驱动和组件扫描,同时把静态资源和 JSON 消息转换器配好:
<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper" ref="jacksonObjectMapper"/> </bean> </mvc:message-converters> </mvc:annotation-driven> <context:component-scan base-package="com.example.controller"/>很多同学踩过的一个坑是:Spring 容器和 Spring MVC 容器重复扫描同一个包,导致事务失效。正确做法是 Spring 的扫描排除 Controller,Spring MVC 的扫描只包含 Controller。
2.2 数据库表设计
考勤系统的表不用太复杂,但一定要满足三范式并支持业务闭环。我常用的表设计如下:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| t_user | id, username, password, role, name, email | 统一用户表,用 role 区分管理员/教师/学生 |
| t_course | id, course_name, teacher_id, start_time, end_time, location | 课程信息 |
| t_student_course | id, student_id, course_id | 学生选课关联表 |
| t_attendance | id, course_id, student_id, attendance_date, status, sign_time, sign_type | 考勤记录表 |
| t_leave | id, student_id, course_id, reason, status, apply_time | 请假表 |
这里有个设计取舍:学生和老师分开建表是很多教程的做法,但增量扩展和权限判断比较麻烦。我习惯用一张 t_user 表加 role 字段,前端菜单和接口权限都用 role 控制,后续加管理员角色也方便。
考勤表里的 status 建议用整数存储:0 代表缺勤,1 代表正常,2 代表迟到,3 代表请假。用整数存的优势是统计 SQL 好写,例如统计某门课到课率时直接 SUM(status = 1) 就行。sign_type 可以记为 1 表示扫码签到,2 表示输入签到码,3 表示教师代签,这个字段主要是为了论文里写“支持多种签到方式”时有数据支撑。
建表时还要注意给 course_id 和 student_id 建联合索引,否则后面统计报表时数据量稍大就会出现慢查询。别笑,我的一个学生就是因为没建索引,到答辩演示的时候查一次考勤统计等了三四秒,场面一度很尴尬。
2.3 签到核心逻辑与接口实现
签到的核心逻辑并不复杂,但必须考虑边界情况。教师发起签到时,后端生成一个 4 到 6 位的随机签到码,设置有效时长,默认 5 分钟,存入 Redis 或内存缓存。学生提交签到码时,后端做以下判断:
public Result startSign(Integer courseId, Integer teacherId, int duration) { Course course = courseDao.selectById(courseId); if (course == null || !course.getTeacherId().equals(teacherId)) { return Result.error("课程不存在或无权操作"); } String signCode = String.valueOf((int) ((Math.random() * 9 + 1) * 1000)); // 实际开发中用 redis 存储,key 为 "sign:" + courseId,value 为签到码 signCache.put(courseId, signCode, duration); return Result.success("签到已开启", signCode); }学生提交签到接口,除了校验签到码是否正确,还要判断当前时间是否在有效范围内,以及该学生是否已经签到过,防止重复提交:
public Result doSign(Integer courseId, Integer studentId, String signCode) { String cacheCode = signCache.get(courseId); if (cacheCode == null) { return Result.error("签到已结束"); } if (!cacheCode.equals(signCode)) { return Result.error("签到码错误"); } int count = attendanceDao.countToday(courseId, studentId); if (count > 0) { return Result.error("您已签到,请勿重复提交"); } Attendance record = new Attendance(); record.setCourseId(courseId); record.setStudentId(studentId); record.setAttendanceDate(new Date()); record.setSignTime(new Date()); // 判断是否迟到:课程开始时间前5分钟到课程开始后20分钟内算正常 record.setStatus(judgeStatus(courseId)); record.setSignType(1); attendanceDao.insert(record); return Result.success("签到成功"); }关于“迟到”判定,别在代码里写死规则。因为不同学校对迟到的定义不一样,我建议把这个规则放到配置表或者课程表的字段里,由教师创建课程时设定,比如“上课前5分钟到上课后15分钟内都算正常”。这样论文里还能多写一个“灵活考勤规则设置”的功能点,答辩时能讲的东西一下子又多了一个。
3. 前端 Vue 环境搭建与功能实现
3.1 Vue 环境配置和项目创建
前端这一块,很多同学上来就卡在环境上。首先要确保本机装了 Node.js,建议装 LTS 版本,作者经常看到有人因为 Node 版本过新导致 node-sass 装不上,然后整个人心态崩掉。装完 Node 之后,npm 会自动带上,然后把 npm 源切到国内镜像,装依赖能快一大半:
npm config set registry https://registry.npmmirror.comVue CLI 创建项目,或者直接用 Vite 都可以。如果你用的教材和网课是 Vue CLI 那一套,建议按 Vue CLI 来,因为报错案例多,资料好查:
npm install -g @vue/cli vue create attendance-web创建过程中会让你选预设,选 Manually select features,然后勾选 Babel、Router、Vuex、CSS Pre-processors。创建完成后,进入目录再装 Axios 和 Element UI(或 Element Plus,取决于你 Vue 是 2 还是 3)。这里我提醒一下,很多教学楼 Wi-Fi 环境下载慢,装依赖报错大概率是网络问题,清理缓存重试,或者换镜像源,千万不要一股脑去卸了重装。
安装完成后,熟悉一下目录结构。src/router 里配置路由,src/api 里封装接口请求,src/views 里放页面组件。还有一个细节:Vue Devtools 插件一定要装,调试组件状态和路由跳转能省一半时间,作者见过太多学生用 console.log 调试响应式数据,最后被搞崩溃的。
3.2 路由设计与登录鉴权
考勤系统按角色分页面,所以路由要区分管理员、教师、学生三种视角。我的做法是:
- 登录页是公开路由。
- 登录成功之后,根据返回的 role 字段动态拼接该角色对应的路由表,用
router.addRoutes动态添加(Vue 3 里是router.addRoute)。 - 全局前置守卫里统一判断:没有 token 就跳登录页,有 token 但访问的路由不在当前角色权限内就跳 403 页。
router.beforeEach((to, from, next) => { const token = sessionStorage.getItem('token') if (to.path === '/login') { next() } else if (!token) { next('/login') } else { next() } })这里要注意一个坑:动态路由配合路由守卫时,刷新页面会导致动态添加的路由丢失,所以刷新后要重新拉取用户信息并重新挂载路由。常见的解决办法是把用户信息和路由表缓存到 sessionStorage,刷新时先读缓存再 addRoute。
3.3 Axios 封装与跨域问题
Axios 封装的核心有两点:统一处理请求头里的 token,统一处理 HTTP 错误和业务错误。我常用的一个封装模板:
import axios from 'axios' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) service.interceptors.request.use(config => { const token = sessionStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.message)) } return res }, error => { return Promise.reject(error) } ) export default service跨域问题是前后端分离项目的必经之路。后端就是 SSM 的项目,最简单的方式是在 Controller 或拦截器里统一加 CORS 响应头,或者直接用 Spring MVC 的@CrossOrigin注解。但我建议前后端联调阶段使用 vue.config.js 里的 devServer.proxy 做代理,这样浏览器看到的请求是同源的,省去很多跨域带来的奇奇怪怪的问题:
devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }这样配置之后,前端代码里请求 /api/xxx 就会转发到后端的 localhost:8080。等部署上线时,再把后端接口放到同一域名下,或者用 Nginx 反向代理,前端代码基本不用改。
3.4 考勤签到页和统计报表页实现
签到页是学生最重要的页面。我的实现思路是:
- 页面加载时调用后端 /api/attendance/current 接口,查询当前用户所有正在签到中的课程。
- 每条课程显示课程名称、教师、上课地点、签到剩余时间,旁边放一个“签到”按钮。
- 点击签到后弹窗让用户输入签到码,提交到 /api/attendance/sign。
- 签到成功后按钮置灰,并显示签到成功的时间,防止短时间重复点击造成重复提交。
页面上的倒计时可以用 setInterval 实现,但要注意在组件销毁时清除定时器,否则页面切走之后回调依然在执行,控制台报错不说,还容易造成内存泄漏。
统计报表页面建议用 ECharts 画柱状图和饼图:柱状图展示某门课最近 10 次课的出勤人数变化,饼图展示某次课的正常/迟到/缺勤/请假占比。ECharts 的引入方式不复杂,直接 npm 安装后在组件里引入即可,但要注意图表容器需要有明确的高度,否则图表渲染不出来,这个是新手高频踩坑点。
4. 论文架构与撰写方法
4.1 论文章节结构与每个章节该写什么
毕设论文一般都不少于 1.5 万字,常见的结构是:
| 章节 | 内容 | 建议篇幅 |
|---|---|---|
| 摘要 | 系统背景、主要功能、采用技术、测试结果 | 300字左右 |
| 第1章 绪论 | 课题背景与意义、国内外研究现状、主要工作 | 2000字以上 |
| 第2章 需求分析 | 可行性分析、功能需求、非功能需求、用例图 | 2500字以上 |
| 第3章 系统设计 | 架构设计、功能模块设计、数据库设计、E-R图 | 3000字以上 |
| 第4章 系统实现 | 各模块实现截图、核心代码片段、关键逻辑说明 | 3500字以上 |
| 第5章 系统测试 | 测试环境、功能测试用例表、界面测试、性能测试 | 1500字以上 |
| 总结与展望 | 系统解决了什么问题、还存在哪些不足、后续方向 | 800字左右 |
很多同学容易把第4章写成代码粘贴大会,一整页整页贴代码,导师看一眼就烦。正确做法是每个功能模块先放截图,再贴 10 到 20 行核心代码,然后用自然语言解释这段代码解决了什么问题。比如签到模块,贴判断签到码和重复签到的那段逻辑就够了,不要从 Controller 到 Mapper 全贴一遍。
需求分析章节里,用例图、类图、流程图是重头戏。用 Visio、ProcessOn 或者 draw.io 画清楚即可,注意画图风格统一,别一个图是黑白的一个图是彩色的,这些细节评审老师很在意。
4.2 论文与代码如何保证一致
我见过最离谱的情况是:论文里数据库表有 8 张,实际代码里只建了 5 张;论文里写了“支持签到码和二维码两种签到方式”,代码里只有签到码输入框;论文的架构图画的是 Spring Boot + Redis,代码却是纯 SSM。这种硬伤在答辩时被老师翻出来,基本等于提前送命。
我的建议是,论文和开发同步进行,不要等代码写完了再补论文。开发每完成一个模块,就顺手把该模块的截图、表结构、核心逻辑写进论文草稿里。等开发完成,论文的初稿其实就已经成型了大半,后面只需要润色和补充测试章节就行。
另外,论文中的每个功能点都必须在代码里能找到对应实现,代码里的每个核心类都要能在论文的模块设计里找到位置。哪怕是一些很小的功能,比如“教师可以导出 Excel 考勤表”,论文设计章节写了,代码里就一定要有这个导出按钮,不然就是给自己挖坑。
4.3 答辩演示准备的几个要点
答辩演示环节比很多人想象的重要,作者听过太多“功能做好了但演示翻车”的案例。几个经验:
- 准备一套干净的演示数据,不要用测试时插入的“测试1”“测试2”这类脏数据。演示前把数据库恢复到初始状态,各项数据要看起来真实、规整。
- 提前把各角色账号准备好:管理员账号、教师账号、学生账号各一个,密码统一设成容易输入的,别在台上因为密码打错被卡住。
- 演示流程要提前走三遍以上,尤其是“教师发起签到 -> 学生扫码签到 -> 教师查看统计”这条主线流程,确保每一步都不会因为网络慢、缓存过期等情况挂掉。
- 万一演示时后端崩了,代码层面临时修不好,还有一个终极大法:把所有核心页面截图放进 PPT 里,说“这是我在本地环境的执行效果”,虽然有点投机取巧,但至少不会在台上冷场。
还有一个答辩高频问题要提前准备:“你的系统和其他考勤系统相比,有什么优势或创新点?”如果你没做算法、没做大数据分析,就老老实实回答“本系统在业务流程上覆盖了课内考勤、请假、补签、统计的完整闭环,并在界面交互上针对三种角色进行了设计优化”,比硬吹“基于人工智能的考勤分析”靠谱得多。
5. 前后端联调、部署与常见问题排查
5.1 前后端联调的常见卡点
前后端联调阶段,大部分时间不是在写代码,而是在对着接口文档核对字段。最常见的问题:
- 后端返回的时间是 Date 对象,前端拿到的是时间戳字符串,显示格式不对。解决办法是后端在实体类的日期字段上加上
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解。 - 后端接口返回的字段用的是下划线命名(如 student_name),前端 JSON 里用的是驼峰命名(studentName),导致页面渲染为空。解决办法是 MyBatis 配置里开启
mapUnderscoreToCamelCase,或者在接口统一转一次。 - 分页查询时,前端传 pageNum 和 pageSize,后端返回的 total 是总数,但是前端忘了在表格组件里配置 total 的来源,导致分页组件总显示 1 页。这类问题只要提前约定好返回结构就不会出现。
我的习惯是写接口时同时维护一份简单的接口文档,不用 Swagger 那么重,用 Markdown 或者在代码注释里写好也行,至少包含 URL、请求参数、返回示例。有了这个文档,前后端并行开发时基本不会因为字段名反复扯皮。
5.2 部署打包和环境问题速查
项目做完了要提交或者部署到服务器,后端打成 war 包丢进 Tomcat,或者打成 jar 包直接因为有内嵌容器。但如果你用的是传统 SSM + 外部 Tomcat,注意以下这些点:
- 数据库连接配置不要写死 root 密码在代码里,建议放到 jdbc.properties,部署时只改配置文件就行。
- 文件上传功能如果是写在本地磁盘,部署到服务器后要留意目录是否存在、权限是否足够,否则上传图片会静默失败。
- 前端
npm run build打成静态文件后,直接放到 Tomcat 的 webapps 下也可以,但刷新页面会出现 404,因为 Vue Router 用的是 history 模式。解决办法有两个:一是把模式改成 hash,二是给 Tomcat 手动配置一个转发规则。毕设场景我建议直接用 hash 模式,虽然地址栏多一个 # 号,但省心很多。
下面把作者和学生实际操作中遇到的典型问题整理成一个速查表,直接照着排查就行:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 后端启动时报端口被占用 | 8080 端口被其他程序占用 | 改 Tomcat 端口或杀掉占用进程 |
| 启动时报 ClassNotFoundException | Maven 依赖缺失或版本冲突 | 执行 mvn clean package 后重新导入,检查依赖树 |
| 连接数据库报 Access denied | 数据库账号密码错误或权限不足 | 核对 jdbc.properties,本地 MySQL 重启后重试 |
| MyBatis 查询结果全是 null | 表字段与实体类属性对不上 | 开启 mapUnderscoreToCamelCase 或使用别名 |
| 前端 npm install 报错 | Node 版本太高或太低、网络问题 | 使用 LTS 版本,切换镜像源,删除 node_modules 重装 |
| 跨域请求被拦截 | 后端未加 CORS 配置 | 使用 devServer proxy 或后端加 @CrossOrigin |
| 前端页面空白 | 路由配置错误、js 引入路径不对 | 检查控制台报错,确认 publicPath 配置 |
| idea 里 import 的包标红 | Maven 没有自动导入依赖 | Maven 面板点击 Reload All Projects |
5.3 线上演示环境的备份与回滚
毕设答辩前,最好把项目完整跑在一个稳定的演示环境里。我的经验是数据库一定要提前备份,导出 SQL 文件放到网盘或者 U 盘。答辩现场经常会发生老师随意点了几个按钮,把测试数据改乱或者删掉的情况,如果数据库没备份,当场恢复数据非常狼狈。
备份数据库就一条命令的事:
mysqldump -u root -p attendance_db > attendance_backup.sql恢复也简单,把备份文件导进去就行。另外,演示环境建议禁用“删除成绩”“清空考勤”这类高风险按钮,或者至少加一个二次确认弹窗。这不是逃避问题,而是降低演示出错概率的合理设计。
还有一个小细节,代码里所有涉及本地绝对路径的地方,比如图片上传路径、日志目录,都改成相对路径或者通过配置读取。否则换一台电脑运行项目,这些莫名其妙的地方就会报错,还不好排查。
最后分享一点个人经验,毕设做课堂考勤这个题,最大的陷阱不是技术难度,而是“一眼看去太简单”。我建议你在完成基础功能之后,至少增加一个有区分度的扩展点,比如:导出月考勤汇总 Excel、课程维度的出勤趋势图表、迟到早退规则自定义、课表导入解析,或者按班级批量导入学生名单。这些功能工作量不算大,但能让你的系统从“例子”变成“作品”,论文里的亮点更充分,答辩时也更有底气。选题常见不要紧,能让一个常见题目做出完整的工程质量和清晰的业务闭环,本身就是合格毕业设计的标准。