考勤管理系统在 Java Web 这个圈子里,基本是仅次于商城和博客的第三大练手题材。表面看就是一张上下班打卡表,实际做深了会发现,排班规则、请假审批、加班时长统计、月度汇总导出,任何一项单独拿出来都够折腾好几天。我最近完整过了一遍这套 考勤管理系统源码,技术栈很标准:后端 SpringBoot2,前端 Vue3,持久层 MyBatis-Plus,数据库用 MySQL8.0,还带了一份完整的项目文档。整体看下来的感受是:它既适合做毕业设计、课程设计的底座,也适合刚接触前后端分离开发的人拿来当作“第一个能跑通全链路的项目”,因为踩坑成本比较低,又能把权限、打卡、审批、统计这些典型场景一次看全。
这篇文章不打算把源码逐行念一遍,而是想从“怎么理解这个项目”和“这套技术栈在考勤场景里是怎么配合的”两个角度拆开讲。后端部分会重点聊表结构关系和考勤统计的实现思路,前端部分会集中在 Vue3 的动态路由、状态管理和接口层封装上,最后用一整节整理我在部署运行过程中实际遇到的坑和排查链路。如果你正准备拿这套源码二次开发,或者只是想参考它的组织方式,按这个顺序读下来应该会比自己瞎翻代码高效很多。
1. 项目价值拆解:这个考勤系统到底解决了什么问题
1.1 从标题信息能读出的技术定位
先说说我拿到“SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0”这套组合的第一反应。这属于目前国内中小型管理系统里非常主流的一套前后端分离配置:SpringBoot2 负责提供 RESTful 接口,Vue3 负责页面交互,MyBatis-Plus 把单表 CRUD 的重复劳动降到最低,MySQL8.0 作为底层数据库支撑。相比传统 JSP 项目,这种结构让前端和后端可以独立开发、独立部署,也更容易扩展到微服务架构。
这里有一个值得注意的选型逻辑:为什么用 MyBatis-Plus 而不是 Spring Data JPA?考勤系统的业务特点决定了答案——它包含大量动态查询和报表统计场景,比如按部门筛考勤记录、按日期范围查请假单、统计某个月每个人的迟到次数。MyBatis-Plus 既保留了 MyBatis 手写 SQL 的灵活性,又提供了 LambdaQueryWrapper 这种面向对象的查询构造器,日常的增删改查基本不用拼字符串,需要复杂报表时又能随时退回 XML 手写 SQL,进退都比较从容。
1.2 “含文档”这件事对学习路径的影响
标题末尾写着“含文档”,这个细节在选项目的时候很重要。很多网上的开源项目代码能跑,但没有任何开发文档,新手拿到手根本不知道表结构为什么要这么设计、启动时该改哪些配置、接口返回的结构是什么。这套源码带了文档,意味着至少包含了数据库初始化脚本、接口说明和部署步骤,这对想拿它二次开发的人来说,节省的远不止一两天时间。
不过我也要说句实话:文档能帮你“跑起来”,但很难帮你“改明白”。真正有价值的是把项目里那些模块之间的边界弄清楚。比如用户管理负责维护登录身份,角色菜单负责控制访问权限,考勤组负责把人归到不同的考勤规则下,记录表只负责存打卡原始数据,统计逻辑则放在服务层去计算。理解了这些边界之后,再去看文档里的接口列表,你会发现自己已经能预判每个接口大致长什么样了。
1.3 为什么推荐用它做毕业设计或练手项目
考勤系统的业务复杂度刚好卡在一个非常舒服的位置。如果只做打卡和查询,那它就是个 CRUD 项目,没什么技术含量;但如果把排班、加班、请假审批、月度统计都加进来,它的复杂度又足够覆盖 RBAC 权限模型、流程状态流转、多表关联统计、时间处理等真实开发中的高频问题。对毕设来说,这个复杂度正好能撑起一篇像样的论文;对刚入行的开发者来说,它又是一个能让你把前端路由守卫、Token 鉴权、MyBatis-Plus 分页、MySQL 函数这些零散知识点串成线的项目。
这套源码的价值更偏向“完整的参考实现”而不是“可直接落地的商业系统”。建议你的预期也放在这个位置上:先跑通,再魔改,最后逐步把其中某些模块换成更工程化的实现——比如引入 Redis 缓存用户信息、用消息队列处理打卡异步写入——这时候你对这套技术栈的理解就真正入门了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 后端模块与核心表设计:考勤数据应该如何组织
2.1 从包结构看项目整体分层
后端代码的包组织方式通常是理解一个项目的第一把钥匙。这套源码在顶层上划分得比较常规,但每层内部的处理值得学习:
我按常见实践推测,项目的包结构大致是 controller、service、mapper、entity、config、common 这几层。controller 只做参数接收和结果包装,不写任何业务逻辑;service 层承担全部业务流程;mapper 层对应 MyBatis-Plus 的 BaseMapper 接口;entity 里是和数据表一一对应的实体类;config 放全局配置类,比如拦截器注册、跨域处理、MyBatis-Plus 分页插件;common 则放统一返回结果、异常处理、常量定义等横切内容。
这种分层的意义不在于代码文件放得整齐,而在于出了问题你能快速定位。举个例子,如果加班统计算错了,你不需要去 controller 里找答案,直接打开 service 层对应的方法就能看到完整逻辑;如果发现所有接口都返回 401,十有八九是 config 里的拦截器配置出了问题。对于学习项目,这种可定位性比炫技式写法重要得多。
2.2 考勤模块的数据表关系,这是整个系统的地基
看一个考勤系统设计得好不好,不要先看页面,先看表结构。尤其是“员工、考勤组、班次、打卡记录”这四张表之间的关系,基本决定了后续所有功能的扩展性。
从常见实现来看,核心表可以归纳为:
- 用户表:存账号、密码、姓名、部门、手机号、状态等基础信息。密码一般存 MD5 或 BCrypt 加密后的密文,不会存明文。
- 部门表:以树形结构维护组织架构,部门 ID 会作为用户表的外键,用于按部门统计考勤。
- 角色表 / 菜单表 / 角色菜单关联表:组成一套精简版 RBAC 权限模型,决定不同账号能访问哪些菜单和按钮。
- 考勤组表:定义一组考勤规则,比如组名称、上下班时间、迟到宽限分钟数、工作地点范围等。
- 考勤组与用户关联表:把员工分配到某个考勤组。需要注意,这里用的是关联表而不是在用户表里直接加考勤组 ID,目的是支持一个人从 A 组调去 B 组的变更留痕。
- 班次表:定义更细粒度的排班信息,例如早班、中班、晚班,各自有不同的上下班时间。
- 打卡记录表:存储每天每次的打卡原始数据,一般包含用户 ID、打卡时间、打卡类型(上班/下班)、打卡来源(手机端/PC 端)、地理位置等。
- 请假申请表:包含请假类型、开始时间、结束时间、事由、审批状态。审批状态推荐使用数字字典,而不是直接存“已通过”这三个汉字,后端根据状态值做分支判断更可靠。
- 考勤统计表或视图:按月汇总每人的出勤天数、迟到次数、早退次数、缺卡次数、请假天数、加班时长等。
几个关键字段的设计逻辑我要单独提醒一下:打卡时间字段一律用 datetime,不要用 varchar 存字符串,否则后面做时间范围查询、按月分组统计会非常痛苦;金额类、时长类字段尽可能用 decimal 和整数类型,避免浮点数误差;逻辑删除字段 del_flag 作为全局约定,能统一所有表的删除行为。
2.3 考勤规则设计为什么比想象中复杂
真正完整设计过考勤规则的人都知道,打卡只是入口,规则才是核心。一个最简单的场景:公司规定 9 点上班、6 点下班,那么 9 点 05 分打卡算不算迟到?午休时间是否要排除在工时之外?弹性工作制下怎么计算是否满 8 小时?这套源码如果只是“打了卡就记录”,那它就是个玩具项目;但只要涉及考勤组映射,它的设计维度就立刻不一样了。
我的建议是读源码时重点关注考勤组和班次的关系:一张班次表可以被多个考勤组引用,一个考勤组又可以绑定多个班次,中间通过关联表维护多对多关系。这种设计看似复杂,实际上是为了应对不同的排班策略。举个具体例子:技术部实行 10 点弹性上班,客服部必须 8 点半到岗,你不能在用户表里写死每个人的上下班时间,否则人力资源调整规则时只能一条条去改员工数据。通过考勤组中间层的隔离,规则调整变成了“把人从一个组挪到另一个组”的一次性操作。
2.4 请假审批流的落地方式
和请假相关的表看着简单,但里面藏着整个系统状态流转最重要的逻辑。一张请假单从提交到通过,会经过“待审批 → 已通过 / 已驳回”这样的状态变化。在关系型数据库里,最简单可靠的做法是加一个 status 字段,用整数表示状态:0 待审批、1 已通过、2 已驳回,必要时还可以加一层上级审批,变成 0 待部门审批、1 待人事确认、2 已完成、3 已驳回。
为什么不用字符串存“待审批”“已通过”?因为字符串状态容易因为手误产生脏数据,“已通过”和“通过”在程序里是两个完全不同的值,排查起来极麻烦。用数字字典辅助说明,接口返回时再到前端统一翻译成文字,这是我在很多项目里验证过比较稳的做法。源码里如果能看到状态机思想,哪怕只是简单的 switch 判断,都说明作者考虑到了审批流的本质。
3. 安全认证与统计报表:接口层最容易出彩的两个场景
3.1 登录认证的通用处理方式
现在的前后端分离项目已经很少用 Session 维持登录态了,主流做法是 Token 认证。这套源码从技术栈判断应该也是这样:用户输入账号密码后,后端校验通过生成一个 Token 返回给前端,前端把它存在本地,每次请求时放在请求头里带上,后端通过拦截器统一校验 Token 的合法性。
这里要理解两个细节。第一是拦截器里校验 Token 通过后,通常会把当前登录用户的信息存到 ThreadLocal 里,这样后续的 controller 和 service 方法不需要每个都先从请求头里解析一遍用户 ID,直接通过 UserContext.getUserId() 就能拿到当前操作人。第二是密码加密推荐用 BCrypt 而不是简单的 MD5 加盐,因为 BCrypt 每次生成的哈希值都不同,即使两个用户密码相同,密文也不一样,数据库泄露后的风险会小很多。当然,很多毕设项目为了赶时间会直接做 MD5 加密,如果源码用了这种方式,至少也应该加上自定义 salt,不要裸奔。
3.2 打卡记录的去重设计与补卡逻辑
打卡场景有个很容易被忽略的问题:同一用户同一分钟内多次提交打卡请求怎么办?比如手机网络差,用户狂点了几次“打卡”按钮,后端如果不对这种重复请求做防护,数据表里就会出现多条几乎一样的记录,月度统计时这个人可能被算成打了十次卡。
常见的解决方案有三种思路:一是前端按钮做防重复点击,比如打卡成功后一秒内禁用按钮;二是后端在打卡记录表上建立唯一索引,把 user_id 和 date 组合成唯一键,数据库层面直接拒绝重复插入;三是后端服务里加分布式锁,防止并发请求同时插入多条数据。中小型系统最实用的是方案一加方案二组合——前端防止用户误触,数据库兜底防并发。这套项目如果没做这个处理,我建议你自己在二次开发时补上,因为在真实的考勤场景里,重复打卡是一个非常高频的问题。
补卡是另一个容易忽略的功能。员工早上忘打卡了怎么办?通常需要一个“补卡申请”入口,让员工选择补卡日期和补卡时段,提交后走审批流程,审批通过后由系统写入一条正常打卡记录。这个场景虽然不起眼,但它涉及和请假审批一样的流程设计,一旦加入会显著增加系统的真实感。
3.3 考勤统计的实现思路,把计算留给数据库还是服务层
考勤统计是我认为这套系统里技术含量最高的部分。月度报表里需要展示应出勤天数、实际出勤天数、迟到次数、早退次数、缺卡次数、请假天数、加班时长等指标,这些指标如果全部用 Java 代码去循环计算,不仅代码臃肿,性能也堪忧。更合理的做法是尽量把分组、聚合、排序下推到 MySQL 完成,利用 SQL 的能力减少数据传输量。
MySQL8.0 相比 5.7 一个很大的优势是支持窗口函数,比如 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY punch_time)。这个能力在考勤统计里非常实用:你想找出每个员工每天的第一次打卡时间和最后一次打卡时间,用普通 GROUP BY 很难表达,但用窗口函数可以轻松实现。
我推测这套源码的统计部分应该是用 MyBatis-Plus 的 XML 自定义 SQL 完成的,因为复杂 SQL 写注解里可读性太差。这里也想提醒一下:阅读时不要跳过 mapper XML 文件,统计报表的精华几乎都在那几段 SQL 里。后面我单独讲 MySQL8.0 时还会提到窗口函数在报表中的具体写法。
3.4 MyBatis-Plus 在复杂查询上能做到什么程度
很多人用 MyBatis-Plus 就只停留在 BaseMapper 自带的 selectById、selectList 上,遇到多表关联就慌了。实际上 MyBatis-Plus 在分页、条件构造、逻辑删除上都做得比较完善。分页插件是这套源码几乎一定会配置的组件,它可以把分页参数自动拼装进 SQL,避免每个 Mapper 手写 LIMIT 语句。
使用分页插件时有一个容易踩的坑:分页插件对一对多关联查询的支持有限。如果员工表和打卡记录表是一对多关系,直接对关联查询结果分页会出现数据错乱或总数不准的问题。正确的做法是先在子查询里对打卡记录完成分页,再和员工表关联,或者分别查询后手动组装。读这套源码时如果你发现某些列表页的数据数量不对,可以优先排查是不是在这里出了问题。
时间字段的处理也值得留个心眼。MySQL 里 datetime 和 Java 的 LocalDateTime 映射在大多数情况下是自然的,但如果你在实体里用了 Date 而数据库用了 datetime,配合 Jackson 序列化时极易出现时区偏移问题。这个坑我在后面“运行中实测”那一节会完整展开。
4. 前端侧的协作方式:Vue3 如何承接这些后端能力
4.1 项目构建方式,Vite 和 Vue3 的组合
后端是 SpringBoot2,前端对应 Vue3,那工程化工具大概率选的是 Vite。相比 Vue CLI(基于 Webpack),Vite 在开发环境下的冷启动速度和热更新效率提升是肉眼可见的。Vue CLI 启动一个大项目可能需要几十秒,Vite 往往两三秒就完成了,这对日常开发体验的提升非常明显。
如果你拿到源码后发现前端目录结构里有 index.html 放在根目录、src 下面是 components/views/router/store/api 这种经典结构,基本上就是 Vite + Vue3 的标准布局。不要小看前端目录的整洁程度,后面你要加一个“加班申请”页面时,如果不知道页面组件放哪、路由去哪注册、接口请求去哪封装,再小的功能都会让你寸步难行。
4.2 动态路由与权限控制的完整链路
在 RBAC 权限模型里,前端需要解决的问题是:不同角色登录后看到的菜单不一样。如果只是把用户的菜单权限全量写在 localStorage 里,那前端防得住正常人,防不住会看接口的人。真正实用的做法是动态路由。
流程一般是:用户登录 → 后端返回 Token 和用户基本信息 → 前端拿到用户角色后,再请求一个“获取当前用户可见菜单”的接口 → 根据返回的菜单树动态注册路由,同时渲染侧边栏。Vue Router 4 提供了 addRoute 方法支持运行时添加路由,这套源码大概率就是用这种方式实现的。
按钮级权限则是另一层控制。菜单能看见不代表按钮能用,比如考勤报表页面里“导出 Excel”按钮只有管理员可见。这个一般通过自定义指令 v-permission 实现,指令内部判断当前用户权限列表里是否包含对应编码,没有就直接把元素从 DOM 上移除。阅读代码时你可以在 main.js 或 directive 目录下找找这个指令的定义。
4.3 状态管理工具的选择,以及什么时候需要它
Vue3 项目里状态管理的默认选择是 Pinia。相比 Vue2 时代的 Vuex,Pinia 的 API 设计更简洁,去掉了 mutations 概念,没有嵌套模块的困扰,TypeScript 支持也更友好。在考勤系统里,全局状态通常用来存用户信息、角色、Token、菜单权限,这些数据在多个页面中都要使用,放进全局 store 才能避免每个页面都重复调用接口获取。
但我也想提醒一点:不要把所有东西都塞进 Pinia。打卡记录、请假单列表这些数据,属于页面局部状态,放在组件内部维护就好。过度全局化会导致状态依赖关系混乱,改一个页面时根本不知道谁在引用这个状态。
4.4 接口层封装的几个关键点
axios 封装几乎是每个 Vue3 管理系统的标配,考勤系统也不例外。我在这种项目里比较看重的封装点有四个:
- 基础路径集中管理:根据开发环境和生产环境切换 baseURL,开发环境通常通过 Vite 的 proxy 配置把 /api 转发到后端地址,避免跨域。
- 请求拦截器:统一从 store 或 localStorage 取出 Token,放进 Authorization 请求头。
- 响应拦截器:后端返回统一结构 code/data/msg,拦截器统一判断 code,等于 200 直接返回 data,不等于 200 就弹出错误提示,同时处理 401 场景——Token 过期,自动跳回登录页。
- 类型提示:能定义泛型的尽量定义,避免接口返回的数据在页面上全是隐式的 any,不然 Vue3 + JavaScript 项目后期维护会很痛苦。
关于 401 自动跳登录,有一个体验细节值得关注:如果用户同时在多个标签页打开了系统,单独一个标签页 Token 过期时不应该直接粗暴地清掉 localStorage,否则所有标签页都会瞬间掉线。稳妥的处理是跳转前弹一个提示,或者只清当前页面的状态并做一次轻提示,让用户知道需要重新登录。
4.5 考勤报表页面对时间处理的依赖
考勤页面是时间处理重灾区。日期选择器、月份切换、打卡时间显示、请假时长计算,任何一个环节没注意时区都会出错。前端通常用 dayjs 来格式化时间,体积小、API 与 moment.js 相似,项目里引入它基本不会错。
一个常见的诉求是按月份展示考勤日历,比如 12 月 3 日用绿色标识“正常”,12 月 5 日用红色标识“迟到”。这种页面在实现时的核心不是前端怎么画格子,而是后端接口返回什么格式的数据。推荐后端把某月每天的考勤状态组装成一个数组直接返回,前端只负责渲染;如果后端返回的是打卡明细,让前端自行判断每天状态,判断逻辑就会在前端重复实现三遍,维护成本极高。
5. 环境准备与本地跑通的完整流程
5.1 版本锁定,这一步决定了你能否顺利启动
拿到源码后,第一件事不是看代码,而是对版本。很多项目跑不起来不是代码问题,而是本地环境和作者环境不一致。根据这套源码的技术栈,推荐的版本组合如下:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | SpringBoot2 对 JDK8 支持最稳,JDK17 需要额外适配 |
| Maven | 3.6 以上 | 管理后端依赖 |
| Node.js | 16 以上 | Vite5 对 Node 版本有要求,18 更稳 |
| MySQL | 8.0 任意小版本 | 注意安装时选 utf8mb4 字符集 |
| Redis | 视源码而定 | 如果引入了缓存则需要,否则可跳过 |
如果你本机已经装了 MySQL5.7,想直接切换成 8.0,建议先彻底停掉 MySQL 服务,备份好原数据再操作。Windows 上最常见的问题是 MySQL8.0 安装后服务无法启动,绝大多数情况是 data 目录初始化失败或者端口被占用。安装时请重点关注选认证方式那一步,选“Use Strong Password Encryption”即可,不要选兼容旧版本那一项,否则后续用新版连接驱动反而容易报错。
5.2 数据库初始化的关键步骤
源码文档里应该会附带 sql 文件,没有的话就去项目根目录找 database 或 sql 文件夹。执行顺序要注意:先创建数据库,再按依赖关系执行脚本。可以用命令行执行:
bash复制mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS attendance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
然后导入脚本:
bash复制mysql -u root -p attendance < /path/to/attendance.sql
导入完不要急着启动后端,先核对几个关键数据。打开 sys_user 表看看初始管理员账号是什么,密码大概率是加密后的字符串,不能直接改成明文,否则登录逻辑会失效。再看看菜单表和角色表是不是有初始化数据,如果权限表是空的,即使账号密码对了,登录后前端也拿不到菜单,页面会一片空白。
5.3 后端启动的配置修改清单
用 IDEA 打开后端项目后,在等待 Maven 下载依赖的过程中,可以先修改配置文件。application.yml 里最需要关注的是:
yaml复制server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/attendance?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 你的数据库密码
driver-class-name: com.mysql.cj.jdbc.Driver
这里的 serverTimezone 参数我建议直接写成 Asia/Shanghai,不要用 UTC,否则后面所有时间字段都会差 8 个小时。另外注意 SpringBoot2 的默认驱动类是 com.mysql.cj.jdbc.Driver,如果你在网络上搜到老的 com.mysql.jdbc.Driver(不带 cj),那是 MySQL5.7 时代的写法,适配 MySQL8.0 时会告警甚至报错。
如果你发现项目里有 Redis 相关依赖和配置,本机却没有 Redis 服务,启动大概率会失败。不需要 Redis 时可以直接注释掉相关依赖,或者安装一个 Redis 并修改连接配置。读代码时找到 RedisConfig 或使用 StringRedisTemplate 的位置,你就能判断它是否属于启动必需项。
5.4 前端启动的方法与代理配置
前端目录下执行 npm install 安装依赖,这里提醒一句:如果你处在网络环境不太稳定的区域,装依赖失败是很常见的事,先检查 npm 源配置再重试比反复删 node_modules 高效得多。配置国内镜像源可以有效缓解:
bash复制npm config set registry https://registry.npmmirror.com
依赖装完后启动开发服务器:
bash复制npm run dev
正常情况下 Vite 会打印一个本地访问地址,比如 http://localhost:5173。这时页面能打开,但接口大概率是报错的,因为前端还带着 localhost:5173 的地址去请求后端。真正的关键在 Vite 代理配置。打开 vite.config.js,确认里面有没有这样一段:
javascript复制server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
这段配置的意思是:前端所有以 /api 开头的请求都会被 Vite 转发到后端 8080 端口,从而规避浏览器跨域限制。注意如果你的后端端口不是 8080,这里的 target 要同步修改。很多新手启动完前端发现所有接口 404,查了半天代码,结果只是代理目标端口错了。
5.5 验证系统是否正常的三个检查点
系统启动后,我建议不要急着点一遍所有功能,先做三个快速验证:
- 用管理员账号登录,确认能跳转到首页而不是停留在登录页报错。
- 打开“员工管理”页面,确认列表能加载出数据,说明数据库连接正常。
- 找一个带日期筛选的查询,随便搜几个月,确认时间范围查询能正常返回。
这三个验证点分别对应数据库连接、Token 认证、基础 CRUD,任何一个失败都能说明问题出在底层配置,而不是某个页面代码写错了。
6. 实际运行中四个高发问题的完整排查链路
6.1 打卡时间比本地时间整整早了 8 小时
这个问题在前后端分离项目里实在太经典了。现象是:你在数据库里手动插入一条 18:00:00 的打卡记录,页面上却显示 10:00:00,或者反过来,页面传 9:00,数据库存的变成了 1:00。
排查链路要分三段走。先查 MySQL 连接 URL 里有没有 serverTimezone=Asia/Shanghai,没有就补上;再查后端 Jackson 对 LocalDateTime 的序列化配置,如果你引入了 jackson-datatype-jsr310,需要确保 spring.jackson.time-zone=GMT+8;最后查前端有没有对接口返回的时间做本地化处理。这三层里任何一层用了 UTC,整条链路上的时间就会错位。
MySQL8.0 本身还有一个隐含坑:安装时系统时区如果不是中国时区,MySQL 的默认时区也会跟着偏。可以用下面这条 SQL 查看:
sql复制SELECT NOW();
如果 SELECT NOW() 返回的时间和你的本地时间不一致,说明 MySQL 系统时区有问题。在连接 URL 里加 serverTimezone 能解决 JDBC 层面的转换,但最彻底的方案是修改 MySQL 配置文件 my.ini,在 [mysqld] 段落下设置 default-time-zone = '+08:00',然后重启 MySQL 服务。
6.2 分页插件查询总数不对
MyBatis-Plus 分页插件统计总数时,会自动生成一条 SELECT COUNT(*) 语句。但在多表关联查询场景下,这条自动生成的 count SQL 可能会统计出错误结果。典型症状是:第一页显示 10 条数据,总数为 15,但你把数据全查出来发现一共 12 条,中间差了好几次。
排查的第一步是打开 MyBatis-Plus 的 SQL 日志,看看分页插件实际生成的 count 语句是什么。如果 count 语句把一对多关联的表也 join 进来,就会把同一用户的多条打卡记录都算进去,导致总数膨胀。解决思路有两种:一是写一个专门用于 count 的 SQL 覆盖自动生成的语句;二是在 mapper 接口上手动编写分页查询的不同 SQL,让 count 语句只统计主表。源码中如果已经踩过这个坑,会在 XML 里写两个 SQL,一个查列表一个查总数,阅读时可以留意这一点。
6.3 Vue3 动态路由导致刷新页面后白屏
用动态路由方案时有一个很典型的问题:用户登录后系统通过 addRoute 动态注册了路由,此时页面一切正常,但用户按 F5 刷新后,动态路由失而复得的过程出现时间差,页面匹配不到当前路径,直接白屏或者 404。
排查链路是:检查路由守卫里是否等动态路由全部 addRoute 完成后才放行,检查刷新后是否先重新拉取了用户菜单权限再决定跳转目标,检查是否有单独的通配路由兜底。最常见的修复方式是,在全局前置守卫里增加一个标志位——如果 Pinia 里还没有菜单数据,就先调用接口获取菜单并动态注册路由,注册完成后用 next({ ...to, replace: true }) 重新进入当前路由;如果已经有数据,就直接 next()。只要动态路由的注册动作发生在页面跳转之前,白屏问题基本能杜绝。
读这套前端源码时,你可以在 router/index.js 和 permission.js(路由守卫文件)里看到这个过程。如果作者没有处理得很完善,这正好是你拿来自我训练的改进点。
6.4 MySQL8.0 的 Binlog 不断膨胀,开发机磁盘占用飙升
这个问题和源码本身不一定直接相关,但只要你本机装了 MySQL8.0 做开发,几乎都会遇到。MySQL8.0 默认开启了 binlog 日志,目的是支持数据恢复和主从复制,但在开发机上它没有任何实际作用,只会让日志文件不断累积,占用大量磁盘空间。
排查方法很简单,执行:
sql复制SHOW BINARY LOGS;
如果发现 binlog 文件很多且体积很大,确认不需要主从复制和基于时间点恢复后,可以修改配置文件关闭 binlog。在 MySQL8.0 中,my.ini 或 my.cnf 的 [mysqld] 段落下加上:
ini复制skip-log-bin
然后重启 MySQL 服务。重启之后可以用命令验证:
sql复制SHOW VARIABLES LIKE 'log_bin';
如果显示 OFF,说明已经生效。需要注意,关闭 binlog 会让数据库失去基于日志的恢复能力,但对本地开发项目来说影响微乎其微,反而换来了干净的磁盘空间。
7. 从这套源码里能提炼出的可复用设计
7.1 统一返回结构和异常处理是项目的门面
考勤系统里几十个接口,如果每个接口返回的数据结构都不统一,前端 axios 封装再漂亮也无从适配。这套源码大概率会有一个 Result 或 R 类,定义 code、message、data 三个字段,所有 controller 都返回这个结构。异常处理则通过 @RestControllerAdvice 全局捕获业务异常和系统异常,把堆栈信息转换成友好提示返回前端。
这个小设计看起来微不足道,但在二次开发时价值巨大。你新加一个“导出考勤报表”接口时,只需要遵循同样的返回结构,前端就能直接复用已有的响应拦截器,不需要为这个新接口单独写一套错误处理逻辑。一个项目的可维护性,很多时候不是靠某个大架构决定的,而是靠这些约定统一的细节积累出来的。
7.2 代码生成器的意义,以及为什么不要替代理解
既然用了 MyBatis-Plus,就不得不提它自带的代码生成器。根据数据库表结构反向生成 entity、mapper、service、controller,几分钟就能把所有单表 CRUD 代码生产完毕。对这套考勤系统来说,用户管理、部门管理、菜单管理这类基础模块,用代码生成器快速铺路非常合适。
但代码生成器解决不了业务问题。代码生成器能生成“请假单表的增删改查接口”,但它生成不了“请假时长如何排除周末和法定节假日”的计算逻辑,也生成不了“考勤状态如何自动判定迟到早退”的规则引擎。这部分才是考勤系统的灵魂,也是你读源码时应该花时间最多的位置。如果只是刷一遍自动生成的 CRUD 代码就以为自己看懂了项目,那还不如不看。
7.3 考勤规则抽象的思路可以迁移到其他业务
最后说一个比较大但很有价值的观察。考勤系统的本质是“在既定规则下对用户行为做判定与统计”。这个规则加判定的模型,可以非常自然地迁移到其他系统:订单系统里的超时自动关闭、会员体系里的积分过期策略、任务系统里的截止时间校验、审批系统里的流程节点控制,本质上都是同类问题。
阅读这套考勤源码时,如果你能把“考勤组规则怎么配置、状态怎么流转、异常数据怎么做兜底处理”这套思路抽离出来,那它的价值就不只是一个考勤项目代码本身。我见过不少从考勤系统起步,后来转到审批流引擎、排班调度系统的人,回头再看这段经历,都觉得当时对规则抽象和状态流转的理解帮了大忙。
我自己在实际调试这套系统时最深的一个感受是:它的难点一直不在某个单一技术上,而在所有模块协同时的边界划分。账号体系、考勤组关系、审批状态、统计口径,每个模块单看都简单,合在一起就需要你在改接口时想清楚会不会影响其他模块。如果你能把这个项目的 doc 文档读完,再亲手把某个模块重新实现一遍,比如把请假审批改成两级审批,你对 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 这一整套技术栈的掌握,基本就超过大多数只跟着教程敲过 demo 的人了。
