
SpringBootVue大学生考勤系统、JavaMySQL全栈开发这几个关键词放在一起基本就是我接得最多的毕设需求模板。这类系统本质上是把传统的纸质点名、Excel统计搬到线上属于典型的管理信息系统技术难度适中、业务场景明确、功能边界清晰非常适合用来做毕业设计或者课程设计。源码也好、从零写也好思路对了整个系统的架构、表结构、权限模型和核心接口就能一次定下来。这篇文章就把我做这类系统的完整思路、数据库设计、核心接口实现、前端页面拆解以及实际踩过的坑一次讲清楚给准备做毕设、课设或者想练手全栈的同学一份能直接照着走的实操指南。1. 整体设计与技术选型的底层逻辑1.1 为什么这套技术栈是“开箱即用”的组合先聊技术选型。SpringBootVueMySQL这套组合放在全栈开发里早就是“校招简历标配”级别的存在但对于做毕设和课设的同学来说它的价值不只是“标配”这么简单而是每一层都有现成生态可以借力。后端用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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;-- 课程表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;-- 考勤记录表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;请假表同样重要这个表不仅是业务记录还要承担“考勤状态回写”的角色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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 唯一约束与状态设计两个关键决策考勤表里我特意加了一条UNIQUE KEY uk_stu_course_date (student_id, course_id, attendance_date)这是一个非常重要的细节。这条唯一约束保证了同一个学生同一天同一门课只会有一条考勤记录在代码层面即使并发请求、重复打卡数据库也会兜底拒绝重复插入。状态字段用tinyint而不是字符串这是我在实际项目里比较坚持的做法。很多新手喜欢存“正常”、“迟到”这样的中文看起来直观但后端比较状态、前端筛选统计的时候字符串比较容易出错而且中文一旦数据量大了还有编码和存储空间的问题。用数字存然后定义常量或者枚举去映射是最稳妥的做法。2.3 为什么要加一个“学期”字段课程表里我放了一个sementer字段别小看它。考勤系统如果只做单学期确实用不上学期隔离。但现实中一个学校会有多个学期的数据累积如果统计的时候不做学期区分教师看到的数据就会混在一起。加了这个字段之后查询记录、统计出勤率的SQL都要带上学期的过滤条件那才是符合实际业务形态的做法。从毕设答辩的角度来说这个字段也方便演示“往届数据归档”这类扩展点。3. 后端SpringBoot实操要点3.1 登录认证JWT的前后端完整闭环考勤系统涉及三类角色权限控制躲不开登录认证。我用的方案是JWTJSON 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: GMT8同时在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端口浏览器的同源策略会拦截跨端口请求。解决方案有很多种我用的最省事的是后端加一个CorsFilterConfiguration 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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/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的动态排序如果非要传列名只允许通过白名单方式校验ListString allowSort Arrays.asList(create_time, attendance_date); String sortField allowSort.contains(request.getSortField()) ? request.getSortField() : create_time;这几条做到了哪怕答辩老师问起安全措施你也有话可说。个人经验分享这类系统我做了好几个版本最大的感受是考勤系统的难点从来不在单个功能而在状态之间的流转和数据的完备性。迟到、早退、请假、缺勤这些状态不是孤立的它们之间的转换关系、异常场景的处理才是真正有价值的业务逻辑。如果你正在做这个题目建议先把各种边界情况用笔写下来再动手写代码。数据库表设计花三天理顺后面写代码的速度会快一倍以上。另外前端页面配色和布局稍微用点心答辩时老师对第一印象的评分比重很高。这套项目做完之后你还能往里面加的东西很多比如课程签到二维码、学生自主补卡流程、教师按周导出教学日志、用定时任务自动生成缺勤名单每一个扩展点都能单独写一节内容。先把这个完整闭环跑通后面就都是锦上添花的事了。