资讯动态

基于Spring Boot的考勤管理系统开发实战:从零搭建到部署

发布时间:2026/10/17 12:36:54 来源:尧图企业网站定制
做考勤系统这件事最开始并不是什么宏伟的项目规划。当时团队只有几十号人每个月底HR统计考勤都要加班到很晚用Excel表格一列一列地核对打卡机记录、请假单、加班单。最离谱的一次因为一个员工两个月的调休记录没对上掰扯了整整一个星期。后来实在扛不住了我主动提出做一套基于JavaWeb的Spring Boot企业考勤管理系统技术栈就定成了java springboot layui jsp bootstrap mysql maven。这套系统上线后考勤统计从原来的两三天变成了几分钟请假审批流程从纸质签字变成了线上流转。这篇文章我把自己从零搭建这套系统的完整过程、踩过的坑、以及核心模块的实现细节全部整理出来给准备做类似管理系统的读者一个可参考的完整方案。1. 项目概述考勤系统到底在解决什么问题1.1 核心需求拆解企业考勤系统的本质是把三件麻烦事线上化记录、审批、统计。记录是指员工每天上下班的打卡数据包括正常打卡、迟到、早退、忘记打卡、外勤等场景审批是指请假、加班、调休等申请流程一个完整的请假单要从员工发起经过部门主管审核再到人事归档统计是指月底或者任意时间段内把每个人的出勤天数、迟到次数、请假时长、加班时长汇总成报表。我走访了几个部门后发现考勤系统并不是简单的打卡工具。真正影响效率的痛点在于打卡记录和请假数据分散在多个地方月底需要人工合并审批流程没有留痕出了问题双方各说各话部门主管想看看本月团队谁经常迟到只能找HR要数据。所以这套系统在功能规划上就不能只做打卡还要兼顾审批流、权限控制和数据统计。1.2 系统的角色与功能边界我在设计初期就把用户划分为四类角色每一类看到的界面和可操作的功能完全不同角色核心功能关注点普通员工打卡签到、查看个人考勤、发起请假/加班申请简单好用、记录准确部门主管审批下属申请、查看本部门考勤统计效率高、数据透明HR管理员管理员工信息、处理考勤异常、生成月度报表自动化、可导出系统管理员部门/岗位维护、考勤规则配置、系统参数设置稳定、可配置这个角色划分非常重要它决定了系统的菜单结构和权限拦截逻辑。比如说一个普通员工在系统里只能看到我的考勤、请假申请、我的申请单而部门主管才能看到待审批列表和部门考勤统计。我在后面实现权限控制时基于Spring Boot的拦截器对请求URL做了细粒度拦截每个人进来能干什么、不能干什么都通过角色的菜单权限来控制这样就不容易出现越权操作的问题。2. 技术选型复盘为什么偏偏是这套组合2.1 Spring Boot与JavaWeb的取舍很多人在技术选型时会犹豫传统的SSHSpring Struts Hibernate架构还是直接用Spring Boot。我的建议很明确直接用Spring Boot。考勤系统这种典型的CRUD管理系统用Spring Boot可以省掉大量XML配置起步快内嵌Tomcat让部署也变得异常简单。而且Spring Boot 2.x版本的自动配置能力非常成熟数据源、事务、静态资源、Web MVC这些基础能力都开箱即用。这套系统我从计划到能跑通第一个打卡流程只花了两天时间。要是放在以前的SSH框架时代光整合Struts和Hibernate的配置文件就要调个两三天。2.2 JSP加Layui加Bootstrap的搭配逻辑可能有人会问为什么不用Vue这类前后端分离方案反而选择JSP服务端渲染加Layui说实话这个问题我当时也犹豫过。但仔细分析后就发现企业内部的考勤管理系统有几个天然特点访问量不大、用户都是公司内部员工、业务场景以表单和表格为主、开发周期有限。使用JSP做服务端渲染可以让服务端在处理权限校验之后直接将数据Model传到视图层业务代码结构清晰像表单回显、分页查询这类操作天然简单。Layui负责提供组件化的界面能力它自带的表格、表单、日期选择器、弹出层恰好覆盖了考勤系统的绝大多数页面类型。表格模块的渲染性能足够用表单模块的校验规则写起来也很顺手。而Bootstrap在这里主要是负责响应式布局和弹窗样式我把自己写的几个自定义CSS组件以及打印报表时的排版样式都基于Bootstrap进行了微调。两者并不冲突Layui管组件交互Bootstrap管整体布局和栅格体系。2.3 Maven统一依赖管理的必要性一个项目只要超过十个依赖手动去管理JAR包就是噩梦。Maven在这里的价值不光是自动下载依赖更重要的是版本统一管理。比如Spring Boot的BOM帮你锁定了所有Spring官方组件的版本你在配置文件中只需要声明用哪个starter而不需要操心具体的jar版本号这一点在实际开发中能规避很多莫名其妙的不兼容问题。我还在Maven里配置了MySQL驱动、JSP相关依赖tomcat-embed-jasper和jstl、Apache POI用于导出Excel考勤报表、Lombok减少实体类的getter/setter样板代码。所有依赖在pom.xml中集中管理任何一个同事拉下代码执行mvn clean package就能构建出一个可部署的war包交付起来非常省心。3. 数据库设计一张表结构撑起整个考勤体系3.1 核心表结构规划考勤系统的数据模型是整个项目的地基表设计得不合理后期改起来非常痛苦。我最终把核心表分成了五类人员组织、打卡记录、假期申请、加班调休、考勤规则。人员相关表包括sys_user用户表、sys_department部门表。其中sys_user表除了基础的账号、密码、姓名、手机号之外还加了department_id关联部门以及position岗位字段、hire_date入职日期字段。密码字段我存的是MD5加盐之后的密文保证就算数据库泄露也不能直接反推明文密码。打卡记录attend_record表是这套系统的核心之一。它记录了每个员工每一次打卡操作。字段包括user_id、record_date、punch_time、punch_type上班卡/下班卡、source考勤机/手动补卡、异常标记等。设计时需要特别注意每一次物理打卡都应当原样保留不能因为后来发现是漏打卡就物理删除原记录否则月底审计对账时会非常麻烦。假期相关的表包括请假类型表leave_type、请假申请表leave_request、审批记录表approval_record。请假申请表里有leave_type_id、start_time、end_time、duration_days、reason、status等字段。审批记录表则完整记录一个申请单从发起到最终审结的每一步操作包括审批人、审批意见、操作时间、操作结果。考勤规则表attend_rule是容易被新手忽略的设计。我在里面存放了上午上班时间、上午下班时间、下午上班时间、下午下班时间、宽限分钟数、是否需要打卡等参数。这一张表让考勤时间可以灵活调整不同部门可以拥有不同的考勤规则而不是把上下班时间写死在代码里。3.2 考勤状态的存储策略冗余与实时计算相结合这是我在设计过程中反复权衡的一个点。最初的想法是每次打卡时直接计算出正常、迟到、早退等状态字段存储到数据库。但很快就发现这样做会埋雷。因为考勤规则是会变的假如公司某个月把宽限时间从10分钟改成15分钟那之前存储的迟到状态就全部失效了。所以最终方案是打卡记录表里只存储原始打卡时间和来源状态全部不落库。当有人需要查看日考勤或月统计时根据当前的考勤规则实时计算。这么做虽然查询时会多一步计算但保证了系统在任何时间调整规则历史数据都无需迁移。系统里我加了一个重算考勤的功能就是在考勤规则调整后后台批量重新生成某时间段的统计记录做到既保留原始数据又保证统计结果正确。3.3 时间字段与索引设计的坑打卡记录表的数据量是增长最快的一个两百人的公司一天就有四百条记录一年下来就是十万条级别。因此对这张表建立联合索引是必须的。我建了一个(user_id, record_date)组合索引所有按个人查询考勤的请求都能直接走索引。月报查询通常按一段时间范围来过滤记录所以我在record_date字段上也单独建了索引。时间字段统一用datetime类型存北京时间。这里特别提醒一下MySQL驱动连接串里的serverTimezone参数必须显式配置否则开发机和中国服务器之间可能出现8小时的时差。这个问题我后面在常见问题章节会详细展开。请假相关表的时间字段需要考虑跨天场景。一个员工请的是上午半天假从9点到12点30分但第二天还要补30分钟不是不可能。所以在设计start_time和end_time时我直接用datetime不做日期和时间拆分的过度设计后端用LocalDateTime处理起来足够灵活。4. 后端核心实现把业务逻辑拆明白4.1 登录校验与会话管理登录是整个系统的入口我用Spring Boot的HandlerInterceptor实现了登录拦截。未登录的用户访问任何URL都会被重定向到登录页这比在每一个Controller里手写校验要省心得多。代码层面上我设计了一个简单的会话机制Controller public class LoginController { Autowired private SysUserService sysUserService; PostMapping(/login) public String login(String username, String password, HttpSession session, Model model) { SysUser user sysUserService.login(username, password); if (user null) { model.addAttribute(msg, 用户名或密码错误); return login; } session.setAttribute(loginUser, user); session.setAttribute(roleId, user.getRoleId()); return redirect:/index; } GetMapping(/logout) public String logout(HttpSession session) { session.removeAttribute(loginUser); return redirect:/login; } }这里的Model.addAttribute(msg, ...)用来在JSP页面上显示错误信息服务端渲染在表单验证回显时确实很方便。登录成功后我根据用户的角色ID动态查询其菜单权限然后渲染到主页面的侧边导航栏。Spring Boot的后端在这里只负责把当前用户能看到的菜单列表传过去前端用JSTL的forEach标签循环输出这样每个用户拿到的菜单天然就是个性化的。4.2 打卡签到防重、补卡与时差处理打卡接口的核心逻辑是PostMapping(/attend/punch) ResponseBody public Result punch(RequestParam(type) Integer type, HttpSession session) { SysUser user (SysUser) session.getAttribute(loginUser); LocalDateTime now LocalDateTime.now(); LocalDate today LocalDate.now(); // 防重同一天、同一类型、同一人只能打一次卡 AttendRecord lastRecord attendRecordMapper.selectLatestByUserAndType(user.getId(), type, today); if (lastRecord ! null) { return Result.error(今日该类型打卡已存在请勿重复操作); } AttendRecord record new AttendRecord(); record.setUserId(user.getId()); record.setRecordDate(today); record.setPunchTime(now); record.setPunchType(type); record.setSource(manual); attendRecordMapper.insert(record); return Result.success(打卡成功); }这段代码看起来简单实际落地时还需要考虑几个细节。第一是防重逻辑如果不做校验用户连续点击两次签到按钮就会产生两条上班卡记录后续统计时会认为他上了两遍班。第二是考勤机场景如果公司有实体考勤机一般会提供一个数据导入接口把考勤机的原始数据Excel导入系统这时punch_type和source字段就会体现价值。真正的难点在忘记打卡这个场景。员工早上来了公司但到晚上才发现忘记打卡。系统不能直接允许员工本人补卡否则会出现人不在公司却手动补卡的情况。所以我的方案是员工只能发起补卡申请由主管审批后再写记录。4.3 迟到早退与加班时间的计算迟到早退和加班的计算是所有逻辑里踩坑最多的地方。我单独写了一个AttendanceCalculator类思路是把一天分成上午段和下午段分别判断。比如某公司的考勤规则是上午9点到12点下午13点30分到18点。员工上午8点50分打卡规则里设置宽限10分钟9点10分之前打卡都算正常。计算逻辑就是取当天该员工所有的打卡记录打卡时间早于上午规定上班时间或者在宽限范围内的第一次打卡作为上午上班卡上午下班时间点之后的第一条记录作为上午下班卡如此类推。public AttendStatus calculateDailyStatus(ListAttendRecord records, AttendRule rule) { // 找到上午上班打卡最早打卡时间 // 找到上午下班打卡上午下班时间之后的第一条打卡 // 找到下午上班打卡下午上班时间之后的第一条打卡 // 找到下午下班打卡最晚的打卡记录 // 根据打卡时间和规则中的基准时间比较得到正常/迟到/早退状态 }加班时长的计算与请假相反它统计的是下班时间之后累计的打卡停留时长并且要考虑加班审批单员工不能无审批随意加班。加班明细表里存储了审批通过后的加班开始时间、结束时间系统根据结束时间减开始时间自动算出加班时长调休时长也从这个数据派生。这个模块后来被HR多次夸奖因为调休和加班工资计算再也不用人工翻记录了。4.4 请假审批状态机的设计与流转请假审批流我用了一个简单的状态机模型。leave_request表里的status字段有四个取值范围0待审批、1已通过、2已驳回、3已撤销。员工新增一条请假申请时状态是0主管审批时把它改成1或者2员工自己在申请未被审批之前可以撤销改成3。系统不允许修改或删除已经审批通过的假单这是防篡改的底线。为了让这个流程对开发者或者接手者清晰我还在代码级别明确了状态转移规则本质上就是一个Map或switch判断核心是防止非法状态跳转。比如已通过的假单不能再次被驳回已驳回的假单不能再次通过只有待审批状态才能同时走审批和撤销两个方向。审批通知用简单的系统站内信实现。审批通过或者驳回时系统自动在通知表里给申请人插入一条新记录登录首页右上角的红点提示数实时增长。4.5 月度统计报表与导出每月考勤统计是HR最关心的模块。我的表设计里有一张统计汇总表attend_summary它存储每个员工在某个自然月内的统计结果包括出勤天数、迟到次数、早退次数、请假天数、加班时长、旷工天数等。统计过程采用按天累加按月汇总的策略。每天晚间通过定时任务回顾当天的考勤记录更新当天汇总月底再生成整月汇总。这样做的好处是数据在月底不用一次性重算全部只要逐天运行即可系统的压力非常平稳。报表导出我用的是Apache POI。最开始直接用HSSFWorkbook写.xls格式后来数据量上来了改成SXSSFWorkbook流式写入导出百人三个月的考勤数据基本两三秒就能完成。关键代码大致如下SXSSFWorkbook workbook new SXSSFWorkbook(100); Sheet sheet workbook.createSheet(月度考勤汇总); // 创建表头、填充数据行、设置列宽、单元格样式 // 最终通过response输出流写出文件导出时要特别注意Workbook的关闭和临时文件的清理SXSSFWorkbook使用后必须调用dispose()否则会留下临时文件占用磁盘。5. 前端页面实现Layui与JSP的详细配合方案5.1 JSP后端渲染的技术细节在Spring Boot中使用JSP确实有特殊配置。首先要在pom.xml中引入tomcat-embed-jasper依赖同时导入JSTL标签库。然后在application.yml里指定视图解析路径spring: mvc: view: prefix: /WEB-INF/jsp/ suffix: .jsp系统打包时用war包部署而不是jar包。因为Spring Boot内嵌Tomcat对JSP支持存在限制如果用jar包运行JSP页面能访问但会有诸多诡异问题比如页面改一下就需要重新打包。部署到外部Tomcat反而是最稳的方式。JSP页面里大量使用JSTL标签c:forEach items${list} varrecord tr td${record.recordDate}/td td${record.punchTime}/td td${record.statusName}/td /tr /c:forEach注意JSP页面中不能直接使用Java 8的LocalDateTime对象直接调用toLocalDate()之类的方法JSTL表达式取值受限。所以我通常在VO类里提前格式化好字符串字段比如把localDateTime字段格式化成一个formattedTime字符串给前端显示这样页面上只做展示不参与时间计算。5.2 Layui表格渲染与分页查询后端列表页几乎全部用Layui的table模块来渲染。Layui的table.render方法结合服务端分页接口交互逻辑非常固定table.render({ elem: #attendTable, url: /attend/list, page: true, cols: [[ {field: recordDate, title: 日期}, {field: punchTime, title: 打卡时间}, {field: punchTypeName, title: 类型}, {field: statusName, title: 状态} ]], request: { pageName: pageNum, limitName: pageSize }, parseData: function(res) { return { code: res.code, msg: res.msg, count: res.total, data: res.list }; } });这里我要求后端返回固定的JSON结构即{code: 0, msg: ..., total: 100, list: [...]}。Layui的table模块对code值为0的判断是成功因此后端Result类封装要统一。这个约定在项目初期就定死前后端并行开发时不至于各写各的对接不上。分页查询的后端代码我用PageHelper插件实现。Maven引入pagehelper-spring-boot-starter然后在查询Mapper接口前调用PageHelper.startPage(pageNum, pageSize)后面紧跟的查询语句就会被自动拦截分页并返回PageInfo对象。5.3 Bootstrap在表单校验与弹窗中的运用系统中的请假申请、补卡申请、审批操作等页面统一使用Bootstrap的Modal弹窗加表单提交。比如主管审批时点通过或驳回按钮后弹出一个Modal里面要求填写审批意见然后AJAX提交到后端。表单校验这块Bootstrap默认只有基础样式没有交互校验所以我在Bootstrap基础上组合使用Layui的form模块做前端校验比如提交请假申请时日期是否选择、请假类型是否为空、时长是否合理这些校验直接写在Layui的表单监听里。这样既保留了Bootstrap的视觉效果又弥补了它的交互短板。5.4 记住一个经验不要盲目引入过多前端框架这套系统的前端页大概有二十多个我最终只用了一个公共的layout.jsp页面壳子加上功能页面。公共壳子引入了Layui和Bootstrap的样式文件页面内容区用include标签引入。不要试图在一个项目里同时用ElementUI、Ant Design、Layui等多套组件体系那会让页面风格混乱到没法维护。一个后端管理系统的前端稳定、统一比花哨重要得多。6. 部署与测试中遇到的经典问题6.1 MySQL时区导致打卡时间偏移8小时这是新手上线最容易踩的第一个坑。现象是系统里记录的打卡时间是下午两点但服务器本地时间明明是晚上十点。原因就是MySQL驱动连接串没有配置serverTimezone。解决的方案是在jdbc连接URL中显式指定jdbc:mysql://localhost:3306/attend_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai同时在后端统一使用LocalDateTime避免使用java.util.Date和数据库datetime之间的时区隐式转换。6.2 JSP页面中文乱码问题JSP页面如果首部没有设置pageEncoding或者数据库连接没设置characterEncodingutf8就会出现中文乱码。我的习惯是在所有JSP页面顶部统一加上% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %同时Spring Boot的配置文件里增加server: servlet: encoding: charset: UTF-8 enabled: true force: true这样能保证请求参数和响应体都使用UTF-8编码基本摘掉乱码隐患。如果仍然乱码绝大多数情况是操作系统本地文件的编码问题检查一下JSP文件本身是不是UTF-8编码保存。6.3 重复打卡与并发请求问题我在打卡接口里写了当天该类型只能打一次的校验但在并发场景下可能存在两次请求同时通过校验、继而插入两条记录的情况。因为两个请求同时执行select时都查不到旧记录。最后的解决办法是给attend_record表加了一个唯一索引索引列为(user_id, record_date, punch_type)。数据库层面兜底应用层再做强校验这是最稳妥的双保险。如果希望更优雅可以在插入时使用INSERT ... ON DUPLICATE KEY UPDATE的方式但考虑到业务需要给出友好提示我仍然保留了先查后插加唯一索引的方案。6.4 Layui与Bootstrap样式冲突同时引入Layui和Bootstrap时按钮、表格、表单控件的基本样式会相互覆盖。我在公共样式里重新定义了一部分覆盖规则比如Layui按钮和Bootstrap按钮的边距、字体大小统一在app.css中收敛。处理思路归纳为一个原则Layui负责表格、表单、日期选择这类交互组件Bootstrap负责布局和栅格尽量避免两套框架同时作用于同一个DOM元素。如果某个页面想用Bootstrap的Modal放Layui的表格没问题但不要给同一个元素同时加两者的class属性。6.5 每月统计对不上账的排查流程考勤系统上线后最容易被HR质疑的就是统计数字。我遇到过统计结果比实际多出一天的情况排查下来是有人在夜间跨天时打卡考勤记录日期取的是打卡时间但统计判断按自然日来算导致某天没有上班卡却有下班卡。后来我在系统里增加了一个考勤异常标记功能当天只有一条打卡记录的、当天没有任何打卡记录的、请假审批通过但当天没有假期的全部自动标记为异常并在考勤月报上以红色叹号提示。这个功能上线后大大降低了人工对账的成本。7. 项目落地的几个建议7.1 先跑通最小闭环再做功能堆砌我在开发这套系统时第一个完成的闭环是员工登录、上班打卡、下班打卡、查看今日打卡记录。这四个功能走通后系统的基本框架就成型了。后续再加请假、审批、统计、导出功能本质上都是在这个框架上增加业务模块。不要一开始就想着把报表做得特别漂亮Excel导出格式调得多么完美。考勤系统的核心数据是准确性和可追溯性在这个基础上做的UI才有意义。先让数据口径正确再优化用户体验。7.2 用权限模型控制操作边界这套系统的权限模型不复杂但非常关键。我在Menu表里保存每个菜单的URL标识在RoleMenu表里关联角色与菜单系统登录后根据角色ID查出所有可访问的URL集合。在拦截器中对于每一个请求都会判断请求路径是否包含在用户允许访问的集合中。一个小经验是权限校验尽量放在后端拦截器而不是前端菜单显示上。因为前端菜单隐藏不代表接口不能访问一个懂技术的员工直接通过URL调用你的后端接口就能绕过菜单限制后端的权限校验才是真正的安全边界。7.3 开发期就保留数据迁移脚本考勤系统的表结构随着需求迭代变化非常快。我从一开始就规定所有DDL变更必须写SQL脚本放到项目的sql目录下。这样任意一个同事拉取最新代码后直接执行迁移脚本就能把数据库升级到最新版本不需要手动传输数据库文件。这次教训来自一次上线前我改了表结构没写脚本另一个同事的本地库一口老血白干了一下午。8. 写在最后的心里话这套基于Spring Boot的企业考勤管理系统从开发到落地前后用了大约三周的时间。说实话技术上并没有高深的地方它不是一个炫技的项目更像是一个把业务逻辑理清楚、把工程约束做到位的典型案例。但恰恰是这种看似平常的管理系统最容易暴露工程化能力表设计是否合理接口是否稳定权限是否严密统计是否可信。如果这些点都能经得起推敲那这套系统的价值就已经超过了代码本身。我在实际使用中发现最值得骄傲的不是某个功能多厉害而是月底的时候HR不再需要加班核对考勤同事之间不再因为申报的加班时长和调休天数吵架。系统把模糊变成了清晰把口头承诺变成了流程记录。这也让我更加确信好的管理系统应该是存在感很低的让人感觉不到系统的存在只感觉到流程顺畅了。如果你正准备开发类似的JavaWeb管理系统我的建议是先认真思考数据模型它决定了系统的上限再把审批流这种简单状态机想透它决定了系统的稳定度最后才去纠结UI风格和交互细节它们决定了用户愿不愿意每天打开这个系统。希望这篇总结对你有所帮助踏踏实实把每一个模块做好你会收获比代码本身更多的东西。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑