ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue考勤管理系统从0到1开发实战

SpringBoot+Vue考勤管理系统从0到1开发实战 去年接了一个公司内部的考勤管理系统项目技术栈定的是SpringBootVue。需求方一开始说得轻巧——“就是上下班打个卡统计一下迟到早退”——等真正调研完各部门的意见才发现考勤系统属于典型的“看起来简单、做起来琐碎”的类型排班规则、请假审批、加班调休、节假日判断、月度统计、导出报表每一块都能延伸出一堆细节。这篇文章我打算把整套系统从0到1的实现思路完整梳理一遍。重点不是贴大段大段的完整代码而是讲清楚“为什么这么设计”和“哪些地方必须提前想明白”。如果你正准备用SpringBootVue写公司考勤管理系统不管是给企业内部用还是作为毕业设计项目这篇文章里拆解的这些问题你都绕不开。我会从需求边界、数据库设计、后端核心逻辑、前端页面组织、权限控制一直讲到部署上线最后把我实际踩过的坑和排查过程完整贴出来。1. 考勤系统的真实场景先搞清楚“管什么”再动手写代码1.1 手工考勤的痛点远远不止“统计麻烦”这么简单很多团队一开始用Excel管考勤几十个人的时候还好超过一百人就开始失控了。我在项目调研阶段看到的典型现象是HR每个月要花两三天收集各部门的Excel还得手工核对请假单、加班申请、调休记录经常对不上账。员工那边也不舒服忘打卡了要找主管签字补卡补卡流程走一圈下来比加班还累。更麻烦的是排班。有的部门是固定早九晚六有的部门是早晚班轮换还有销售团队根本不坐班但要打卡外勤。如果用一张excel存所有规则公式写到后面根本没人敢动。所以考勤系统在企业管理类项目里真正的核心价值不是“替代手工记录”而是“把规则统一收口”。系统上线以后HR不再需要理解每个人的出勤状态是怎么算出来的所有判定交给代码她要做的只是处理异常和审批。1.2 功能边界画清楚MVP阶段先做这四件事和很多项目一样考勤系统最怕一开始就贪大。我建议按照这四块来做第一版基本能覆盖绝大多数中小公司的需求打卡管理支持上下班打卡、外勤打卡记录打卡时间和位置信息补卡申请走审批流程。排班管理管理员维护班次早晚班、正常班、弹性班给不同部门或员工分配排班规则。请假与加班请假事假、病假、年假、调休、加班申请、审批流流转审批通过后自动影响考勤统计。统计与导出按月度生成考勤汇总表包含出勤天数、迟到次数、早退次数、缺卡、请假时长、加班时长导出Excel给HR。先别急着做自动扣薪、人脸识别打卡、钉钉/企业微信集成这些高级功能。第一版上线跑顺了第二版再加都来得及。我见过太多项目死在第一步——功能清单列了二十几项最后连打卡都没做完。1.3 角色与权限模型三种角色起步就够用权限设计上第一版建议做三种角色。员工自己打卡、查看个人考勤记录、提交请假/加班/补卡申请。部门主管审批本部门员工的申请查看本部门考勤统计。系统管理员HR维护员工信息、排班规则、班次处理所有审批查看全员统计并导出报表。不要一开始就做多级审批链比如“主管→经理→HR”三层第一版全部做成一级审批就好。等系统稳定了再在流程表里加一个审批层级字段就不会动到核心逻辑。2. 技术选型为什么2025年我仍然选SpringBootVue这套组合2.1 后端选SpringBoot核心是生态成熟和团队招聘容易现在写Java后端SpringBoot基本是默认选项。它最大的优势不是性能而是“约定大于配置”带来的开发效率和庞大到几乎什么问题都能搜到答案的社区生态。公司内部的考勤系统需求变动频繁今天要加一个字段明天要调整一个计算规则SpringBoot项目改起来非常快。MyBatis-Plus做CRUD、Spring Security做认证授权、EasyExcel做导出每个环节都有现成方案。性能上完全不用焦虑。考勤系统不是高并发场景一个五百人的公司每天早上8:30到9:00是打卡高峰QPS也就几十。单机部署、MySQL数据库绰绰有余。2.2 前端选Vue对后端开发者最友好没有之一前端框架里Vue和React我都用过如果团队里后端工程师需要兼顾前端页面Vue的上手成本明显低一截。它的模板语法接近原生HTML响应式数据绑定理解起来直觉化中文文档和社区也很完善。配合Element UI或者Element Plus后台管理类的页面基本是“拼积木”——表格、表单、弹窗、日期选择器都是现成组件改改字段就能用。考勤管理系统的界面以数据表格、表单、日历视图为主几乎没有复杂的前端交互Vue这套技术栈属于“杀鸡用牛刀但牛刀好上手”。2.3 版本搭配清单这不是迷信是真的会踩坑版本选择上我强烈建议稳妥优先。我的推荐组合是组件版本说明JDK1.8企业级项目最稳几乎不会出兼容性问题SpringBoot2.7.x不要轻易上3.x除非你有精力处理javax到jakarta的迁移MyBatis-Plus3.5.x单表CRUD效率极高MySQL5.7 / 8.08.0默认utf8mb4推荐Vue2.7 / 3.x新项目用3.x Element Plus老项目2.x Element UINode16Vue3配合Vite需要16以上特别提醒SpringBoot 3.x要求JDK17并且把javax.servlet包迁移到了jakarta.servlet很多老教程里的代码直接搬过来会报“包不存在”。如果不想踩这个坑就老老实实SpringBoot 2.7.x JDK8。我后面专门有一个章节讲版本踩坑这里先不展开。3. 数据库设计考勤的核心不是“打卡记录表”而是“排班表”3.1 先忘掉打卡记录想清楚“应该上什么班”才能判定“是否正常”我见过不少考勤系统的数据库设计第一张表就建“打卡记录表”最后做到统计环节才发现一个致命问题没法判断某个人某天算不算迟到。因为你不知道他当天应该几点上班。没有排班数据打卡时间就是一堆无意义的数字。所以数据库设计的第一步是先设计班次表和排班表。班次表定义一天内有几个上班时段比如正常班是09:00-18:00早班是08:00-17:00晚班是14:00-22:00。排班表则记录“某员工在某天执行哪个班次”。有了这两张表后面的所有判断逻辑才有依据。3.2 六张核心表的结构与关系我第一版用的表结构全库一共十几张表核心六张如下sys_user用户表字段包括id、username、passwordBCrypt加密、real_name、department_id、role、status。attendance_shift班次表字段包括id、shift_name、work_start_time、work_end_time、allow_late_minutes宽限分钟。attendance_schedule排班表字段包括id、user_id、shift_id、work_date。attendance_record打卡记录表字段包括id、user_id、work_date、check_in_time、check_out_time、status、source_type正常打卡/补卡/外勤。leave_request请假/加班申请单字段包括id、user_id、typeleave/overtime/retroactive、start_time、end_time、reason、statuspending/approved/rejected、approver_id、approve_time。attendance_summary月度汇总表字段包括id、user_id、month、total_days、late_count、early_count、absent_count、leave_hours、overtime_hours。核心关系一句话就说得清排班表告诉你“该上什么班”打卡记录表告诉你“实际上打了什么卡”两张表一比对状态就出来了。汇总表是结果表每月月底定时任务跑批生成避免统计时实时计算拖慢查询。3.3 打卡记录到底怎么存一行一天还是每次打卡一行这里有个设计分歧。每次打卡存一行结构上更“标准”但要判断一个人某天是否迟到需要先查他当天所有打卡记录再取第一条和最后一条逻辑上绕一步。而且如果某天打了四次卡中午外出吃饭回来又打一次判断逻辑会更乱。我最终选择了“一天一行”的冗余设计attendance_record每员工每天最多一条记录check_in_time存当天第一次打卡时间check_out_time存当天最后一次打卡时间。打卡接口在写入时先查当天是否已有记录有则更新check_in或check_out没有则插入。这样查询和统计都非常简单直接一条记录就是一个完整的工作日状态。代价是失去了“一天多次出入”的明细但对于考勤管理这个场景完全够用。如果你需要统计午休外出时长之类的数据可以再单独建一张明细表但第一版不必。3.4 索引规划这张表一个月几万行索引设计要提前想按五百人规模计算一个月的打卡记录大约1.5万行一年18万行。这个量级不大但查询模式很固定索引还是要建好。我实际使用的索引建议attendance_record联合唯一索引(work_date, user_id)防止重复数据单独索引(user_id)用于个人查询。attendance_schedule联合唯一索引(user_id, work_date)保证一人一天一条排班。leave_request索引(user_id)和个人待办查询索引(status)用于管理员查看待审批列表。attendance_summary联合唯一索引(user_id, month)。联合唯一索引最大的好处是在数据库层面兜住了并发重复插入的问题。比如用户在8:59:59同时用手机和电脑各打了一次卡如果没有唯一索引就可能产生两条记录一天一行模型就废了。4. 后端核心实现打卡接口、状态判定、统计导出这四块是重头戏4.1 打卡接口怎么设计才能尽量防止“代打卡”打卡接口看起来就是插入一条记录但有几个细节必须处理。第一个是来源识别。我设计的打卡接口为POST /api/attendance/checkin参数包含latitude、longitude、address。员工在公司范围外打卡时前端可以弹出确认框让用户选择“外勤打卡”后端会记录source_typeoutside。当然第一版不做GPS围栏强校验因为考勤系统最忌讳的就是“技术限制太死导致员工打不了卡”。第二个是重复打卡。前端按钮要防止连点后端通过唯一索引和事务兜底。插入时使用INSERT ... ON DUPLICATE KEY UPDATE如果当天已有记录则根据当前时间更新check_in_time取更早的或check_out_time取更晚的。第三个是时间窗口校验。正常上班打卡允许从班次开始前2小时到班次开始后allow_late_minutes30分钟之间超出这个窗口的记录标记为异常。比如正常班09:00上班宽限5分钟那么7:00到9:35之间打卡都算正常上班打卡早于7:00的记为异常晚于9:35的直接算缺卡。核心代码如下打卡记录的更新逻辑是关键Transactional public AttendanceRecord checkIn(CheckInDTO dto) { String today LocalDate.now().toString(); // 查询当天排班 AttendanceSchedule schedule scheduleMapper.selectOne( new LambdaQueryWrapperAttendanceSchedule() .eq(AttendanceSchedule::getUserId, dto.getUserId()) .eq(AttendanceSchedule::getWorkDate, today) ); if (schedule null) { throw new BizException(当天无排班无需打卡); } Shift shift shiftMapper.selectById(schedule.getShiftId()); LocalTime now LocalTime.now(); // 判断是上班打卡还是下班打卡 boolean isCheckIn now.isBefore(shift.getWorkStartTime().plusMinutes(30)); AttendanceRecord record recordMapper.selectOne( new LambdaQueryWrapperAttendanceRecord() .eq(AttendanceRecord::getUserId, dto.getUserId()) .eq(AttendanceRecord::getWorkDate, today) ); if (record null) { record new AttendanceRecord(); record.setUserId(dto.getUserId()); record.setWorkDate(today); if (isCheckIn) { record.setCheckInTime(now); } else { record.setCheckOutTime(now); } record.setSourceType(dto.getSourceType()); recordMapper.insert(record); } else { if (isCheckIn (record.getCheckInTime() null || now.isBefore(record.getCheckInTime()))) { record.setCheckInTime(now); } else if (!isCheckIn (record.getCheckOutTime() null || now.isAfter(record.getCheckOutTime()))) { record.setCheckOutTime(now); } recordMapper.updateById(record); } return record; }这里的isCheckIn判断逻辑有一个隐含假设如果员工中午12点打开APP系统会认为当天没有上班打卡记录把12点记成check_in_time。这样会不会有问题会但实际场景中很少有员工中午才来上班——如果真的来了记成check_in反而是合理的。真正的极端情况是晚班员工下午上班时isCheckIn判断要基于班次的work_start_time来算。4.2 迟到、早退、缺卡的判定状态计算要写成独立的Service考勤状态计算是整个系统里最容易写乱的地方。我的建议是抽一个独立的AttendanceCalculator组件输入是排班、打卡记录、请假单输出是状态枚举。不要把这个逻辑散落在Controller或者Mapper里。状态判定规则如下NORMAL打卡时间在班次开始时间宽限分钟内且下班打卡时间不早于班次结束时间。LATE上班打卡时间晚于班次开始时间宽限分钟但未超过班次开始时间60分钟。EARLY下班打卡时间早于班次结束时间但晚于班次结束时间-60分钟。ABSENT全天无打卡或只有一次打卡且时间异常。HOLIDAY_LEAVE当天有请假单且状态为approved。REST当天没有排班。这里有一个容易忽视的点如果员工早上迟到晚上又早退状态应该是什么不能只记一个LATE或EARLY要用一个字段存主要状态再加一个字段存异常明细。我的做法是attendance_record表里加了一个status VARCHAR字段存逗号分隔的标记比如“LATE,EARLY”。统计的时候用FIND_IN_SET或者直接在Java里拆分哪种都行但一定要能表达组合状态。4.3 月度统计先聚合后补全不要一条SQL想把什么都查出来统计报表是考勤系统的门面HR每个月看的就是这张表。刚开始我想用一个复杂的SQL把出勤天数、迟到次数、请假时长全查出来结果SQL写了一百多行查询性能还行但可维护性极差——需求一变动SQL就要重写。后来换了个思路先查明细再在内存里聚合。逻辑分三步查出某员工某月所有attendance_record记录。查出该员工该月所有已批准的leave_request记录。遍历每一天结合排班表判定当天状态累加汇总。五百个人一个月的明细也就一万多条放内存里处理完全没有压力而且逻辑清晰、改起来快。月底用定时任务把汇总结果写进attendance_summary表HR查报表直接查表就行不需要实时计算。4.4 导出ExcelEasyExcel处理动态列头的方案导出月度考勤表是HR最喜欢的功能。用EasyExcel基础用法是定义实体类加注解一行代码就能导出。但考勤报表有个麻烦点列头可能是动态的。比如HR选择导出“2025年6月”列头是一号到三十号每个日期一列员工每一行显示当天状态。这种动态列头用注解的方式就没法实现了。我的做法是使用EasyExcel的动态头API传入ListList 作为表头不需要实体类public void exportMonthlyReport(String month, HttpServletResponse response) { ListListString head new ArrayList(); head.add(Arrays.asList(员工姓名, 员工姓名)); head.add(Arrays.asList(部门, 部门)); for (int day 1; day daysInMonth; day) { head.add(Arrays.asList(day 日, day 日)); } head.add(Arrays.asList(迟到次数, 迟到次数)); head.add(Arrays.asList(请假天数, 请假天数)); ListListObject dataList buildData(month); EasyExcel.write(response.getOutputStream()) .head(head) .sheet(考勤汇总) .doWrite(dataList); }注意每个表头的List 里如果有多个元素会生成多级表头。我不想生成多级所以每列只放了一个元素。导出的文件名要注意处理中文用URLEncoder编码否则浏览器下载时会出现乱码文件名。5. Vue前端页面组织、日历视图、审批流的交互设计5.1 Vue项目结构和axios封装目录清爽比什么都重要前端项目用Vue CLI或Vite创建都可以。我推荐按业务模块划分目录不要全部塞在views下面当扁平的页面列表src/ api/ // 接口请求封装按模块拆分 attendance.js leave.js user.js views/ dashboard/ // 个人考勤面板 admin/ // 管理员页面排班、班次、人员管理 approval/ // 审批中心 report/ // 月度报表 components/ // 公共组件 router/ // 路由配置 store/ // Pinia或Vuex状态管理axios封装要做的事情不复杂统一baseURL、请求头带token、响应拦截器里统一处理401跳转登录页、错误信息ElMessage提示。最关键的一点是封装完后所有请求方法都返回Promise页面里统一用async/await别在组件里到处写.then地狱。5.2 考勤日历视图v-calendar组件改造成本项目需要的样式考勤系统的前端页面里最有技术含量的就是日历视图。员工个人页面要把每个日期标记成绿色正常、红色迟到/缺卡、蓝色请假、灰色休息。实现方案有两种用Element Plus的el-calendar自定义date-cell插槽。用独立的v-calendar组件功能更强但需要引入额外依赖。我第一版用的是v-calendar因为它支持月份切换动画、日期范围高亮、事件标记API很完善。给每个日期设置一个自定义属性然后在cell插槽里根据属性渲染不同颜色的小圆点v-calendar v-modelcalendarDate :attributescalendarAttrs template v-slot:day-content{ day, attributes } div classday-cell span{{ day.day }}/span div classstatus-dots span v-forattr in attributes :keyattr.key classdot :style{ background: attr.customData.color } / /div /div /template /v-calendar日历数据来源是后端接口返回的当月状态JSON前端按日期映射成attributes数组。这个方案实测下来页面很流畅五百人规模下没有卡顿。5.3 审批流的前端状态机按钮状态跟着流程走请假/加班/补卡申请的前端页面核心是“不同角色看到不同操作”和“流程状态不同按钮状态不同”。我总结了一个简单的状态机思路申请单状态存后端数据库前端拉到一个字段叫approvalStatus取值是PENDING/APPROVED/REJECTED。员工端列表PENDING的显示“催审批”按钮APPROVED的什么都不显示REJECTED的显示“重新申请”。主管端待办列表只显示PENDING的单子操作按钮是“通过”和“驳回”驳回时必填理由。管理员端可以看到全部单子包括已审批的历史记录便于审计。这个状态机不要写在每个页面里抽成一个utils函数输入是当前用户角色和申请单状态输出是可见的按钮列表。这样改需求时只改一个函数不会出现“列表页改了审批页忘了改”的问题。5.4 动态路由与按钮权限Vue Router判断用户角色前端权限控制分两层。第一层是路由守卫每个人登录后只能跳转到自己角色允许访问的页面。第二层是页面内的按钮权限比如员工看不到“导出报表”按钮主管看不到“排班管理”入口。我用的方案是路由meta里配置roles数组router.beforeEach里判断当前用户角色是否在允许列表内。按钮权限则用自定义指令v-permission指令内部比较用户权限码和后端返回的权限列表没有权限就移除DOM元素// 自定义指令v-permissionattendance:export app.directive(permission, { mounted(el, binding) { const required binding.value; const userPerms useUserStore().permissions; if (!userPerms.includes(required)) { el.parentNode el.parentNode.removeChild(el); } } });这里的permissions列表是登录成功后后端返回的权限码数组。虽然第一版只做了三种角色权限码已经按按钮粒度分配了后面扩展新角色会省很多事。6. 前后端联调与权限控制JWT只是第一步会话管理才是关键6.1 JWT认证流程生成、签发、拦截三步走前后端分离项目的认证方案JWT是主流。流程不复杂用户输入用户名密码后端校验通过后生成一个token返回前端存在localStorage里后续每次请求在Authorization头里带上后端拦截器解析token并设置当前用户上下文。生成token时建议把用户ID、用户名、角色放进去但不要放敏感信息。过期时间设置成8小时前端在axios响应拦截器里捕获401跳转登录页并清除本地token。有一个细节要注意JWT是无状态的服务端无法主动让某个token失效。如果员工离职了管理员需要能强制该账号下线。解决方案有两种一是维护一个token黑名单表二是缩短token有效期并用refresh_token刷新。第一版考勤系统用黑名单表更简单——在sys_user表加一个token_version字段每次登录生成新token时带上tokenVersion拦截器里比较当前用户tokenVersion和token里的是否一致不一致就拒绝。6.2 后端拦截器和前端路由守卫的双保险权限控制不能只靠前端隐藏按钮真正的安全防线在后端。每个接口的Controller方法上都要加权限注解比如PreAuthorize(hasAuthority(attendance:export))。前端隐藏按钮是为了用户体验后端拦截才是安全兜底。我用Spring Security Sa-Token的组合比纯Spring Security要轻量不少学习成本低。Sa-Token的StpUtil.getLoginId()就能拿到当前登录用户ID比从token里手动解析要省事得多。6.3 跨域问题为什么前端请求老是被浏览器拦截开发环境前后端分离跑在两个端口如前端8080后端8081必然遇到跨域。这个问题我在项目里处理了两遍第一次是开发环境的代理第二次是测试环境的Nginx反向代理。开发环境最简单的方案是配置Vite或Vue CLI的proxy代理前端请求统一发到前端端口由开发服务器转发到后端// vite.config.js server: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } }这种方式浏览器看到的所有请求都指向同一个源不存在跨域问题。生产环境用Nginx把/api路径反向代理到后端服务。绝对不要在SpringBoot里配置全局CORS允许所有来源那等于把API裸奔在公网上。7. 踩坑实录这几个问题让我加了好几天班7.1 SpringBoot版本太高引发的依赖冲突项目一开始我图新鲜用了SpringBoot 3.0.2 JDK17结果刚起步就踩坑。MyBatis-Plus当时的最新版勉强支持但一些老牌Excel导出库、代码生成器全部不兼容javax报ClassNotFoundException。而且Spring Security 6的配置方式和5完全不一样网上大部分教程都是基于5.x的照抄根本跑不起来。排查链路是这样的先看到启动报错NoClassDefFoundError定位到是security包里的javax.servlet.Filter找不到。用mvn dependency:tree查依赖发现SpringBoot 3.0.2内部引用的jakarta.servlet-api而MyBatis-Plus的某个插件还依赖javax.servlet-api两者互相冲突。最后决定不折腾了降级到SpringBoot 2.7.9 JDK8所有问题消失连配置都不用改。给新手的建议做企业管理系统SpringBoot老老实实用2.7.x别追新。7.2 Vue打包后布局异常路由mode和静态资源路径的坑本项目第一个上线版本打包部署后页面白屏控制台报一堆资源404。排查发现是publicPath的问题。Vue CLI默认publicPath是绝对路径/部署到服务器二级目录后就找不到了。把它改成相对路径./后资源加载正常。但接着又出现第二个问题路由刷新时404。因为用了Vue Router的history模式刷新页面时Nginx找不到对应的后端路由返回404。排查后确认需要配置Nginx的try_files指令让所有前端路由都回退到index.htmllocation / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }之后刷新就正常了。这两个问题叠加在一起排查花了大半天但原因都很简单一个是路径配置一个是服务器回退配置。7.3 考勤系统最隐蔽的坑时区问题考勤系统对时间极其敏感时区不对全盘皆输。我测试时发现了一个诡异现象员工明明17:00下班打卡数据库里存的却是09:00。排查过程先查前端发现前端调试时显示的LocalTime是正常的再查后端接收参数发现传给后端的字符串也是17:00最后查数据库连接串发现JDBC URL里少了一个关键参数。MySQL驱动连接串必须显式指定serverTimezone否则会使用JVM默认时区我服务器上配置的是UTC插入时相差8小时jdbc:mysql://localhost:3306/attendance?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiJava后端涉及时间的一律用LocalDateTime和LocalTime不要用Date和Timestamp。Java 8新的时间API不包含时区信息配合MySQL DATETIME类型不会出现Date类那种时区转换的坑。前端用dayjs格式化时间字符串传给后端后端用LocalTime.parse接收两端统一用HH:mm:ss格式问题基本就避免了。7.4 EasyExcel动态列导出的两个坑动态列头导出时遇到两个问题。第一个是每个单元格的样式错乱——动态列的表头里如果合并了单元格必须保证表头每行的长度一致。我在表头里混用了单列和多列导出后Excel提示文件损坏。解决办法是让所有列的表头List长度一致都只放一个元素。第二个是导出大数据量时内存溢出。五百人一个月的明细不算多但如果你把表头和数据一次性全加载进内存再一次性写入内存占用还是很可观的。EasyExcel提供了分页查询配合分批写入的能力每查1000条就write一次最后调用finish方法。我实际测试下来内存占用从峰值300MB降到了80MB以内效果非常明显。8. 部署上线与运维细节能用Docker就用Docker备份别偷懒8.1 前后端分离部署Nginx JAR包 MySQL的经典组合生产环境我用的是一台2核4G的云服务器部署结构如下前端npm run build生成dist目录用Nginx托管静态文件。后端SpringBoot打成JAR包用systemd配置开机自启。数据库MySQL 8.0独立部署每天凌晨自动备份。Redis第一版没用到第二版考虑做缓存和token黑名单时会加。后端启动命令建议加上JVM参数调优nohup java -Xms256m -Xmx512m -jar attendance-system.jar --spring.profiles.activeprod app.log 21 512MB堆内存对这个应用完全够用。如果服务器内存吃紧-Xms和-Xmx可以都配成200MB跑起来也没问题。8.2 数据库查询优化除了索引还要关注分页深翻页系统上线一个月后管理员反馈“考勤记录查询页面变慢了”。排查发现是管理员习惯用默认排序翻到最后一页MySQL深翻页offset到几十万行时性能急剧下降。优化方案是改成“查询条件带时间范围的游标分页”或者用延迟关联子查询先查出主键ID再回表SELECT * FROM attendance_record WHERE id IN ( SELECT id FROM attendance_record WHERE user_id ? AND work_date BETWEEN ? AND ? ORDER BY work_date DESC LIMIT 500, 100 );由于考勤查询几乎都会带员工ID和时间范围这两个条件联合索引(user_id, work_date)命中后深翻页的性能问题基本就没有了。另外建议管理员查询时默认限制最大查询范围比如一次最多查三个月减少一次捞全年的情况。8.3 月底自动跑批定时任务生成汇总报表月度汇总用SpringBoot自带的Scheduled注解就能实现。配置一个cron表达式每月1号凌晨1点执行生成上个月的汇总Scheduled(cron 0 0 1 1 * ?) // 每月1号凌晨1点 public void generateMonthlySummary() { String lastMonth LocalDate.now().minusMonths(1).toString().substring(0, 7); // 遍历所有员工计算考勤数据写入attendance_summary }跑批逻辑要考虑幂等性如果因为服务器停机导致跑批失败下个月1号补跑时要先删除上个月的汇总记录再重新生成否则会出现重复数据。简单做法是在insert之前先执行delete where month 目标月份。备份方面每天凌晨用mysqldump备份全库保留最近7天的备份文件加上一个异地备份用另一台服务器的定时任务从那边拉取。考勤数据涉及员工工资核算丢数据的后果很严重备份不能省。写在最后的个人经验这套系统从需求调研到上线用了大约八周其中一半时间花在需求确认和数据库设计上真正写代码的时间并不多。回头看最值得的投资是花了整整两天把排班模型和状态判定规则定义清楚后面所有模块的开发都是在这个基础上顺水推舟。如果再让我重做一次我会提前把“消息通知”机制纳入第一版设计。考勤系统天然有通知需求——审批通过要通知员工员工提交申请要通知主管忘打卡了系统应该提醒。第一版我全部用站内信实现员工根本不看后来加了邮件通知效果才好一些。如果你们的公司用企业微信或钉钉第二版优先接入它们的消息推送员工的感知度会高很多。另一个经验是考勤系统最容易翻车的不是技术而是考勤规则。建议上线前让HR部门把所有规则写成文档你对照文档实现再让HR参与测试验收。一个“迟到宽限5分钟”的规则看起来简单但它和“9:05打卡算不算迟到”“9:35打卡算不算缺卡”是关联的规则之间一定要做全组合测试。最后分享一个小技巧开发阶段把打卡接口的时间参数做成可配置的mock模式前端可以传入指定时间这样测试迟到、早退、缺卡这些场景时不用真的等到对应时间点去点按钮。这个功能在演示给领导看的时候特别好用。
返回列表