ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot员工考勤系统实战:从表设计到审批流程与定时跑批

Spring Boot员工考勤系统实战:从表设计到审批流程与定时跑批 这段时间正好把一个基于 Spring Boot 的员工考勤系统从零到一完整落地了。从最开始梳理考勤业务规则到表结构设计、打卡和审批流程实现再到每天凌晨自动跑批的数据汇总前后踩了不少坑也沉淀出一些可以直接拿来用的经验。这篇文章就把整个设计思路、技术选型、核心功能实现和遇到的典型问题完整记录下来给正在做类似 Spring Boot 管理系统、或者想从业务角度理解考勤系统的朋友一些参考。考勤系统看似简单无非是上班打卡、记录出勤、月底算工资。但真做起来会发现光“迟到几分钟算迟到”这种问题就牵扯到班次、宽限时间、外勤定位、补卡申请一系列设计。再加上请假审批、加班时长、异常考勤处理逻辑并不比一套进销存系统简单。我下面按实际开发顺序来讲从需求拆解开始到技术栈搭配再到核心模块的代码级实现和踩坑实录尽量把关键决策背后的为什么也一并说清楚。1. 先想清楚考勤业务再动手写代码1.1 别被“打卡”两个字带偏真正要梳理的是角色和流程很多新手拿到这种题目第一反应就是做一张打卡表记录用户ID、打卡时间就完事。但考勤系统的核心绝不是打卡而是围绕考勤产生的数据如何在“员工—主管—人事”这三方之间流转。我的做法是先画三角色模型。员工要能打卡、请假、加班申请、查看自己的出勤汇总部门主管要能审批下属的请假和加班申请、查看所辖团队的异常考勤HR或系统管理员负责排班、维护班次、处理补卡申请、查看全公司月度报表。这三类角色的数据权限完全不同后续所有表结构和接口设计都得围绕它们展开。有了角色之后还要梳理一条主线流程员工某天正常上班在班次时间窗内打卡系统自动记录并判断状态如果请假走申请流程主管审批通过后系统在日结时按请假类型处理当天考勤如果漏打卡员工发起补卡申请审批通过后修正当天记录每天晚上系统自动跑批生成当天的考勤汇总月底再汇总成月度报表供薪资系统使用。这条链路一旦理清功能清单就自然而然有了后续写代码只是把这些节点逐个实现。1.2 功能需求拆成四个模块打卡、审批、统计、异常管理我实际开发时把功能拆成四个模块而不是按角色拆。这样代码分层更干净业务边界也清晰。打卡模块上班卡、下班卡、外勤打卡、补卡申请。核心是打卡时间窗判断。审批模块请假申请与审批、加班申请与审批、补卡审批。核心是状态流转和多级审批。统计模块日汇总、月汇总、个人出勤明细、部门出勤统计。核心是精确的工时计算和异常归类。异常管理模块迟到、早退、缺卡、旷工提醒异常改判。核心是标记规则和人工修正记录。这四个模块之间不是孤立的。请假审批通过后要影响当天统计补卡审批通过后要回写打卡记录加班审批通过后要在月末汇总时生成加班工时。所以我在设计表的时候就预留了审批单号与考勤记录的关联字段初衷很简单考勤的最终结果必须是由“打卡记录 审批单据”共同决定的少了任何一个都不完整。1.3 状态机设计审批流程别用一堆if else硬写审批模块是最容易被低估的部分。请假从提交到归档中间至少经历“草稿、待审批、已通过、已驳回”四个状态如果公司要求部门主管审批后再由人事复核状态就更多。这里一定要用状态机思维而不是在每个Service方法里堆if (status 1) { ... } else if (status 2) { ... }。我的实现方式是定义一个审批状态枚举每个枚举里维护“可流转到哪些状态”和“当前状态允许谁操作”。public enum ApproveStatus { DRAFT(0, 草稿), PENDING(1, 待审批), APPROVED(2, 已通过), REJECTED(3, 已驳回), CANCELLED(4, 已撤销); }关键点是建一个attendance_approval表把审批单号、业务类型请假、加班、补卡、当前状态、申请材料、审批意见、审批人、审批时间全部统一存储。这样不管是请假还是加班都走同一条审批链路前端也可以共用一套审批组件。我最初偷懒给请假和加班分别建了审批表后来发现写两套逻辑很痛苦果断合并成一张表彻底解决了这个问题。2. 技术选型与项目结构Spring Boot只是起点搭配才是关键2.1 技术栈组合为什么选MyBatis-Plus而不是JPA这个项目我用的组合是Spring Boot 2.7 MyBatis-Plus 3.5 MySQL 8.0 Redis MinIO Vue 3没有用更花哨的微服务组件。先说为什么是 Spring Boot 2.7 而不是 3.x。考勤系统是典型的单体应用2.7 版本生态最成熟网上资料几乎踩不到坑MyBatis-Plus 和各种生成工具适配也很稳定。遇到有些教程说 Spring Boot 3.x 必须用 jakarta 命名空间、部分框架还没适配的情况选型时直接避开省下大量排查时间。如果确实要用 3.x记得把javax.servlet换成jakarta.servlet但还是建议没特别需求就停留在 2.7。MyBatis-Plus 的理由更直接单表 CRUD 不用写 SQLLambdaQueryWrapper一行代码搞定条件查询分页插件也就一个配置的事。考勤系统一半以上的操作是简单的增删改查用 MyBatis-Plus 能省掉大量重复的 Mapper XML。但复杂统计类 SQL 我仍然手写比如月汇总报表的关联查询用框架拼条件反而可读性差。Redis 在系统里承担两件事存登录会话和做接口防重复提交。MinIO 用来存打卡时上传的自拍照片和头像这是参考了公司实际需求加的比直接把图片存服务器本地更规范。2.2 项目分层结构一张图记住包结构很多人拿到 Spring Boot 项目不知道包怎么划分。我的做法是严格按controller / service / mapper / entity / dto / vo六层来组织。com.company.attendance ├── controller # 接收请求参数校验返回统一结果 ├── service # 业务逻辑层事务放这里 │ └── impl ├── mapper # MyBatis-Plus 接口 ├── entity # 数据库实体 ├── dto # 前端传参对象 ├── vo # 返回前端的数据封装 ├── config # 配置类如 Redis、MinIO、拦截器 ├── common # 工具类、统一返回结果、异常处理 └── job # 定时任务分层的好处是考勤这种业务后续一定会改需求比如增加一个“加班转调休”的功能你只需要改 Service 层Controller 基本不动Mapper 可能加一条 SQL改造成本很低。我这次遇到部门主管要求能批量审批就是只在 Controller 层加了一个批量审核的接口复用同一个 Service 方法十分钟搞定。2.3 数据库表设计考勤核心表和关系数据库是考勤系统的地基我最终设计了几张核心表这里只挑最关键的三张讲。第一是attendance_shift班次表。字段包括班次名称、上班时间、下班时间、宽限分钟数、是否跨天、加班起始时间。注意跨天场景比如晚班 22:00 到次日 06:00如果只存时间不存跨天标记日历判断会出大问题。第二是attendance_record打卡记录表存储员工某天的上班卡、下班卡、打卡来源、定位地址、打卡照片URL、考勤状态。这张表我加了work_date字段单独存“考勤归属日期”而不是直接用打卡时间因为晚班跨天时打卡日期和考勤日期是不同的。第三是attendance_daily_summary日汇总表每天定时任务把原始打卡记录加工后写入这里包含迟到分钟数、早退分钟数、缺卡标记、请假类型、工时等。前端展示的日考勤直接查这张表不查明细表性能压力小很多。CREATE TABLE attendance_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, work_date DATE NOT NULL, shift_id BIGINT NOT NULL, clock_in_time DATETIME, clock_out_time DATETIME, clock_in_address VARCHAR(255), clock_out_address VARCHAR(255), clock_in_photo VARCHAR(500), clock_out_photo VARCHAR(500), source_type TINYINT COMMENT 1-考勤机 2-手机定位 3-补卡, leave_type TINYINT DEFAULT 0 COMMENT 0-正常 1-事假 2-病假 3-年假, attendance_status TINYINT, create_time DATETIME );这里有个细节我在建表时把“考勤状态”和“请假类型”分开了。之前有同事把请假直接写成一种考勤状态结果月末统计时“请假”和“迟到”没办法同时存在比如上午请假下午迟到如果合在一起就丢数据。分开存之后状态判断就灵活了。3. 核心功能实现打卡、审批、统计逐一落地3.1 打卡接口时间窗、外勤定位、防重复提交打卡接口是系统的最前端入口看起来简单实现时有三件事必须处理好时间窗判断、重复提交拦截、外勤定位。时间窗判断的逻辑我封装成一个独立的AttendanceCalculateUtil工具类输入班次信息和打卡时间输出“正常、迟到、早退”等初始状态。判断的核心逻辑并不复杂但宽限分钟的处理很关键。LocalTime shiftStart shift.getStartTime(); LocalTime clockTime record.getClockInTime().toLocalTime(); // 宽限分钟比如上班时间 09:00宽限 5 分钟09:05 前不算迟到 boolean late clockTime.isAfter(shiftStart.plusMinutes(shift.getGraceMinutes()));打卡时系统会要求用户处于公司定位范围内前端把经纬度传来后端用 haversine 公式计算距离超过 500 米就拒绝打卡。有些系统做得很粗只在文章开头写一句“外勤打卡”实际完全可以做得更细区分内勤和外勤两种模式外勤打卡上传定位和现场照片存到 MinIO 里这样月末核对时也有据可查。防重复提交是我强烈建议加的一个功能。如果有个员工手抖连点两次“打卡”两次请求都进来就会生成两条上班卡。我在 AOP 层用 Redis 做了一个简单防重机制以userId 日期 卡类型作为 keysetnx 成功后执行打卡逻辑执行完再删除 key。这样同一用户同一卡类型同一自然日只能成功一次。3.2 工时计算迟到、早退、加班怎么算才不吵架工时计算是考勤系统的灵魂也是最容易让业务方不满意的地方。这里必须把规则从业务那边确认清楚再落到代码里。我采用的是一种比较通用的算法应出勤工时取“打卡实际时长”和“班次计划时长”的逻辑关系而不是直接硬算两次打卡间隔。比如某天班次 09:00 到 18:00员工 09:30 才到18:00 准时走那实际在岗时间是 8.5 小时按 8 小时计还是按 8.5 小时计薪资逻辑完全不同。我这里的规则是迟到时长按分钟扣减早退同理午休时间固定扣除。这样算下来每天工时 下班打卡时间 - 上班打卡时间 - 午休分钟数 - 迟到分钟数 - 早退分钟数再和应出勤工时做比较。加班工时的计算要关联加班审批单。员工加班前先提交加班申请审批通过后系统才把他 18:00 之后的打卡时长记为加班工时。没有审批单的时间即使人在公司也不算加班。这个逻辑必须在代码里强制否则后面人事核对时各种扯皮。实现上我在attendance_daily_summary表里加一个overtime_minutes字段每天跑批时先查当天有没有审批通过的加班单再结合下班打卡时间计算加班分钟数。3.3 请假审批一张表统一搞定所有审批类型请假模块我重点解决了“不同请假类型不同审批流程”的问题。最初的设计是每个请假类型写一个审批方法后来发现代码重复率极高无非是判断一下请假天数有没有超过主管权限。我把审批流程整合成统一的ApprovalService.submit(ApprovalDTO)。前端传审批类型、申请人、起止日期、事由后端自动判断审批层级。Transactional(rollbackFor Exception.class) public void approve(Long approvalId, Long approverId, boolean pass, String comment) { // 校验当前审批人是否有权限操作当前状态 // 追加一条审批记录 // 更新审批单状态 // 如果最终通过则回调对应的业务处理 }审批通过后的业务回调我是用 Spring 的事件机制实现发布一个ApprovalPassedEvent请假、加班、补卡模块各自监听事件执行自己的后置逻辑。这样做的最大好处是以后加一种新的审批业务类型不需要改动审批主链路代码只需要新增监听器就行。3.4 统计报表与Excel导出给HR少加一点班月底给人事用的月度统计报表核心是“应出勤天数、实际出勤天数、迟到次数、早退次数、请假天数、加班总时长”六项指标。我建了一张attendance_monthly_summary表每月1号定时跑批生成。跑批逻辑比较直接当月有排班且没有请假的天数算应出勤每天汇总表状态为正常的算实际出勤迟到、早退次数从每日状态里count请假天数汇总leave_days加班时长汇总overtime_minutes。Excel 导出我用的是 EasyExcel一个注解标注字段名就能生成报表。实际操作中遇到的问题是月底跑批时如果某天考勤状态是“缺卡”但补卡流程还没走完这天的数据会被自动标记成异常等补卡审批通过后日汇总状态变化月度汇总却不会自动更新。我在跑批逻辑里加了一个“重新计算”机制每月2号之前如果补卡审批完成重新拉取当月数据刷新月度汇总。这个细节如果不处理人事用 Excel 对账时就会发现数字对不上。4. 实战中一定会踩的坑排查过程与经验4.1 定时任务漏跑Spring Schedule的隐藏陷阱日结和月结都依赖定时任务我用的是 Spring Boot 自带的Scheduled注解。开发阶段一切正常但我模拟跨天测试时发现有一天日结任务跑出来后第二天早上看数据却少了几个人的记录。排查了半天发现问题出在任务执行时间上我设置的日结时间是0 30 0 * * ?也就是凌晨 00:30但有个页面的日期边界判断用的是new Date()而数据库里的work_date已经过了零点就对不上了。这里的教训是所有考勤相关逻辑的“当前日期”不要直接取系统时间而是由定时任务框架统一传入业务日期。我在日结任务里手动指定“统计昨天”而不是“统计今天”代码里写死了LocalDate.now().minusDays(1)跨天测试就再也没出过问题。4.2 事务失效同一个类内调用导致Transactional不生效有段时间补卡审批通过后总出现“审批状态更新了但考勤记录没有同步修正”的现象。查日志发现异常被吞了。后来定位到是事务没有生效。问题出在我把approve()方法和修正打卡记录的方法写在了同一个 Service 类里而调用时使用了this.approve()Spring AOP 基于代理实现内部自调用绕过了代理事务注解自然就失效了。解决方法是把修正打卡记录的逻辑拆到独立的AttendanceAdjustService在ApprovalService里注入它来调用。这里提醒一句任何涉及多表更新的业务方法不要写成同类自调用要么拆类要么从外部Bean调用否则事务很容易静默失效。4.3 时区与时间格式LocalDateTime不是存的万能药考勤系统对时间的精确度要求极高时区问题非常隐蔽。项目初期本地测试一切正常部署到服务器后发现打卡记录全部差了 8 小时。原因很简单MySQL 的连接串里没有加serverTimezoneAsia/Shanghai而且服务器系统时区默认是 UTC应用与数据库的时区不一致导致 JDBC 在转换时间时做了偏移。我的处理方法是统一所有时间标准。数据库连接参数明确指定serverTimezoneAsia/Shanghai应用配置spring.jackson.time-zoneGMT8所有实体时间字段统一用LocalDateTime而不是Date。这样就避免了大部分时区转换问题。另外前端 Vue 页面拿到的时间戳后要用dayjs按东八区格式化避免浏览器时区差异把时间显示错乱。4.4 Redis缓存穿透频繁查询不存在的员工导致数据库压力陡增做考勤详情页时我先查 Redis 缓存员工信息没有再去查数据库。测试发现有人反复访问“不存在的员工ID”每次都绕过缓存直达数据库查库次数暴增。虽然考勤系统并发量不算高但这种明显可以被攻击的接口还是要防。防穿透的常规方案是缓存空值。查不到数据库时把空对象也写进 Redis过期时间设置成 3 分钟这样短时间内的相同查询直接返回空不会压到数据库。针对恶意乱传ID的请求还可以在 Controller 层加参数校验ID 小于等于 0 直接返回参数错误。4.5 Spring Boot版本与前端打包vue项目放进Spring Boot的坑部署阶段我用的是把构建好的 Vue 静态资源复制到 Spring Boot 的static目录这样同一个端口就能同时提供前端页面和后端接口。这里有一个需要注意的地方如果使用 history 模式路由刷新页面会出现 404。解决办法是配置静态资源路径把未匹配的请求转发到index.html。Configuration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{spring:\\w}) .setViewName(forward:/index.html); } }另外一个版本相关的坑是Spring Boot 2.7 里如果引入了spring-boot-starter-validation空注解校验需要额外添加Validated到 Controller 层不然NotBlank完全不生效。这个找错方向的话会浪费不少时间建议直接看一眼启动日志有没有MethodValidationPostProcessor的提示。5. 个人实践体会如果再做一个考勤系统我会怎么改做完这个项目如果让我再从头做一遍有三件事我会在一开始就做好。第一是把可配置做到极致。班次、宽限分钟、迟到扣款规则、审批层级这些最好都做成配置表不要写死在枚举或者常量里。我做的时候就是这样每个公司考勤规则都有自己的特殊条款业务方第一次提需求往往说不全系统上线了才开始补充细节配置化能省掉三天两头发版本的麻烦。第二是前端界面上把异常颜色标注清楚。迟到、早退、缺卡、请假各项用颜色区分HR 一目了然。这是后期根据使用反馈加的但确实比堆数字直观得多也算是这个项目里性价比最高的一个改动。第三是重视操作日志。考勤数据涉及工资修改操作必须留痕。谁改了补卡记录改了哪天的数据修改前是什么值修改后是什么值这些都应该写入审计表。这次做的时候由于时间有限只做了简单的日志记录如果正式商用这一步一定不能省。实际做下来最大的感受是考勤系统技术难度不高真正的复杂度全在业务规则上。表设计的时候多想一步状态机定义得干净一点就能避免后面大量返工。如果你也准备写这个选题的毕设或者工作项目希望这些经验能帮你少踩几个坑。
返回列表