SpringBoot+Vue大学生考勤系统、Java+MySQL全栈开发,这几个关键词放在一起,基本就是我接得最多的毕设需求模板。这类系统本质上是把传统的纸质点名、Excel统计搬到线上,属于典型的管理信息系统,技术难度适中、业务场景明确、功能边界清晰,非常适合用来做毕业设计或者课程设计。源码也好、从零写也好,思路对了,整个系统的架构、表结构、权限模型和核心接口就能一次定下来。这篇文章就把我做这类系统的完整思路、数据库设计、核心接口实现、前端页面拆解,以及实际踩过的坑一次讲清楚,给准备做毕设、课设或者想练手全栈的同学一份能直接照着走的实操指南。
1. 整体设计与技术选型的底层逻辑
1.1 为什么这套技术栈是“开箱即用”的组合
先聊技术选型。SpringBoot+Vue+MySQL这套组合,放在全栈开发里早就是“校招简历标配”级别的存在,但对于做毕设和课设的同学来说,它的价值不只是“标配”这么简单,而是每一层都有现成生态可以借力。
后端用SpringBoot,最大的好处是自动装配和起步依赖。你不需要像Spring MVC时代那样写一堆XML配置文件,只要引入spring-boot-starter-web,一个@SpringBootApplication启动类就能跑起来。对于学生项目来说,这意味着可以从繁琐的配置里解放出来,把时间花在业务逻辑上。而且SpringBoot和MyBatis、MyBatis-Plus的集成已经非常成熟,数据库操作基本就是写Mapper接口和XML映射的体力活。
前端用Vue,核心优势是组件化和响应式数据绑定。对于考勤系统这种页面结构相似、表单密集、列表密度高的管理类系统,Vue的双向绑定让表单校验、状态切换、列表刷新变得非常直观。配合Vue Router做路由控制、Vuex或Pinia做用户状态管理、Element UI或Element Plus做后台界面,一天就能搭出完整的后台管理界面框架。
数据库用MySQL,这个没什么悬念。单机部署、表结构清晰、SQL查询灵活、Navicat等可视化工具也方便调试,对于考勤系统这种数据量级在“单学校单学期几万条记录”的应用,MySQL的性能完全够用。毕设项目不需要一上来就上Redis做缓存、上ES做搜索,那是给自己增加工作量。
1.2 系统画像:谁在用、解决什么痛点
很多同学拿到这类题目第一反应是“考勤系统不就是签到打卡吗”,这样想就把项目做窄了。实际的考勤管理场景里有三类角色,每个角色关心的东西完全不同:
- 学生:关心我今天这节课有没有签上、本周出勤是否正常、缺勤了怎么请假、请假审批通没通过。
- 授课教师:关心我开的课程有哪些学生选课、每次课学生到课情况、出勤率如何、谁经常缺勤、批量导出某门课的考勤统计。
- 系统管理员(教务或学工角色):关心学生信息、班级信息、教师信息、基础课程数据的维护,以及全系统考勤记录的汇总报表。
所以这个系统最少要有三个权限维度,然后对应下来,核心功能至少包括:
- 用户模块:登录、注册、个人信息维护、密码修改
- 考勤模块:按课程打卡、签到时间判定、迟到早退标记、签到记录查看
- 请假模块:学生提交请假、教师审批、请假与考勤记录关联
- 课程与选课模块:课程创建、学生选课、课程表维护
- 统计模块:按课程、班级、学生个人三维度统计出勤情况,支持导出Excel
把这些功能理出来,整个项目的范围就清楚了。做毕设的时候最怕一开始就闷头写代码,先把用例图画清楚、把角色和功能边界定下来,后面写代码就是翻译工作。
1.3 项目结构怎么分层才不容易烂尾
我见过不少同学的项目烂尾,不是代码写不出来,而是项目结构乱成一锅粥,查一个Bug要在十几个类里跳来跳去。考勤系统这种管理信息系统,推荐直接沿用经典三层架构,后端目录大致是这样:
src/main/java ├─ com.example.attendance │ ├─ config // 跨域配置、拦截器注册、MyBatis配置 │ ├─ controller // 控制层:接收请求、参数校验、返回结果 │ ├─ service // 业务层:核心业务逻辑处理 │ ├─ mapper // 数据访问层:Mapper接口 │ ├─ entity // 实体类 │ ├─ dto // 请求/响应DTO │ ├─ vo // 前端展示数据对象 │ ├─ common // 公共类:返回值封装、异常处理、常量 │ ├─ security // JWT工具类、拦截器、上下文 │ └─ AttendanceApplication.java前端Vue项目则按照页面模块划分:
src ├─ api // 接口请求封装,按模块拆分 ├─ assets // 静态资源 ├─ components // 通用组件 ├─ layout // 布局组件:侧边栏、顶栏、面包屑 ├─ router // 路由表 ├─ store // 状态管理(Pinia或Vuex) ├─ views │ ├─ login │ ├─ dashboard │ ├─ attendance │ ├─ leave │ ├─ course │ ├─ user │ └─ statistics └─ utils // 请求工具、时间格式化、校验工具这样分的好处是前后端都可以独立测试和迭代。我在实际开发里习惯先把后端的接口用Postman全部调通,再写前端页面,这样定位问题就快很多——数据层的错误直接在接口层面暴露,不会跟前端渲染的问题混在一起。
2. 数据库设计与核心表结构
2.1 从需求到建表的思考路径
数据库设计是这类系统的命脉。考勤系统的业务逻辑看起来不复杂,但表设计稍有不慎,后面写统计SQL的时候就会想骂人。
我建议先梳理出几个核心实体:
- 用户(User):包含学生、教师、管理员三种角色,用
role字段区分 - 班级(Class):学生属于班级,一个班级对应一个班主任教师
- 课程(Course):课程有名称、时间、上课地点,归属某个教师
- 选课记录(StudentCourse):学生和课程是多对多关系,需要中间表
- 考勤记录(Attendance):一条记录代表某个学生某次课的一个状态
- 请假申请(LeaveApplication):请假单,关联学生和课程,审批后回写考勤
我把建表SQL的核心片段放出来,这个是实际在项目中验证过的版本,字段做了精简,但核心关系都保留着。
-- 用户表 CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名(学号/工号)', `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)', `real_name` varchar(30) NOT NULL COMMENT '真实姓名', `role` tinyint NOT NULL DEFAULT '3' COMMENT '角色:1管理员 2教师 3学生', `class_id` bigint DEFAULT NULL COMMENT '班级ID,学生专属', `phone` varchar(20) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 课程表 CREATE TABLE `course` ( `id` bigint NOT NULL AUTO_INCREMENT, `course_name` varchar(100) NOT NULL, `teacher_id` bigint NOT NULL COMMENT '授课教师ID', `week_day` tinyint NOT NULL COMMENT '星期几 1-7', `start_time` time NOT NULL COMMENT '上课时间', `end_time` time NOT NULL COMMENT '下课时间', `location` varchar(100) DEFAULT NULL, `semester` varchar(20) NOT NULL COMMENT '学期,如2024-2025-1', PRIMARY KEY (`id`), KEY `idx_teacher` (`teacher_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 考勤记录表 CREATE TABLE `attendance_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `student_id` bigint NOT NULL, `course_id` bigint NOT NULL, `attendance_date` date NOT NULL COMMENT '考勤日期', `status` tinyint NOT NULL COMMENT '1正常 2迟到 3早退 4缺勤 5请假', `sign_in_time` datetime DEFAULT NULL COMMENT '签到时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_stu_course_date` (`student_id`, `course_id`, `attendance_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;请假表同样重要,这个表不仅是业务记录,还要承担“考勤状态回写”的角色:
CREATE TABLE `leave_application` ( `id` bigint NOT NULL AUTO_INCREMENT, `student_id` bigint NOT NULL, `course_id` bigint NOT NULL, `leave_date` date NOT NULL COMMENT '请假日期', `reason` varchar(255) NOT NULL, `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待审批 1通过 2驳回', `apply_time` datetime DEFAULT CURRENT_TIMESTAMP, `audit_time` datetime DEFAULT NULL, `auditor_id` bigint DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;2.2 唯一约束与状态设计:两个关键决策
考勤表里我特意加了一条UNIQUE KEY uk_stu_course_date (student_id, course_id, attendance_date),这是一个非常重要的细节。这条唯一约束保证了同一个学生同一天同一门课只会有一条考勤记录,在代码层面即使并发请求、重复打卡,数据库也会兜底拒绝重复插入。
状态字段用tinyint而不是字符串,这是我在实际项目里比较坚持的做法。很多新手喜欢存“正常”、“迟到”这样的中文,看起来直观,但后端比较状态、前端筛选统计的时候,字符串比较容易出错,而且中文一旦数据量大了还有编码和存储空间的问题。用数字存,然后定义常量或者枚举去映射,是最稳妥的做法。
2.3 为什么要加一个“学期”字段
课程表里我放了一个sementer字段,别小看它。考勤系统如果只做单学期,确实用不上学期隔离。但现实中一个学校会有多个学期的数据累积,如果统计的时候不做学期区分,教师看到的数据就会混在一起。加了这个字段之后,查询记录、统计出勤率的SQL都要带上学期的过滤条件,那才是符合实际业务形态的做法。从毕设答辩的角度来说,这个字段也方便演示“往届数据归档”这类扩展点。
3. 后端SpringBoot实操要点
3.1 登录认证:JWT的前后端完整闭环
考勤系统涉及三类角色,权限控制躲不开登录认证。我用的方案是JWT(JSON Web Token)配合Spring Boot拦截器,这也是目前这类系统最主流的做法,比Session更符合前后端分离的需求。
整体流程是这样的:
- 用户提交用户名密码,后端校验通过后签发一个Token返回
- Token里携带用户ID、角色等信息,并设置过期时间(我习惯设7天,方便学生演示)
- 前端拿到Token后存到localStorage,并在每次请求的Header里带上
Authorization: Bearer xxx - 后端拦截器校验Token有效性,解析用户信息并放行
JWT工具类的核心逻辑大概是这样的:
public class JwtUtil { // 实际项目中这个密钥要放到配置文件中 private static final String SECRET = "your-32-byte-secret-key-for-hs256"; private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L; // 7天 public static String createToken(Long userId, Integer role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }拦截器的注册也很简单,实现HandlerInterceptor接口,在preHandle方法里从请求头取Token并校验,如果无效就返回401状态码。然后在Web配置类里配置拦截路径,把登录接口、静态资源放行,其余全部拦截。
注意:拦截器拦截的是“请求”,不是“按钮”。前端可以隐藏按钮,但是后端接口同样必须校验权限。我在项目里就是每次请求都从Token里解析出role,然后在教师接口和管理员接口上加角色校验,这样即使有人绕过前端直接调接口,也拿不到权限之外的数据。
3.2 签到接口:迟到判断与防重复打卡
签到是这个系统的核心业务,也是最容易出现幺蛾子的地方。我先说一个最简单的设计方案,再指出坑在哪里。
签到的参数就是课程ID和当前时间,后端需要做三件事:
- 确认当前时间是否在该课程的时间窗口内
- 确认该学生该课程当天是否已有考勤记录
- 根据签到时间与上课时间的差值判断状态(正常/迟到)
核心逻辑像这样:
public Result signIn(Long courseId, Long studentId) { LocalTime now = LocalTime.now(); Course course = courseMapper.selectById(courseId); if (course == null) { return Result.error("课程不存在"); } // 判断时间窗口 LocalTime courseStart = course.getStartTime(); if (now.isBefore(courseStart.minusMinutes(15))) { return Result.error("签到未开始"); } if (now.isAfter(course.getEndTime())) { return Result.error("签到已结束"); } // 查是否已签到 AttendanceRecord exist = attendanceMapper.selectByStuCourseDate( studentId, courseId, LocalDate.now()); if (exist != null) { return Result.error("请勿重复签到"); } // 迟到判定:上课时间后15分钟内算迟到,再晚算缺勤 int status = 1; if (now.isAfter(courseStart)) { status = 2; // 迟到 } AttendanceRecord record = new AttendanceRecord(); record.setStudentId(studentId); record.setCourseId(courseId); record.setAttendanceDate(LocalDate.now()); record.setStatus(status); record.setSignInTime(new Date()); attendanceMapper.insert(record); return Result.success(record); }这里有个很重要的细节:Unresolved: 上课时间过了之后学生还能不能签到?。我这里的策略是“上课后15分钟内仍然允许签到,但标记为迟到”,超过15分钟直接拒绝签到,标记为缺勤要由教师手动补录或者在考勤管理页面里手动改为缺勤。这个时间阈值我抽到了常量类里,方便调整。做毕设的同学可以根据自己学校的规则去调整,但一定不要写死在多个地方。
3.3 请假审批:状态机与回写逻辑
请假审批涉及状态流转,学生提交请假后,状态是“待审批”,教师通过后状态变成“通过”,同时需要往考勤表插入一条“请假”状态的记录,不然统计出勤率的时候,这个学生既没有签到记录也没有请假记录,就会被当成缺勤。
我在实际项目中是把请假和考勤两个操作放在同一个事务里处理的。教师审批通过的时候,先查这个学生这天的课程考勤记录是否存在——如果存在,就把状态改成5(请假);如果不存在,就插入一条请假状态的新记录。这一步用事务注解@Transactional保证两步操作是原子性的,不会出现审批通过了考勤却没同步的中间状态。
@Transactional public void approveLeave(Long leaveId, Long auditorId) { LeaveApplication leave = leaveMapper.selectById(leaveId); if (leave == null || leave.getStatus() != 0) { throw new BusinessException("审批单不存在或已处理"); } leave.setStatus(1); leave.setAuditTime(new Date()); leave.setAuditorId(auditorId); leaveMapper.updateById(leave); // 同步到考勤记录 AttendanceRecord record = attendanceMapper.selectByStuCourseDate( leave.getStudentId(), leave.getCourseId(), leave.getLeaveDate()); if (record == null) { record = new AttendanceRecord(); record.setStudentId(leave.getStudentId()); record.setCourseId(leave.getCourseId()); record.setAttendanceDate(leave.getLeaveDate()); record.setStatus(5); attendanceMapper.insert(record); } else { record.setStatus(5); attendanceMapper.updateById(record); } }3.4 统计报表:三条核心SQL
考勤系统的统计功能是答辩时的加分项,也是很多同学写起来容易卡壳的地方。我这里整理三条最核心的统计SQL,基本能覆盖需求。
按学生个人统计出勤率:
SELECT stu.real_name, COALESCE(SUM(CASE WHEN ar.status IN (1,2,3) THEN 1 ELSE 0 END), 0) AS attend_count, COALESCE(SUM(CASE WHEN ar.status = 5 THEN 1 ELSE 0 END), 0) AS leave_count, COALESCE(SUM(CASE WHEN ar.status = 4 THEN 1 ELSE 0 END), 0) AS absent_count, COUNT(ar.id) AS total_count FROM sys_user stu LEFT JOIN attendance_record ar ON ar.student_id = stu.id WHERE stu.role = 3 AND stu.id = #{studentId} AND ar.attendance_date BETWEEN #{startDate} AND #{endDate} GROUP BY stu.id;按班级统计出勤率:
SELECT c.class_name, COUNT(ar.id) AS total, SUM(CASE WHEN ar.status IN (1,2,3) THEN 1 ELSE 0 END) AS present FROM attendance_record ar JOIN sys_user stu ON ar.student_id = stu.id JOIN class c ON stu.class_id = c.id WHERE ar.attendance_date BETWEEN #{startDate} AND #{endDate} GROUP BY c.id;按课程统计各状态数量:
SELECT ar.course_id, co.course_name, SUM(CASE WHEN ar.status = 1 THEN 1 ELSE 0 END) AS normal_count, SUM(CASE WHEN ar.status = 2 THEN 1 ELSE 0 END) AS late_count, SUM(CASE WHEN ar.status = 3 THEN 1 ELSE 0 END) AS early_count, SUM(CASE WHEN ar.status = 4 THEN 1 ELSE 0 END) AS absent_count, SUM(CASE WHEN ar.status = 5 THEN 1 ELSE 0 END) AS leave_count FROM attendance_record ar JOIN course co ON ar.course_id = co.id WHERE ar.attendance_date BETWEEN #{startDate} AND #{endDate} GROUP BY ar.course_id, co.course_name;用LEFT JOIN是为了保证即使某学生没有考勤记录,也能在列表里显示出来,这个非常关键,不然统计结果会少人。
4. 前端Vue实现的关键细节
4.1 路由守卫与权限控制
前端权限控制的本质是“体验控制”,真正的安全在后端。但路由守卫做的好不好,直接决定系统好不好用。
我用的方案是在Router的beforeEach钩子里做三层判断:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.path === '/login') { next(); return; } if (!token) { next('/login'); return; } const role = localStorage.getItem('role'); if (to.meta.role && !to.meta.role.includes(Number(role))) { next('/dashboard'); // 无权访问,跳回首页 return; } next(); });这里有个小技巧:to.meta.role存储的是允许访问的角色数组,比如教师路由meta: { role: [2] },管理员路由meta: { role: [1] },学生路由不限制。这样配权限的时候一眼就能看清楚。
4.2 Axios封装与请求拦截
每个前端同学的request.js可能长都不一样,但核心都差不多。我分享一个省心版的写法:
const request = axios.create({ baseURL: '/api', timeout: 10000 }); request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); request.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { ElMessage.error(res.message || '请求失败'); if (res.code === 401) { localStorage.clear(); router.push('/login'); } return Promise.reject(new Error(res.message)); } return res; }, error => { ElMessage.error(error.message || '网络异常'); return Promise.reject(error); } );拦截器统一处理后,每个页面的接口调用代码就很干净了。特别是401统一跳登录这个逻辑,几乎每个接口都要用到,不封装的话每个页面写一遍容易漏。
提示:后端接口统一返回
{ code, message, data }结构,前端拦截器只管处理data。这个约定我建议在项目一开始就定好,因为接口多了之后,每个接口都单独处理错误会很痛苦。
4.3 签到页面的交互设计与关键实现
学生的签到页面是使用频率最高的页面,交互设计上要做减法。我做的版本就只有三样东西:
- 当前时间显示(实时刷新)
- 当天待签到的课程列表
- 每个课程右侧一个“签到”按钮
签到时按钮要加loading状态,防止用户重复点击。前端在点击后立即把按钮置灰,后端再通过唯一约束兜底防重,双保险。
还有一个我特别想提醒的点:日期和时间的格式化。后端返回的LocalDateTime走JSON序列化后是类似2025-07-04T10:30:00的格式,前端直接显示会很丑,而且new Date()解析这个格式在个别浏览器上还会出问题。我统一在后端配置了Jackson的日期格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8同时在Vue的utils/date.js里封装了格式化工具,碰到日期筛选范围选yyyy-MM-dd,碰到列表展示用yyyy-MM-dd HH:mm。前后端统一用“东八区+字符串格式”传值,踩过的时区坑就得这样填。
4.4 教师统计面板的可视化思路
统计模块不要只做表格。我用ECharts做了一个柱状图展示班级出勤率对比,一个折线图展示某门课程近几周的出勤趋势,再辅以明细表格。工作量不大,但是整个系统的完成度瞬间提升,这个在答辩里是很加印象分的。
ECharts在Vue项目里用vue-echarts组件封装,或者直接在页面里引入echarts再手动初始化,都行。我是用onMounted里初始化图表,然后调用统计接口拿到数据后用setOption更新。要注意组件销毁时要调用dispose()释放实例,不然页面来回切换会内存泄漏。
5. 前后端联调与部署避坑指南
5.1 跨域问题,一次讲透
前后端分离项目必会遇到跨域问题。前端开发服务器跑在8080端口,后端跑在8081端口,浏览器的同源策略会拦截跨端口请求。
解决方案有很多种,我用的最省事的是后端加一个CorsFilter:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }在开发阶段允许所有来源没问题,但部署到生产环境时,建议把allowedOriginPattern改成实际的域名或IP,防止任意网站都能调用你的接口。
5.2 数据库中文乱码,老问题重提
MySQL 5.7创建的数据库默认字符集可能是latin1,插入中文数据就变成乱码或者直接报错。创建数据库时用一条命令解决问题:
CREATE DATABASE attendance_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;同时数据库连接串也要加上字符集参数:
jdbc:mysql://localhost:3306/attendance_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/ShanghaiserverTimezone这个参数特别重要,不指定的话,Java 8的时间类型和MySQL交互时经常碰到变UTC时区导致的8小时偏差问题。
5.3 部署方案,我建议的“最小可靠路径”
毕设项目通常还要现场演示,部署环境往往是一台配置很一般的机器,或者是大家共用的实验室服务器。我推荐的部署方案是“Jar包+Nginx”:
- 后端打完Jar包后,用
nohup java -jar attendance-server.jar &跑起来 - 前端执行
npm run build,生成dist目录 - Nginx里配置一条server,把静态文件指向
dist目录,同时把/api开头的请求转发到后端的8081端口
Nginx的关键配置片段:
server { listen 80; server_name localhost; location / { root /opt/attendance/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这种部署方式的好处是只占用一个80端口,看起来就是一个完整的网站,访问路径干净,也不用处理复杂端口开放。
6. 常见问题与排查技巧实录
6.1 花了两天排查出的“8小时时差”
有个同学在我这咨询,说签到接口明明传的是14:00,数据库里存的是06:00,整个人都懵了。排查思路很简单:先用查询接口打印后端收到的时间,再用日志看插入前的时间,最后定位到是Jackson反序列化时使用了默认的UTC时区,而后端的系统时区又是东八区,导致LocalDateTime序列化出来带了一个隐形的时区偏移。
解决方法是全局配置时间字段统一处理。除了上面说的application.yml里配Jackson,实体类的createTime字段加@TableField(fill = FieldFill.INSERT)配合自动填充处理器,保证所有时间统一由服务端生成,不依赖数据库默认值。
6.2 前端明明有数据,表格却空白
这个是我自己踩过最愚蠢的坑,也很有代表性。Vue的el-table绑定的data字段是数组,但后端返回的分页数据结构是{ total: 100, records: [...] },接口封装层没把data取出就直接塞给了前端,结果就是表格组件拿到一个对象,渲染不出任何行。
排查方法很简单:前端浏览器控制台看请求返回的JSON结构,再对照页面代码里的数据赋值。最后在request.js的响应拦截器里统一返回res.data,页面里只用关心业务数据,就再也没有这种问题。
6.3 签到时提示“请勿重复打卡”,但确实没打过
这个问题要关注数据库的唯一约束和状态一致性。有一种情况是学生同一门课既在考勤表有记录,又在请假表有待审批的请假单,教师审批通过时回写了考勤状态为5(请假),但是前端页面因为缓存没有刷新,还停留在“可签到”状态。
解决办法是在教务端考勤管理页面提供“手动纠偏”功能,教师可以直接修改某条考勤记录的状态。虽然这是管理员的特权操作,但实际业务里总是会出现学生请假后又来上课、签到时间卡在迟到和缺勤边界这类特殊情况,没有人工干预入口的系统是不完整的。
6.4 SQL注入与安全基线
最后提一个很多毕设忽略的安全点。在MyBatis的XML里写SQL时,#{}是预编译占位符,${}是字符串拼接。所有传参都应该用#{},不能用${}拼接排序字段和表名。Order By的动态排序如果非要传列名,只允许通过白名单方式校验:
List<String> allowSort = Arrays.asList("create_time", "attendance_date"); String sortField = allowSort.contains(request.getSortField()) ? request.getSortField() : "create_time";这几条做到了,哪怕答辩老师问起安全措施,你也有话可说。
个人经验分享
这类系统我做了好几个版本,最大的感受是考勤系统的难点从来不在单个功能,而在状态之间的流转和数据的完备性。迟到、早退、请假、缺勤这些状态不是孤立的,它们之间的转换关系、异常场景的处理,才是真正有价值的业务逻辑。如果你正在做这个题目,建议先把各种边界情况用笔写下来,再动手写代码。数据库表设计花三天理顺,后面写代码的速度会快一倍以上。另外,前端页面配色和布局稍微用点心,答辩时老师对第一印象的评分比重很高。这套项目做完之后,你还能往里面加的东西很多,比如课程签到二维码、学生自主补卡流程、教师按周导出教学日志、用定时任务自动生成缺勤名单,每一个扩展点都能单独写一节内容。先把这个完整闭环跑通,后面就都是锦上添花的事了。