
1. 为什么选课堂考勤签到系统作为练手项目以及这套系统解决了什么课堂考勤这事经历过的人都懂。纸质点名一堂课四五十号人喊完名字五分钟没了代签、漏签、补签全是人情账用微信群接龙刷屏刷到老师翻不到底后来有人用在线表格又免不了有人手滑把别人的名字也勾了。疫情之后很多学校用企业微信、钉钉或者校园APP签到但这类通用工具绑定组织架构外聘老师、旁听生、跨院选修课压根没法进系统老师也拿不到精细的统计报表。我开始动手做这套基于JavaSpringBoot的课堂考勤签到系统最初就是冲着毕业设计去的代码量适中、需求明确、业务闭环清晰特别适合作为SpringBoot从入门到实战的完整练手项目。但做着做着发现它其实完全可以当做一个真实的课堂管理工具来用学生扫码或输入签到码完成打卡老师实时看到出勤情况课后自动生成考勤汇总和平时分。整套东西包含源码、配套的说明文档也就是LW毕业论文或设计说明书、调试文档和讲解视频基本覆盖了从需求分析到部署上线的完整生命周期。如果你正在找毕业设计选题或者刚学完SSM/SpringBoot想用一个真正跑得起来的完整项目来验证自己所学这套系统是非常合适的切入点。它既没有电商那种复杂的支付和库存链路也没有社交软件那种高并发的消息推送压力但把Web开发里最常见的CRUD、权限控制、数据统计、API接口设计全都串了一遍做完了你对SpringBoot的掌握会上一个明显的台阶。文章后面对应的是我这几个月从选题、设计、编码、调试到写文档的完整复盘重点不是把每一行代码贴出来而是把为什么要这样设计当时卡住的地方到底卡在哪讲清楚。你可能不打算照搬我的代码但如果能顺着这个思路把同类系统搭出来这份经验就值了。2. 需求拆解考勤签到系统到底该有哪些功能边界画在哪很多同学拿到这类题目第一反应是先把页面画出来或者先建表。我建议反过来——先写用例图把所有人和事理清楚再考虑怎么用代码实现。一个真实课堂场景里参与者就两类老师和学生管理员这个角色在课堂场景里其实可以弱化甚至可以和老师合并。但作为课设项目多一个管理员角色会让权限设计的层次更好讲也方便演示所以最终保留了三个角色。2.1 角色权限怎么划分为什么不能全堆在一个Controller里系统分为三种身份管理员、教师和学生。管理员负责维护教师账号、学生账号、院系和班级基础数据可以查看全局的所有考勤统计但不参与具体课堂业务。教师创建课程、发布签到、查看某门课的历史考勤记录、导出缺勤名单、设置签到有效时长。学生查看自己加入的课程列表、参与签到、查看自己的出勤率和个人考勤详情。这里有一个很关键的实践原则权限控制的粒度要画到接口层不能只在前端藏按钮。前端隐藏菜单只是体验问题后端接口不做校验就是安全漏洞。在后端我用Spring MVC的拦截器配合自定义注解实现角色鉴权——具体来说写了一个RequireRole注解标记在Controller方法上拦截器读取当前登录用户的角色字段校验不过直接返回401或403的JSON响应而不是跳转到一个无权限页面就算完事。很多学生的课设项目只做前端路由守卫后端接口裸奔这是个必须纠正的习惯。2.2 签到业务的两种方式二维码签到和签到码签到背后的取舍签到功能是整个系统的核心也是设计上最容易纠结的地方。我做的时候给了学生和老师两种选择老师发布签到后系统自动生成一个二维码和一组随机的四位签到码学生既可以扫码也可以手动输入签到码。二维码方案的优点是效率高学生打开手机扫一下大屏幕上的码一秒完成签到缺点是如果课堂网络不稳定或者一批学生挤在同一时间发起请求对服务器的瞬间压力会比较大——当然学校机房的网络不至于太离谱但做设计时还是要考虑到。签到码方案是二维码的兜底投影仪解析率低、教室光线太亮、学生手机摄像头坏掉这些意外都能靠四个数字解决。但随之而来的问题是作弊成本低——前排同学随口把签到码往后传全班全勤。所以签到码设计成随机且一签一换每个学生签到成功后该签到码立即失效其他人再输入这个码会提示签到码已失效。同时限制同一账号只能签到一次同一设备指纹这是通过前端把设备信息MD5后随请求带上的不建议做死限制因为苹果和安卓的WebView对设备标识的处理差异很大容易误伤正常用户只做同账号判断就够了。还有一个细节是迟到判定。我会在发布签到的时候设置一个课堂开始时间在开始时间之前签到记为正常开始时间之后到签到关闭之前签到的记为迟到。这个时间差的判定放在后端用服务器时间统一计算前端页面上的倒计时只是展示不能作为判定的依据——你永远不知道哪个学生的手机时间快了五分钟。3. 技术选型与技术架构SpringBoot为主干辅助组件怎么搭配更省心如果你去GitHub搜考勤系统能翻出上百个仓库技术栈从JSPServlet到SpringCloud微服务五花八门。但最终我选定了SpringBoot作为主框架其他组件围绕它来搭理由很务实SpringBoot的自动配置和约定优于配置的特性让一个项目的落地成本压到了最低对课设开发周期来说没有更优选。3.1 核心依赖清单与版本选型的思路整套后端我用了下面这些核心依赖Spring Boot 2.7.x稳定、社区资料多、和当前大部分课设模板兼容。不要追新用3.x除非你愿意折腾Spring Native和Jakarta命名空间迁移。Spring Data JPAHibernate实体关系映射清晰省去大量XML Mapper的配置。对课设规模的系统JPA的N1问题靠fetch join就能解决性能完全够用。Spring Security用于登录认证和密码加密存储。课设项目不建议自己写MD5加盐那套Spring Security内置的BCryptPasswordEncoder既安全又没多少代码量。MySQL 8.0数据存储主力。代码里和接口文档里统一用utf8mb4字符集避免中文乱码。Lombok消除大量getter/setter模板代码。注意IDE必须装Lombok插件否则编译直接报错这个坑在后面的调试章节专门说。前端Vue3 Element Plus Axios。如果不想引npm工程那么重也可以用直接把Bootstrap样式嵌进Thymeleaf模板的方式。各有利弊我选择前后端分离因为接口文档写起来更规整答辩时好展示实际开发中调试也能前后端独立进行。3.2 三层架构与数据流向从HTTP请求到数据库的完整链路整个后端采用经典的三层架构——Controller、Service、Repository。我见过很多课设代码把业务逻辑全部堆在Controller里一个方法两百行能跑但没法维护。职责拆清楚之后代码读起来是顺的改起来也不慌。请求链路大致是前端Axios发送HTTP请求经过JWT认证过滤器进入Controller层做参数接收和简单校验再下沉到Service层处理业务规则比如判重、算分、生成签到码最后由RepositoryJPA仓库接口操作数据库返回结果。所有跨层传递的数据统一封装在一个Result对象里code状态码、message提示信息、data业务数据体。这样一个统一返回结构的好处是前端可以只写一个响应拦截器统一处理错误提示和未登录跳转不用每个接口单独写异常处理逻辑。至于为什么用JWT而不是传统的Session核心原因是前后端分离后后端不维护会话状态扩展和部署都更灵活。签发的JWT默认有效期设置成两小时学生整节课的时间内不用重复登录另外为方便答辩演示还有一个记住我选项把有效期延长到七天。安全方面用HS256算法做签名密钥通过配置文件注入而不是硬编码在源码里——这个小习惯也很好回答答辩时老师关于安全性的提问。3.3 为什么不用微服务架构单体项目的边界如何把控可能有人觉得毕业设计或课程设计不上点微服务就显示不出水平。我的看法是脱离场景谈架构是耍流氓。课堂考勤系统的并发量级到顶就是一门课几百人同时签到单体应用完全可以撑住没必要上Eureka、OpenFeign那一套。提到微服务反而让项目复杂度在运维层面暴增答辩时被问到一个问题就容易暴露短板你这个服务挂了怎么降级——你不一定答得上来。单体项目的好处是部署简单一个Jar包配一个说明文档就能跑起来逻辑内聚一个模块改起来不需要跨服务联调。这也是为什么很多学校对课设项目的架构要求宁可保守也不求花哨。把单体做干净、做好注释和文档远比硬凹微服务更稳。4. 数据库设计考勤表的结构直接决定统计逻辑好不好写数据库是这类系统最不该省时间的地方。如果表结构设计得不好后续写统计SQL或者做JPA映射时要么疯狂查几次数据库拼数据要么写一堆奇奇怪怪的转换代码。我建表之前画了三遍E-R图最终定型为六张核心表。4.1 核心表结构与字段设计的逻辑说明下面这是最终的建表结构我精简掉了公共字段重点看业务相关的列用户表(user) id, username, password(bcrypt加密), real_name, role(admin/teacher/student), enabled(0禁用/1启用), created_at 班级表(clazz) id, class_name, grade_year, teacher_id(班主任/导员) 学生信息表(student_info) id, user_id, student_no, clazz_id, phone, gender 课程表(course) id, course_name, course_code, teacher_id, course_time, classroom, credit 课程选课表(course_student) id, course_id, student_id, join_time 签到活动表(attendance) id, course_id, teacher_id, start_time, end_time, sign_code, status(未开始/进行中/已结束), create_time 签到记录表(attendance_record) id, attendance_id, student_id, sign_time, status(正常/迟到/缺勤/请假), location_desc, remark这里有几个设计上的细节我展开讲讲为什么要单独拆一张course_student选课表最开始我也想过在course表里直接加一个student_ids的逗号拼接字符串后来做统计发现查某学生所有课程要写split做查某门课所有学生要反过来split提交选课去重、改选都麻烦得要死。拆成关联表之后一个多对多关系一目了然一条SQL就能解决JPA只需要定义ManyToMany注解即可这是教科书也会推荐的写法。签到活动的开始时间和结束时间为什么分开因为要支持老师提前发布签到这个场景。老师可以在上课前五分钟就生成签到码系统记录计划开始时间到点了状态自动变成进行中超过下课时间自动关闭。状态字段我提供三种NOT_STARTED未开始、OPEN进行中、CLOSED已结束这个状态是逻辑判断出来的不用单独定时任务去改只要在查询接口里比对当前时间和start_time、end_time即可。减少后台任务就减少了部署调试时的负担。为什么attendance_record里要有status而不是只存sign_time迟到判定要从签到时间算出来但数据库中直接存判定结果可以少做一堆时间比较。签到记录落库时Service层已经算好了状态sign_time与start_time比较若早于或等于则写正常晚于则写迟到未签到学生则不在表里计算缺勤时用学生总数减去签到人数再加上请假人数。这样写统计代码只要一个group by极省心。4.2 请假与补签看起来很小但绕不开的功能做需求分析时老师和我说他其实不指望系统能完全取代手工考勤——最大的痛点是事后对不上账学生生病请假了课代表收假条结果假条丢了学生说补签老师根本记不住当时是不是真来了。所以我在设计里加了两张辅助功能方向一个是为请假流程服务的申请记录另一个是签到补录入口。请假学生在系统里提交请假申请附带起止时间和原因老师通过后对应时段内的缺勤记录自动标记为请假。补签老师手动选择某个学生把一次未签到记录改为正常或者迟到修改操作会留下操作日志谁改的、什么时候改的、原来的值是什么这是答辩时一个不小的加分点。这两个功能不占多少代码量但对使用场景的贴合度提升很大试想一下如果根本没有请假通道那系统实际应用中就会流出很多线下操作考勤数据的真实性就大打折扣了。4.3 用JPA连表查询遇上的坑N1问题与懒加载的取舍Spring Data JPA虽然方便但如果不了解懒加载机制很容易在查询时踩坑。最典型的情况是查出签到记录列表然后遍历每条记录去取对应的学生姓名每取一次姓名实际都在执行一条新SQL——这就是N1查询。如果一门课两百个学生签到记录两百条累计查询次数可能会到几百次接口响应慢到肉眼可见的卡。解决办法三种我实际用的是第三种在Repository方法上加Query用JOIN FETCH语句提前把关联对象取出来。用EntityGraph注解在实体类上通过attributePaths显式指定要关联加载的字段。干脆在返回给前端的DTO里只放需要的字段如studentName、courseName用投影查询Spring Data JPA的Projections直接从SQL层组装好避免懒加载带来的序列化问题。另外实体类上如果用ManyToOne的默认策略是EAGER立即加载那么在返回JSON时小心无限递归的问题——典型的坑是student对象里contains courseListcourseList里又contains studentListJackson序列化时栈溢出。解决办法是在关联字段上标注JsonIgnoreProperties或者用JsonIgnore只向前端暴露必要字段。这个坑我调试了整整一晚上后面在排错章节里会再提到。5. 前端页面的设计与交互如何让签到这件事在三秒内完成很多人做课设有个误区认为前端页面随便写写就行、反正重点是后端。但课堂考勤这个场景极其依赖易用性老师站在讲台上如果还要三步以上才能发起签到他就宁可直接喊到了。学生对签到页的第一印象直接决定他对整套系统的评价。5.1 学生签到页极简设计的核心原则学生端的签到页是重中之重。我的设计原则是打开页面只需要回答现在签到还是查看考勤不出现任何多余元素。页面布局上只展示三个信息当前课程名称、上课时间、签到按钮。学生点击签到后如果有签到码输入框则自动弹出如果是二维码模式则调起摄像头扫码。签到成功或失败都用全屏的toast提示文案覆盖原界面不要求学生去读弹窗里的细节文字。前端技术选型上我用Vue3 Element Plus Axios。考虑到大多数学生的手机性能一般页面完全不做动画特效和复杂动效。样式方面统一用清爽的蓝色调错课报警学生扫了不属于自己的课二维码用红色警示框注意力引导很有效。移动端适配方面用viewport meta rem布局避免字体大小在iPhone和Android上显示差异过大。这些在开发时都属于加分项很容易在答辩时被提到。5.2 教师端的实时统计面板写接口前先想清楚要展示什么教师端除了发布签到之外最核心的就是实时统计面板。老师发布签到后页面需要实时刷新出已签到人数/应到人数/班级出勤率以及动态列表刚签到的同学信息滚动显示。这个实时刷新我用的是最简单的方案——前端定时轮询每5秒请求一次统计接口。有的同学喜欢用WebSocket做推送但WebSocket要考虑断线重连、心跳机制、服务端session管理在课设答辩中如果没有做得很完善反而容易被追问细节后卡壳。轮询虽然看起来笨但在这个人数量级下没有任何性能问题代码简单可靠调试起来也容易。如果后续要扩展成大并发场景可以平滑升级成WebSocket方案业务接口不用改动。统计接口返回的数据结构我这样设计{ courseName: 软件工程导论, totalStudents: 86, signedCount: 73, lateCount: 5, leaveCount: 2, absentCount: 6, recentRecords: [ {name: 张三, signTime: 09:02:11, status: 正常}, {name: 李四, signTime: 09:05:34, status: 迟到} ] }一次性把页面需要的所有数据打包给前端而不是让前端调五六个接口自行聚合能减少大量联调时间。这也是我在后端写Controller时始终遵守的一个原则尽量以页面为单位设计接口而不是以表为单位。5.3 二维码功能在小程序形态上的取舍为了扫码签到流畅我最初的设想是做一个微信小程序版本后来在评估开发量时放弃了。原因是微信小程序需要额外认证、审核、还要处理微信登录OAuth对课设周期来说时间成本翻了至少一倍。最后采用Web页面生成二维码的方案——老师端页面用JavaScript库qrcode生成二维码学生用微信扫一扫或者手机浏览器扫一扫就能打开签到页面既规避了小程序审核的问题又保留了扫码的快捷性。二维码内容不是一个单纯的URL而是携带了两个参数courseId和attendanceId。学生扫出来打开的就是该课程当前的签到页面。这里还暗含一个安全设计二维码中嵌入的attendanceId是无规律的雪花ID学生无法自行构造来刷签到因为还得配合签到码或登录态校验。二维码设置过期时间——签到活动结束后二维码自动失效展示端会给出提示本次签到已结束这也避免学生课后截图满天飞、第二天还能扫的尴尬。6. 后端核心代码结构与权限认证的落地实现后端代码结构的组织方式直接影响你对整个项目的掌控力。我见过不少同学习惯把所有类都丢在同一个包下面靠IDE的搜索功能找文件。这种写法在系统还小的时候看不出问题等一旦有五十个以上的class找文件就会变成灾难。我采用经典的按模块分包package按业务域划分org.example.attendance ├── config // 配置类跨域、Spring Security过滤链、WebMvc配置 ├── controller // 控制器 ├── service // 业务逻辑接口及实现 ├── repository // JPA仓库接口 ├── entity // 实体类 ├── dto // 数据传输对象请求参数封装、响应结果封装 ├── common // 通用返回结构、自定义异常类、常量定义 ├── util // 时间处理类、二维码生成工具类、JWT工具类 └── interceptor // 登录校验拦截器、权限校验拦截器6.1 统一返回结构与全局异常处理的设计所有接口都统一返回Result结构体这个在开发前就要定好规范不能等到写了一半再改否则所有接口将返工。{ code: 200, message: 操作成功, data: {} }错误码含义在常量类里做好定义400参数错误、401未登录或登录已过期、403无权限、404资源不存在、500系统内部异常。这些状态码和前端的响应拦截器一一对应只要后端返回非2xx的状态码或者code不是200前端都会弹出对应的错误提示Toast。全局异常处理使用RestControllerAdvice注解声明一个全局异常拦截器内部用ExceptionHandler分别处理参数校验异常、业务异常、数据库异常和兜底的Exception异常。这里关键点在于业务异常要自己定义异常类例如签到已过期、重复签到这些场景抛出BizException并在全局处理前端拿到的data为nullmessage为你已签到请勿重复操作体验就清晰了。如果不做全局异常框架默认会把堆栈信息返给前端这个问题在线上环境属于安全漏洞在答辩时也会被扣印象分。6.2 Spring Security JWT认证链路登录之后每个请求如何被识别JWT登录流程是这样的用户提交用户名密码Controller层接收后交给Service层。Service层调用BCryptPasswordEncoder的matches方法校验密码匹配成功则生成一个JWT令牌token令牌中放入userId、用户名、角色三个核心字段。前端把token存储到localStorage并在Axios请求拦截器中给每个请求头加Authorization: Bearer token。后端写一个OncePerRequestFilter过滤器每次请求到来时先检查header里的token解析成功后把用户Id和角色信息放入Spring Security的上下文UserDetails存入SecurityContextHolder。后续的业务代码通过SecurityUtils.getCurrentUserId()就能拿到当前登录人的Id无需每个Controller方法都传UserId参数。这段逻辑的完整代码大致结构如下public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { String jwtToken token.substring(7); try { Long userId JwtUtil.parseToken(jwtToken); User user userRepository.findById(userId).orElse(null); if (user ! null) { UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); } } catch (Exception e) { // token非法则不设置认证上下文后续拦截器会判断为未登录 } } chain.doFilter(request, response); } }Spring Security的配置类记得放行登录接口、二维码签到接口扫码用户如果尚未登录需要先登录再跳转和静态资源。其他接口一律拦截。这里特别提醒不要为了省事把Spring Security整个禁掉答辩时老师一定会问你的系统怎么做权限控制的没有安全框架的Web系统不仅不安全也没有技术亮点。6.3 签到核心接口的实现逻辑并发去重如何考虑签到接口是系统的核心瓶颈点和易错点。一个学生点击签到后后端究竟经历了什么完整流程是校验token取出userId。根据attendanceId查出签到活动判断当前时间是否在有效期内。用userId attendanceId去查attendance_record表判断是否已存在签到记录。如果存在直接返回你已签到。如果不存在生成签到记录落库同时更新签到活动表的已签到人数。如果是签到码模式在签到后将sign_code置为空或标记为失效下次其他人再用同一个码会提示签到码已失效。这里并发下会有个典型的竞态问题同一学生开了两个窗口同时点签到两个请求同时走到第3步都没有查到签到记录然后各自插了一条记录等于一键签了两次。常规的解决方式是给attendance_record表加唯一约束让数据库来保证同一学生同一签到活动只能有一条记录。我在建表时就在两者上加了联合唯一索引同时在逻辑层再查一次双保险后续代码上没有出过重复签到问题。另一个要注意的点是签到人数计数器。如果每次签到都去update attendance表中的signed_count字段非常容易产生行锁竞争而且并发高的时候还会出现更新丢失。更稳妥的方案是签到时不维护这个计数查询时用count()实时统计。这个做法牺牲了一点点性能换来了逻辑的正确性和代码的简洁性数据量在几百人级别时count()查询毫秒级返回完全可以接受。7. 课设开发中的典型踩坑清单从环境、调试到兼容性问题这部分是我最想写的也是最值钱的。一个项目调试过程中踩过的坑如果没人提醒你可能会在这个同款坑上浪费好几个晚上。7.1 Lombok插件没装导致的诡异编译错误新手用SpringBoot集成Lombok最容易遇到的报错信息是找不到符号getXxx()方法或者找不到符号builder()之类。代码看着没问题getter/setter也写了注解但编译就是不通过。原因很简单你的IDE没有安装Lombok插件注解处理器没生效。我在帮A同学排查代码时才发现这个问题至少有四种表现形态Eclipse环境编译报错找不到符号。IntelliJ IDEA需要安装Lombok插件并在Settings中勾选Enable annotation processing。Maven命令行构建需要pom里显式声明Lombok依赖并且JDK版本匹配JDK8配Lombok老版本没问题JDK17以上建议用新版Lombok。多模块项目某个子模块忘了加Lombok依赖也会报同样错误。最简单的排查工具就是pom里有没有lombok依赖、IDE里有没有装插件、编译器注解处理有没有开启三板斧检查完基本八九不离十。7.2 JPA懒加载导致的Jackson序列化递归与懒加载异常我在第4节提到的序列化递归问题实际现象是这样的查询签到记录时分页返回的是记录实体类实体类关联了学生实体学生实体又关联了课程集合课程集合又关联了学生集合……无限循环Jackson会栈溢出返回500后台堆栈打出一堆at com.fasterxml.jackson...报错的日志。我在项目里用了两个办法解决这类问题一是DTO方案。给Controller层返回专门构造的响应对象比如SignRecordVO只包含studentName、studentNo、signTime、status完全脱离实体关联结构。这样不仅彻底规避序列化问题还让前端少了一层字段适配。二是懒加载异常。如果坚持直接返回实体很可能会在序列化阶段碰到org.hibernate.LazyInitializationException这是因为Hibernate的Session已经关闭而懒加载代理仍没有被触发。解决办法是用Transactional将查询和序列化放在同一事务里或者在实体上用JsonIgnoreProperties忽略不加载的关联字段。但说实话用好DTO方案之后这些Hibernate特有的坑基本都不会碰上了。我在给某同学的代码做review时发现他们用的直接实体返回连续遇到两个这类Bug改成DTO之后一夜清爽。7.3 时间字段的时区问题为什么你传的时间总是慢八小时国内常见的问题是前端传给后端的时间字符串在后端用LocalDateTime接收后发现转换成时间戳之后比北京时间慢了8小时。原因是连接MySQL的URL里没有设置serverTimezone参数或者设置为UTC而不是Asia/Shanghai。我的解决办法是三步一次解决MySQL连接URL显式加上serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8。后端统一使用LocalDateTime不用java.util.Date。前端传时间时统一使用标准ISO8601格式例如2025-03-20T09:00:00。另外签到时间比较不要自己去算毫秒差然后除以86400000去算日期可以用LocalDate.from(localDateTime)取日期部分用Duration.between()计算差值代码不仅简洁也不会出现闰年之类的边界问题。JDK8开始的时间API是线程安全的而SimpleDateFormat不能做成全局静态变量用不对会出现并发错乱的日期数据。7.4 跨域问题与端口占用联调阶段的两个高频小病前后端分离开发后端跑在8080前端跑在5173两边没配跨域浏览器就报CORS错误甚至localhost和127.0.0.1之间都有跨域问题。处理跨域我推荐在Spring配置类里统一配置而不是在过滤器里自己写响应头Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }需要注意的是allowCredentials(true)时allowedOrigins不能简单设置为*必须用allowedOriginPatterns或者指定具体域名否则浏览器还是会拦截。端口占用这个问题在Windows下面特别常见。Idea里启动后突然8080被其他进程占用了可以打开命令行用netstat -ano | findstr 8080 查端口对应的进程PID再用taskkill /PID xxx /F强制结束。如果经常出现多个Java进程同时启动可以尝试在启动类中设置一个随机端口server.port0但这样前端把后端的端口写死了就会连不上更推荐在IDEA的启动配置里指定端口并开启Allow parallel run控制。8. 从开发环境到部署演示调试文档、讲解视频和验收演示要点配套的调试文档和讲解视频看起来很冗长但对课设验收和答辩来说反而价值很大。调试文档最适合以问题—原因—解决步骤的格式来写。平时开发中每解决一个异常我都顺手记录下来最后整理成一份规范排错文档包含环境变量配置、依赖安装注意事项、修改配置文件的路径、常见报错对照表。这份文档后来被我周围好几个同学要走过他们说比很多培训机构上网课内容对自己更直击要害。8.1 本地调试的完整启动步骤与环境要求新手最容易在启动环节翻车我总结了一个标准启动检查清单JDK版本确认项目在pom里声明了java.version如果你本机装了多个JDK版本IDEA的Project Structure和Maven的JDK设置必须完全一致否则编译报错时会出现一些与代码无关的莫名其妙问题。MySQL初始化严格按README里的顺序执行建库建表SQL脚本注意MySQL账号密码要和yml配置文件一致。Redis是否需要我的版本里没有依赖Redis缓存用的ConcurrentHashMap和本地缓存减少一个中间件能减少极大部署难度。如果你自己的项目加了Redis记得本地也必须先启动Redis服务再启动SpringBoot否则启动直接fail。前端依赖安装npm install时间较长过程中可能因为网络问题失败建议配置npm淘宝镜像源。Vite启动默认是5173端口要和后端CORS配置一致。我见过最典型的启动失败场景是同学照着视频操作SpringBoot启动到一半报数据库连接失败他截图发我我一看配置文件里的数据库密码还是默认的123456而他本地MySQL的密码改成了自己的。五分钟就解决的问题卡了两小时。这种问题就是你越急越看不到自己的失误所以调试文档里我特意把数据库连接失败排在了常见问题第一位。8.2 答辩演示时的两条路线功能演示与代码走读答辩之前一定要自己完整跑一遍功能流程这个是常识。但演示的顺序和节奏是有门道可讲的不要按菜单顺序从头点到尾而是要讲一个完整的故事。我的建议是分两条线第一条线是学生视角注册账号、登录、选课、扫码签到/输入签到码、查看个人出勤情况。这条链路能让评委直观看到系统的完整用户体验。第二条线是教师视角创建课程、发布签到设置时间参数、查看实时统计、处理请假、导出缺勤名单。这条链路展示了系统如何影响和帮助真实教学管理。两条线走完后再切换到管理员视角简单浏览基础数据维护功能说明系统不仅好用还好维护。这样演示逻辑清晰比对着菜单机械点击要自然得多。代码走读环节优先展示你在项目里做得最有深度的部分——如果签到去重用了唯一索引就打开实体类讲联合唯一约束如果权限校验用了自定义注解就打开注解类和拦截器讲原理如果用了全局异常就打开advice类讲统一错误处理。不需要讲无关紧要的代码细节评委在一个方向上最多追问两三层只要这部分的完整链路你心里有数基本就稳了。8.3 导出考勤报表的极简实现方案需求里通常会有导出缺勤学生名单很多同学一听到Excel导出就开始引入Apache POI依赖其实在这个场景下有一个更轻量的替代方案直接用后端动态生成一个CSV文件前端用window.location跳转下载链接即可。CSV格式可以直接用Excel打开中文乱码问题通过输出BOM头\uFEFF解决。StringBuilder sb new StringBuilder(); sb.append(\uFEFF); // BOM头防止Excel打开中文乱码 sb.append(学号,姓名,缺勤次数,联系方式\n); // 遍历数据拼接 response.setContentType(text/csv;charsetutf-8); response.setHeader(Content-Disposition, attachment;filenameattendance.csv);这个方案代码量不到40行不需要额外引入任何依赖答辩时讲起来还能显得你对技术选型的冗余问题有思考能用简单方案解决的就不要引入复杂框架。如果你把CSV方案和POI方案的取舍讲清楚面试官反而会认为你有真实工程经验。9. 如何基于现有系统做二次扩展从一个课设变成一套可用的教学工具做完整套系统之后回头再看课堂考勤这件事其实往下深挖还有不少可以做的方向。如果你学有余力以下三个方向可以往系统上挂第一个方向是自动签到分析。在现有签到数据的基础上生成一个多维度的统计报表每门课的出勤趋势折线图、某个班级的缺勤热力图、某位学生的出勤曲线。这些用ECharts在前端直接画就行后端不需要太多改造只要按课程和班级维度聚合查询方法即可。第二个方向是消息通知。签到开始后给选课学生发送一条站内信或者推送通知。这个可以扩展Spring Boot的WebSocket模块也可以利用企业微信或钉钉机器人的Webhook推送。如果想稳妥上手站内信是最容易实现的。把已读/未读状态存库前端在导航栏显示未读角标。第三个方向是集成到已有教务系统。大多数学校已经有教务系统课程数据、学生选课数据都存在那边。如果能在做毕设或课设时设计好导入接口比如支持Excel模板批量导入就能让这个系统更容易落地到真实教学环节。把一个毕业设计打磨到可以实际演示给教务老师看对有留校任教或读研想法的同学来说会是一个非常亮眼的成果。我自己在完成系统后最明显的体会是能跑通的代码只是第一步真正拉开差距的是你在设计阶段对场景的理解在开发阶段对边界条件的考虑在调试阶段对异常抽丝剥茧的能力。把这一套走下来你得到的不仅是一个可以答辩的课设而是一份完整的工程思维训练。如果有朋友对这个系统的某些模块的详细实现感兴趣欢迎一起交流讨论比如签到的并发去重、JWT续签方案或者二维码有效期控制单独拎出来一个都能写一篇完整的干货。