资讯详情

资讯详情

SpringBoot+Vue+MyBatis+MySQL大学生考勤系统全栈开发实战

做大学生考勤系统很多人第一反应是“不就是一个打卡加统计嘛”但真正落地的时候你会发现“打卡”两个字背后藏着定位、时间窗口、请假联动、防作弊、定时补缺、多角色权限一整套逻辑。这个项目用SpringBootVueMyBatisMySQL四件套实现算是国内高校里最典型、也最值得吃透的一套技术组合。无论你是拿它当毕业设计、实训项目还是想把这套全栈流程跑通后写进简历这篇东西都值得认真看完。我会把数据库设计、后端接口、前端交互、部署避坑整个链路都拆开讲把我踩过的坑和改过的版直接给你。1. 项目概述与核心需求解析1.1 大学生考勤系统到底要解决什么问题高校考勤和公司考勤有本质区别。公司考勤的最终目的是算工资迟到扣多少钱、旷工扣多少天逻辑非常线性。学校考勤不一样它的产出是“平时成绩”和“预警记录”迟到多少次扣多少平时分、缺勤累计几次触发辅导员约谈、请假是否影响评优。这意味着系统不能只记录“几点到”还要维护一套可配置的规则。跟真实的课堂场景对比一下就知道痛点在哪。传统点名方式老师拿纸质名单一喊五六十人的大课喊完至少五分钟一个学期下来Excel统计能让人崩溃。更麻烦的是代签问题一个宿舍四个人后面的室友能帮前面全答到。到期末算平时分的时候老师要翻每节课的记录人工折算比例出错率极高。所以这个系统要解决的核心问题是四个快速签到、防代签、自动统计、请假联动。快速签到对应移动端页面学生扫码或定位就能完成防代签对应定位范围和IP/MAC指纹校验自动统计对应后端定时任务和动态SQL聚合请假联动对应审批通过后自动把时间段内课程置为“请假”状态。四件事做扎实了系统才能真的用起来而不是一个花架子CRUD。1.2 技术选型为什么是SpringBootVueMyBatisMySQL这套组合在2025年依然是国内Java全栈的“标准答案”之一。SpringBoot负责后端快速搭建内嵌Tomcat、自动配置、起步依赖这些特性让项目能在一个小时内跑起来Vue负责前端组件化开发学生端、教师端、管理员端拆成独立视图路由守卫做权限拦截MyBatis负责数据库访问相比JPA那种全自动ORMMyBatis的SQL完全由自己掌控尤其适合考勤统计里那种“多条件动态拼接”的场景MySQL负责数据存储免费、稳定、生态齐全。选这套而不是别的背后有三个现实考量。第一是学习成本可控。SpringBoot的自动配置屏蔽了大量Spring底层细节Vue有成熟的中文文档和Element UI/Element Plus组件库MyBatis上手简单这三个技术点都是面试高频学会了之后复用性很强。第二是团队协作清晰。前后端分离之后前端同学和后端同学可以并行开发接口文档定好就能推进。第三是部署成本极低。后端打包成Jar前端构建成静态资源一台2核4G的云服务器就能跑得很稳。这里要提醒一句SpringBoot版本不是越高越好。如果你手头的JDK还是8Spring Boot 3.x根本跑不起来老老实实用2.7.x系列。很多实训项目环境老旧为了“最新”两个字把时间浪费在版本兼容上得不偿失。Vue也是同理Vue3Element Plus是主流但如果你的团队只熟Vue2用Vue2Element UI同样能完成项目关键是业务逻辑要正确。2. 数据库设计与核心业务模型2.1 用户与角色模型一张基础表还是三张表大学生考勤系统里有三类用户学生、教师、管理员。学生需要学号、班级、入校年份教师需要工号、职称、所属学院管理员只需要登录账号。很多新手会建三张彼此独立的设计表然后用一张“用户角色关联表”去映射结论是做成了RBAC权限模型。我的建议是别把简单问题复杂化。对于这种单体中小型项目一张sys_user基础表存账号、密码、角色再配一张student_profile和一张teacher_profile做扩展性价比最高。sys_user表里只放用户ID、用户名、密码、角色、状态、创建时间。学生信息放到扩展表通过user_id关联。这样既避免了JOIN三张以上的大麻烦又保留了扩展空间以后要加辅导员角色照葫芦画瓢加一张profile就行。密码存储务必使用BCrypt加密不要用MD5。MD5撞库太容易了Spring Security或者SpringBoot自带的spring-security-crypto包里就有BCryptPasswordEncoder一行代码搞定加密和校验。如果项目里用的是shiro或者其他鉴权框架同样优先选择BCrypt。至于“一个学生能不能兼任教师助教”这种边界情况别纠结系统设计阶段直接定死一个账号一个角色。助教需要管理权限让老师给单独账号不要做动态多角色否则Vue前端的路由守卫和后端的权限拦截都要跟着复杂一倍。2.2 考勤记录与请假流程的数据模型考勤记录表是整个系统的核心字段设计直接决定后续统计是否痛苦。我的核心表示意如下attendance_recordid主键student_id学生IDschedule_instance_id排课实例ID而不是存一个笼统的course_idcheck_in_time签到时间check_out_time签退时间按需使用statusNORMAL、LATE、EARLY、ABSENT、LEAVEsource_type1-定位签到 2-扫码签到 3-教师代签 4-管理员补录latitude / longitude签到时的定位坐标用于防作弊溯源remark备注status字段是整个业务状态机的核心。NORMAL表示正常出勤LATE表示迟到EARLY表示早退ABSENT表示缺勤LEAVE表示请假被批准。这里有一个关键决策请假记录不要直接改考勤记录而是在统计时用JOIN判断。如果你在status里存了LEAVE那么请假被驳回之后必须把LEAVE改回原状态操作链路长且容易出错。更稳的做法是考勤记录只存实际签到情况统计出勤率时如果某学生没有考勤记录但有通过状态的请假单则该时段计为“请假不算缺勤”。请假流程单独一张leave_request表字段包括学生ID、开始时间、结束时间、请假类型事假/病假、请假理由、附件路径、审批教师ID、状态PENDING/APPROVED/REJECTED、审批备注、创建时间。这张表的核心作用是和排课实例做时间重叠判断。2.3 排课实例避免统计混乱的关键设计这是整个数据库设计里最容易被忽视、但最影响后期开发效率的一张表。很多考勤系统会直接设计“课程表”字段是课程ID、上课时间、上课星期、上课节次。然后考勤记录直接挂课程ID统计“高等数学第3周出勤率”的时候就得把“课程ID周次星期节次”四个条件拼SQL写起来又臭又长。我推荐的方案是增加schedule_instance表也就是“排课实例表”。一门课程“高等数学”在整学期里被拆成多个实例每个实例代表“第X周星期X第X节在X教室上课”。这个实例有自己的主键ID考勤记录直接关联这个实例ID。这样一来某一节课的应到人数、实到人数、缺勤名单都是天然的分组维度。CREATE TABLE schedule_instance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_id BIGINT NOT NULL, teacher_id BIGINT NOT NULL, week_number INT NOT NULL COMMENT 第几周, day_of_week INT NOT NULL COMMENT 星期几1-7, start_time TIME NOT NULL, end_time TIME NOT NULL, classroom VARCHAR(100), status TINYINT DEFAULT 1 );配合这个表课后统计就变得非常优雅按schedule_instance_id分组查考勤记录一眼看出这节课谁没来。定时任务生成缺勤记录时也是遍历当天的schedule_instance而不是遍历课程表。这个设计初期会多花半小时建表后期帮你省下大把改SQL的时间。3. 后端实现SpringBootMyBatis实战3.1 项目骨架与MyBatis基本配置后端工程我习惯用Maven构建清清爽爽分包。controller层只做参数接收和结果返回service层写业务逻辑mapper层只负责数据库操作entity对应数据库表dto用于前端交互。common包里放统一返回结果类Result、全局异常处理器、常量定义。config包放跨域配置、MyBatis配置、定时任务开关。application.yml里MyBatis的关键配置有两行一个是mapper-locations指定XML文件位置一个是map-underscore-to-camel-case开启驼峰映射。mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.attendance.entity configuration: map-underscore-to-camel-case: true这个驼峰映射必须开启。数据库字段create_time在Java实体里对应createTime如果不开启映射查询结果里createTime就是null排查起来毫无头绪。很多新手卡在这一步卡半天其实就少这一行配置。实体类里的时间字段建议直接用LocalDateTime配合MyBatis的TypeHandler比如MyBatis-Plus的LocalDateTimeTypeHandler或者自己实现一个不要用java.util.Date。Date在前后端JSON序列化和时区处理上太容易出问题了LocalDateTime配合Jackson的配置反而干净。3.2 动态SQL写出灵活的考勤统计考勤统计的特点是查询条件不确定教师端可能按课程筛选也可能按日期范围筛选还可能按班级筛选管理员端可能按学院筛选也可能按学生姓名模糊搜索。这些条件如果写死SQL那就得写十几个接口动态SQL就是干这个的。MyBatis的ifwhere标签是最基础也是最核心的组合。比如统计某个时间段内各状态的数量可以这样写select idselectAttendanceStat resultTypemap SELECT DATE_FORMAT(a.check_in_time, %Y-%m-%d) AS day, COUNT(*) AS total_count, SUM(CASE WHEN a.status LATE THEN 1 ELSE 0 END) AS late_count, SUM(CASE WHEN a.status ABSENT THEN 1 ELSE 0 END) AS absent_count FROM attendance_record a where if testcourseId ! null AND a.course_id #{courseId} /if if teststartDate ! null and startDate ! AND a.check_in_time gt; #{startDate} /if if testendDate ! null and endDate ! AND a.check_in_time lt; #{endDate} /if if teststudentId ! null AND a.student_id #{studentId} /if /where GROUP BY day ORDER BY day DESC /select这里要注意几个细节。第一XML里的小于号要用转义大于号用不然XML解析直接报错。第二参数是String类型时if判断要同时判null和isEmpty不然前端传个空字符串也会拼进SQL。第三GROUP BY之后ORDER BY用别名day是可以的但WHERE里不能用别名。这些坑看起来小实际调试的时候非常消耗时间。3.3 签到接口定位校验与时间窗口判定签到接口是后端最核心的接口逻辑分三步走。第一步前端通过浏览器的Geolocation API或者微信小程序的定位接口获取学生的经纬度连同课程实例ID一起提交到后端。后端拿到经纬度后和该课程实例绑定的教室坐标做距离计算。这里推荐Haversine公式因为地球是球面直接用平面距离在纬度跨度大的地区会失真严重。private static final double EARTH_RADIUS 6371000; private double distance(double lat1, double lng1, double lat2, double lng2) { double radLat1 Math.toRadians(lat1); double radLat2 Math.toRadians(lat2); double a radLat1 - radLat2; double b Math.toRadians(lng1) - Math.toRadians(lng2); double s 2 * Math.asin(Math.sqrt( Math.pow(Math.sin(a / 2), 2) Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2) )); return s * EARTH_RADIUS; }实际使用中教室半径建议设置为80到120米。设置太严GPS在室内的误差会导致学生明明在教室却签不上设置太松隔壁教学楼都能代签。我实践下来的经验是100米比较合理如果教学楼比较密集可以再收紧到70米。第二步时间窗口判断。先根据schedule_instance查到这节课的start_time和end_time。如果签到时间在start_time之前15分钟内允许签到状态为NORMAL如果签到时间在start_time之后但未超过迟到阈值比如10分钟状态为LATE如果签到时间超过迟到阈值且仍在课程时间内可以允许签入但状态标记为LATE绝不允许“下课了才录入一条踩点记录”。这里要特别注意下课后才来补签到应该直接拒绝并提示“本课程已结束”而不是无限放行。第三步写考勤记录。插入时需要带上lat/lng和source_type这样后续如果发生“人不在教室但系统显示签到”的纠纷可以调出记录溯源。同时要防重复签到。同一student_idschedule_instance_id在数据库里要加唯一索引插入时先查后插捕获DuplicateKeyException时返回“你已完成签到”。3.4 定时任务自动生成缺勤记录手工补录缺勤效率太低课程多的时候老师也不可能记得每个没来的学生。所以要在后端写一个定时任务每天课程全部结束后自动扫描当天的考勤情况生成缺勤记录。SpringBoot里用Scheduled注解就能实现。每45分钟扫描一次比如当天21:30开始处理白天的考勤。Scheduled(cron 0 30 21 * * ?) public void generateAbsentRecord() { ListScheduleInstance instances scheduleInstanceMapper.findByDate(LocalDate.now()); for (ScheduleInstance instance : instances) { ListLong absentStudentIds studentMapper.findAbsentByInstance(instance.getId()); for (Long studentId : absentStudentIds) { // 先查请假表看是否已有审批通过的请假记录覆盖这个时间段 int leaveCount leaveRequestMapper.countApprovedOverlap( studentId, instance.getStartTime(), instance.getEndTime()); if (leaveCount 0) { continue; } // 再查考勤记录已经签到的学生跳过 int attendanceCount attendanceRecordMapper.countByStudentAndInstance(studentId, instance.getId()); if (attendanceCount 0) { continue; } AttendanceRecord absentRecord new AttendanceRecord(); absentRecord.setStudentId(studentId); absentRecord.setScheduleInstanceId(instance.getId()); absentRecord.setStatus(ABSENT); absentRecord.setSourceType(0); absentRecordMapper.insert(absentRecord); } } }这里最关键的细节有三个。第一必须先查请假表再查考勤记录。因为请假被批准的学生理论上不会主动去签到如果先查考勤就会把请假学生误判为缺勤。第二要加幂等控制也就是先查再插否则定时任务每次执行都会重复生成缺勤记录。第三定时任务只处理“已经结束”的课程不要处理正在进行中的课程否则下半场才来上课的学生会被误判。3.5 请假流程的状态流转与冲突检测请假流程相对简单状态从PENDING到APPROVED或REJECTED前端提交请假单后端校验时间合法性教师端审核。但有一个非常容易出问题的点时间冲突检测。学生提交请假申请时系统必须校验“这段时间内有没有已经审批通过的请假重叠”。如果不做这个检测可能出现一个学生同一时间段提交了两个不同事由的请假单后端的考勤判定逻辑会被搞乱。检测逻辑实际上就是区间重叠判断假设已有请假记录时间是start1到end1新提交的请假时间是start2到end2只要满足start1 end2 且 end1 start2就算重叠。SELECT COUNT(*) FROM leave_request WHERE student_id #{studentId} AND status APPROVED AND start_time lt; #{endTime} AND end_time gt; #{startTime}如果返回值大于0直接提示“该时间段已有请假申请请先撤销原申请”。这个检测在插入之前执行就已经够用了不用加复杂的锁。请假被批准之后前端调课表接口时对应时间段内显示的课程状态要变成“已请假”。这一步不需要写回考勤记录而是在查询接口里JOIN请假表判断前面已经说过这个原则。到期末统计时请假时间段的考勤记录不参与缺勤次数计算但也不会计入出勤天数而是单独一个“请假”分类。4. 前端实现Vue与交互细节4.1 前端工程结构与路由权限设计前端工程我用Vite或者Vue CLI创建结构大致是views、components、router、store、api这几个目录。views下面按角色拆分student/pages里的打卡页、课表页、请假页teacher/pages里的课程管理、考勤统计、请假审批admin/pages里的用户管理、班级管理、课程管理。components目录放一些公共组件比如通用的时间选择器、上传组件、状态标签。路由权限是前后端分离项目里最值得注意的一环。Vue Router的beforeEach钩子里先判断是否有token没有就踢到登录页有token再判断目标路由的meta.roles是否包含当前用户的角色不包含就踢到403页面。router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); return; } if (!token) { next(/login); return; } const roles store.state.user.roles; if (to.meta.roles !to.meta.roles.some(r roles.includes(r))) { next(/403); return; } next(); });这里要注意一个细节动态路由和静态路由的区别。对于学生、教师、管理员权限差别很大的系统可以写死静态路由但通过meta控制访问权限。如果角色再复杂一点后端返回菜单列表前端addRoute动态添加。我的建议是考勤系统最多三个角色静态路由meta检测完全够用别搞动态路由维护成本反而更高。axios封装里请求拦截器自动带上token响应拦截器统一处理错误码。比如401跳登录、500弹错误提示。这一层一定要做不然每个页面都要写try catch去处理登录过期代码会爆炸。4.2 学生端打卡页面的定位实现打卡页面是整个系统里用户体验要求最高的地方。场景是上课铃响了学生点开手机页面要在几秒内完成签到。所以我做成了一个大的圆形按钮“立即签到”四个字背景色用醒目的绿色点击之后立刻展示当前定位状态和距离。定位用浏览器原生Geolocationnavigator.geolocation.getCurrentPosition( (position) { const lat position.coords.latitude; const lng position.coords.longitude; // 提交给后端校验 this.submitCheckIn(lat, lng); }, (error) { this.$message.error(定位失败请检查是否开启定位权限或改用扫码签到); }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 30000 } );这里会遇到两个现实问题。第一是校园室内GPS信号弱很多教学楼因为建筑结构问题手机在教室里无法获取精确定位。我的解决方案是前端定位成功后显示距离当距离在100米内时直接允许签到后端接口同样做校验如果前端定位失败允许学生改用扫码签到教师在课堂上展示一个动态二维码学生扫码即可签到。第二是部分浏览器要求页面必须在HTTPS环境下才能调用Geolocation。本地开发用localhost没问题一旦部署到服务器必须用HTTPS域名否则定位API被浏览器禁用。这一点很多人在部署后才遇到提前配好证书能省很多事。签到成功后页面要立刻反馈显示签到时间、签到状态正常/迟到、距离教室位置。这样学生知道自己签上了没也避免反复点击重复提交。重复提交统一由后端唯一索引兜底前端也要在收到成功响应后禁用按钮。4.3 教师端统计报表与可视化教师端是数据消费的核心场景。老师关心三件事今天谁没来、这个月每个人缺勤多少次、这门课的出勤率趋势。所以统计页面至少要有三个可视化模块。我用ECharts做图表第一个是出勤率折线图按周展示横轴是第1周到第18周纵轴是出勤率百分比。这个数据来自一个接口后端返回课程所有排课实例的应到人数、实到人数前端算好百分比后直接渲染。第二个是缺勤预警柱状图展示本学期累计缺勤超过3次的学生名单红色高亮方便老师重点关注。第三个是表格明细支持按日期、按状态筛选后端用动态SQL支持多条件查询。这里有一个非常实用的优化下载Excel。老师期末要交平时成绩表不能让他们对着网页一个个抄。后端可以集成EasyExcel或Apache POI把考勤统计结果导出为xlsx文件。导出接口要设计成异步任务因为一个班几十人、一学期十几周的数据量虽然不大但如果并发导出可能会导致接口超时。简单的做法是同步导出数据量大的时候再加异步和文件存储。前端页面还有一个容易被忽略的细节筛选条件的默认值。默认显示“本学期”“本课程”不要默认“全部时间”否则统计图表加载慢且没有重点。日期范围选择器限制最长跨度为一个学期避免学生选了三年数据导致查询超时。5. 联调、打包与部署实战5.1 前后端联调的跨域问题开发环境下前端跑在Vite的默认端口5173后端跑在8080这就是典型的跨域场景。解决方式有两种。一种是在后端配置CORS写一个配置类实现WebMvcConfigurer允许指定来源。另一种是在前端配置代理Vite的vite.config.js里设置server.proxy把/api开头的请求转发到后端地址。我个人强烈推荐第二种方式因为开发环境和生产环境可以保持同一个接口路径打包后不需要改前端代码。// vite.config.js server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }代理的好处是请求发到前端的5173端口Vite服务器再把请求转发到8080浏览器看到的请求是同源的不存在跨域问题。生产环境如果用Nginx同样把/api路径转发到后端服务即可前后端联调路径完全一致。5.2 Vue打包放进SpringBoot的两种方式很多人的目标是部署成单机版本不想搞前后端两个服务那最常用的方案就是把前端打包产物塞到SpringBoot的静态资源目录里。前端执行npm run build生成dist目录里面是index.html、static等文件。第一种方式是把dist目录下的所有文件复制到后端工程的src/main/resources/static目录下然后重新打包SpringBoot的Jar包。第二种方式是在后端pom.xml里配置maven-resources-plugin构建时自动把前端dist目录拷贝到target/classes/static下实现前后端一体化构建。这里有一个必须处理的坑Vue路由使用history模式时页面刷新会404。因为SpringBoot默认只处理静态资源映射/course/list这样的路径在服务端并不存在。解决方案是写一个WebMvcConfigurer的视图控制器把非/api的所有路径转发到index.html。Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{path:[^\\.]*}) .setViewName(forward:/index.html); }这段配置的本质是让所有没有点号后缀的路径都交给前端路由处理。注意这里的正则排除了带点号的请求这样静态资源.js、.css、.png不会被拦截。实现这个配置后前端路由的刷新问题才算是彻底解决。5.3 三种部署形态对比部署形态主要有三种对应不同使用场景。第一种是单体Jar包部署前端静态资源打进SpringBoot后执行java -jar适合实训演示和个人项目推荐用这个。第二种是前后端分离部署前端放到Nginx后端打Jar包单独运行中间通过/api路径转发适合正式上线。第三种是Docker容器化部署一个Dockerfile里打包后端一个Nginx容器托管前端再用docker-compose编排适合需要频繁迁移环境或者做CI/CD的团队。我做过一次Docker部署踩过一个大坑MySQL容器里没有设置时区默认是UTC导致考勤记录在显示的时候比实际时间少8个小时。排查到最后发现问题不是Java代码而是MySQL容器的时区设置。解决方式是启动容器时加参数docker run -d \ -e MYSQL_ROOT_PASSWORD123456 \ -e TZAsia/Shanghai \ -p 3306:3306 \ mysql:8.0这个TZ环境变量必须加上。如果你已经启动了容器也可以用docker exec进入容器修改全局时区但下次重建容器还会丢失不如启动参数一步到位。6. 常见问题与避坑经验6.1 MySQL安装与时区、SSL问题MySQL的安装过程本身不难但有两个问题经常让人卡住。第一个是MySQL 8.x的驱动类变成了com.mysql.cj.jdbc.Driver不是原来的com.mysql.jdbc.DriverSpringBoot 2.7自动配置没问题但如果用老版本MyBatis或者手动配置数据源就容易踩坑。第二个是连接时报SSL错误原因是MySQL 8.x默认启用SSL而本地开发环境的连接串没有配SSL参数。推荐在JDBC连接串上关掉SSL并指定时区spring: datasource: url: jdbc:mysql://localhost:3306/attendance?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver这里的serverTimezoneAsia/Shanghai是必须的。如果你不指定MySQL驱动会取系统默认时区一旦服务器时区和中国时区不一致LocalDateTime的读取就会出现8小时偏差考勤时间全部错位排查起来比代码逻辑错误更头疼。MySQL 5.7和MySQL 8.0的安装差异也提一下。5.7没有caching_sha2_password认证插件8.0默认用这个很多老旧的客户端连接8.0时会报认证错误。解决办法是安装时选mysql_native_password或者在创建用户时指定认证方式。不过用Navicat新版、IDEA新版连接8.0基本没问题问题多出在跑老版本代码的JDBC驱动上。6.2 MyBatis的缓存与TypeHandlerMyBatis的二级缓存是新手容易踩雷的地方。一级缓存是SqlSession级别的同一个SqlSession内重复查询同一SQL直接走缓存这个基本没问题。二级缓存是namespace级别的一旦开启多个SqlSession之间会共享缓存。问题在于如果表之间有关联查询二级缓存可能导致脏数据。比如查“课程教师姓名”这个关联结果被缓存到CourseMapper的namespace里此时Teacher表的数据更新了CourseMapper的缓存并不知道查询结果还是旧数据。这就是我在考勤系统里默认不开二级缓存的原因。什么时候适合开纯单表查询、数据几乎不变、并发又高比如系统配置表、字典表。其他情况尤其是带JOIN的业务表别开。TypeHandler在MyBatis里的作用是将Java类型与JDBC类型互转。最典型的场景是LocalDateTime和数据库DATETIME的转换。MyBatis 3.4.5之后原生支持LocalDateTime基本不用自己写。但如果遇到枚举类型比如考勤状态字段status在Java代码中是枚举在数据库中是String那么实现一个TypeHandler让MyBatis自动完成转换就很方便。MappedTypes(AttendanceStatus.class) public class AttendanceStatusTypeHandler extends BaseTypeHandlerAttendanceStatus { Override public void setNonNullParameter(PreparedStatement ps, int i, AttendanceStatus parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter.getCode()); } Override public AttendanceStatus getNullableResult(ResultSet rs, String columnName) throws SQLException { return AttendanceStatus.fromCode(rs.getString(columnName)); } // 其他两个 getNullableResult 类似实现 }配置到application.yml里或者通过TableField注解指定。这个做法让代码可读性提升一个档次状态流转的逻辑不会散落在各个if/else里。但也要注意TypeHandler一旦定义错误排查难度远大于普通的类型转换问题建议在单元测试里把枚举和字符串的互转都跑一遍。6.3 前端路由模式与刷新404很多人前端开发一切正常打包部署后学生访问/course/list直接白屏或者404就是路由模式的问题。Vue Router默认是hash模式地址栏里带#号刷新不会404但看起来不专业。改成history模式地址干净漂亮但生产环境必须配合服务端配置。如果后端是SpringBoot的单体部署用前面说的addViewControllers方法转发。如果后端是Nginx在配置里加一行location / { try_files $uri $uri/ /index.html; }这句话的意思是找不到对应的静态文件时一律返回index.html。这样前端路由的所有路径在刷新时都能正常加载。还有一个小问题打包后的资源路径。Vue CLI默认的publicPath是/如果部署在服务器的根路径下没问题如果部署在子路径比如example.com/school/必须把publicPath改成相对路径或者子路径前缀否则所有js、css请求都会404。这个配置虽然在开发环境看不出来但部署到生产环境几乎必踩。6.4 一个真实排查案例批量考勤统计慢的优化有一个真实的优化经历值得分享。教师端点击“导出整学期考勤Excel”时接口响应时间从0.5秒暴增到8秒多。排查下来发现问题出在查询逻辑上。最初的实现是查询每个学生的每次考勤记录然后在Java里循环判断“这个学生是否请假”“这个学生是否缺勤”。思路没错但实现方式是“N1查询”先查出课程所有学生列表然后每个学生再查一次考勤表、再查一次请假表。一个班级50个学生数据库就要执行150条SQL当然慢。优化方案是用SQL一次查出所有学生的考勤汇总用LEFT JOIN考勤表和请假表一次性把状态计算出来SELECT stu.id AS student_id, stu.name AS student_name, COUNT(att.id) AS total_classes, SUM(CASE WHEN att.status LATE THEN 1 ELSE 0 END) AS late_count, SUM(CASE WHEN att.status ABSENT THEN 1 ELSE 0 END) AS absent_count, SUM(CASE WHEN leave.id IS NOT NULL THEN 1 ELSE 0 END) AS leave_count FROM schedule_instance si JOIN student stu ON stu.class_id #{classId} LEFT JOIN attendance_record att ON att.schedule_instance_id si.id AND att.student_id stu.id LEFT JOIN leave_request leave ON leave.student_id stu.id AND leave.status APPROVED AND leave.start_time lt; si.end_time AND leave.end_time gt; si.start_time WHERE si.course_id #{courseId} AND si.week_number BETWEEN #{startWeek} AND #{endWeek} GROUP BY stu.id, stu.name这一条SQL替代了原来的150条SQL响应时间从8秒降到了0.3秒。这个经历说明了一个普适原则能用JOIN和聚合解决的统计问题不要放在Java循环里做。SQL虽然复杂一些但数据库天生就是干这个的。做完这个优化之后我又在班级、学院两个粒度的统计页面上复用了这套逻辑本质都是调整WHERE条件和GROUP BY字段。所以第一版设计时把排课实例表做出来后续写统计SQL时就特别顺。如果一开始没有这张表这种聚合查询会复杂到让人崩溃。我个人在实际项目里的最大体会是考勤系统难的不是某个单独的技术点而是数据口径的一致性。签到状态、请假状态、缺勤状态、出勤率百分比如果每个模块各算各的最后对账一定对不上。所以动手写代码之前先把状态字典和统计口径定义清楚比如“出勤率 正常签到次数 /应出勤次数 - 请假次数”并且让所有接口都复用同一个统计SQL模板这样后期才不会改一处崩三处。最后再分享一个小技巧开发阶段可以把MyBatis的SQL日志打印开起来在application.yml里配置mybatis.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl这样每条SQL执行时都能在控制台看到完整的语句和参数。排查“查到0条数据但数据库明明有数据”这种魔幻问题的时候这个日志能帮你把问题迅速缩小到SQL拼接逻辑上。等部署上线前再关掉避免日志刷屏和参数泄露。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →