工厂考勤系统避坑指南:3个致命Bug让你少加班
工厂考勤系统避坑指南:3个致命Bug让你少加班 刚接手工厂考勤模块,控制台全是红字,StackTrace 长得像天书,连哪一行代码报的错都找不到。别慌,这种“报错一堆看不懂 StackTrace”的情况,在制造业信息化改造中太常见了。今天这份避坑指南,就是把你从泥潭里拽出来的绳索。 我们不做那种“大而全”的理论推导,直接上代码。这个项目基于 Java Spring Boot + MyBatis Plus,数据库用 MySQL,前端用 Vue3。目标很明确:实现员工打卡、异常标记、月度统计,且能扛住早高峰 500 人同时打卡的并发压力。 项目目标与核心难点拆解 很多新手一上来就写代码,结果写到一半发现逻辑不通。我们先定目标。工厂考勤和互联网公司不同,核心痛点不在“功能多”,而在数据一致性和边缘场景处理。并发安全:早 8 点整,车间大门口的闸机同时接收 500 个请求,如果数据库行锁没处理好,要么死锁,要么数据丢失。 异常判定逻辑复杂:迟到、早退、旷工、请假、加班,这些状态不是简单的“时间比较”,还涉及跨天、夜班、轮班制。 数据追溯:一旦算错工资,HR 需要能查出具体的打卡记录、审批流程和计算公式,这就对日志和审计表提出了要求。我们的架构选型坚持“够用就好”。不用微服务,单体应用足够支撑千人规模工厂。引入 Redis 做缓存,不是为了解决高并发读取,而是为了防重提交和状态预判断。 目录结构:扁平化优于深层嵌套 好的目录结构是代码可维护性的第一道防线。我强烈建议采用分层架构,但层级不要超过 3 层。以下是核心模块的目录树,注意看 service 包下的 impl 和 strategy 的区别,这是解决复杂业务逻辑的关键。 src/main/java/com/factory/attendance/ ├── controller │ ├── PunchController.java # 打卡接口 │ └── ReportController.java # 报表接口 ├── service │ ├── AttendanceService.java # 核心服务接口 │ ├── impl │ │ └── AttendanceServiceImpl.java # 核心逻辑实现 │ └── strategy # 策略模式处理不同班次逻辑 │ ├── DayShiftStrategy.java │ └── NightShiftStrategy.java ├── mapper │ └── EmployeePunchMapper.java ├── model │ ├── entity │ │ └── EmployeePunch.java # 打卡记录实体 │ ├── dto │ │ └── PunchResultDTO.java # 返回给前端的对象 │ └── vo │ └── MonthlyReportVO.java # 月度统计视图对象 └── common├── exception│ └── AttendanceException.java # 自定义异常└── util└── TimeCalculator.java # 时间计算工具类避坑点:不要把所有时间计算逻辑都写在 Service 里。工厂的班次规则经常变(比如某个月搞“两班倒”),如果逻辑硬编码在 Service 中,改一次需求就要发一次版。使用策略模式,将“日班”、“夜班”、“大小周”的计算逻辑抽象成独立类,新增班次只需新增一个实现类,符合开闭原则。 核心代码实现:并发与时间计算 这是最硬核的部分。我选取了两个最容易出 Bug 的场景:并发打卡防重、跨天夜班时间计算。 1. 并发打卡:Redis 分布式锁 + 数据库唯一索引 很多初学者喜欢用 SELECT FOR UPDATE 或者 synchronized,在集群环境下都是灾难。正确的做法是“双保险”:Redis 做前置过滤,数据库唯一索引做最终兜底。 @Service public class AttendanceServiceImpl implements AttendanceService {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate EmployeePunchMapper punchMapper;@Overridepublic PunchResultDTO punchCard(String employeeId, Long timestamp) {// 1. 生成唯一键:员工ID + 日期 + 打卡类型(0:上班, 1:下班)// 假设前端传入 timestamp,我们先判断是上班还是下班boolean isMorning = isMorningPunch(timestamp);String lockKey = att:lock: + employeeId + : + LocalDate.now() + : + (isMorning ? 0 : 1);// 2. Redis 设置过期时间锁,防止死锁Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 5, TimeUnit.SECONDS);if (!locked) {throw new AttendanceException(请勿重复提交,正在处理中);}try {// 3. 业务逻辑:查询该员工今日是否已有记录EmployeePunch existPunch = punchMapper.selectByEmployeeAndDate(employeeId, LocalDate.now());if (existPunch != null) {// 4. 如果已有记录,判断是否允许补卡或覆盖(根据业务规则)// 这里简化处理:直接返回已有状态,避免重复写入return buildResult(existPunch, Duplicate);}// 5. 构建新记录EmployeePunch newPunch = new EmployeePunch();newPunch.setEmployeeId(employeeId);newPunch.setPunchTime(new Date(timestamp));newPunch.setType(isMorning ? 0 : 1);newPunch.setStatus(calcStatus(newPunch)); // 计算迟到/正常// 6. 插入数据库// 注意:数据库表中 employee_id, punch_date, type 必须建立联合唯一索引punchMapper.insert(newPunch);return buildResult(newPunch, Success);} catch (DuplicateKeyException e) {// 7. 捕获数据库唯一索引冲突,说明 Redis 锁失效或并发极高// 此时不能抛错给用户,应查询数据库返回真实状态EmployeePunch dbPunch = punchMapper.selectByEmployeeAndDate(employeeId, LocalDate.now());return buildResult(dbPunch, Conflict);} finally {// 8. 无论成功失败,必须释放锁redisTemplate.delete(lockKey);}} }逐行解析:L12-L15:锁的 Key 设计至关重要。必须包含日期和类型,否则员工上午打了卡,下午下班打卡会被锁住。 L28-L30:setIfAbsent 是原子操作,这是分布式锁的基础。 L42-L45:calcStatus 是纯函数,不依赖外部状态,方便单元测试。 L47-L49:这是最关键的避坑点。即使有 Redis 锁,由于网络抖动、Redis 故障或锁过期,仍可能出现并发写入。数据库的 DuplicateKeyException 是最后的防线。很多开发者在这里直接 throw e,导致用户看到 500 错误,体验极差。正确做法是捕获异常,查询数据库真实状态返回给前端。2. 跨天夜班时间计算 工厂夜班经常从晚上 22:00 干到第二天 06:00。如果简单用 endTime - startTime,当 endTime 的日期小于 startTime 时,结果为负数。 public class TimeCalculator {/*** 计算工作时长,支持跨天* @param startTime 开始时间* @param endTime 结束时间* @return 毫秒数*/public static long calcDuration(LocalDateTime startTime, LocalDateTime endTime) {if (startTime == null || endTime == null) {throw new IllegalArgumentException(时间不能为空);}// 如果结束时间小于开始时间,说明跨天了if (endTime.isBefore(startTime)) {// 将开始时间减去 1 天,使时间差为正// 注意:这里不能直接 endTime.plusDays(1),因为如果是跨月、跨年,逻辑可能出错// 最稳妥的方式是:判断是否跨天,如果跨天,将 startTime 的日期部分减 1startTime = startTime.minusDays(1);}return Duration.between(startTime, endTime).toMillis();} }避坑点:不要用 new Date().getTime() 做差值计算,Java 8 之后的 LocalDateTime 和 Duration 是线程安全的,且语义更清晰。另外,不要假设所有夜班都跨天。有些工厂夜班是 20:00-04:00,有些是 22:00-06:00,必须根据配置表动态判断,或者统一规定“只要 endTime 的 hour 小于 startTime 的 hour 即视为跨天”。 运行与测试:如何验证你的避坑是否有效 代码写完只是完成了一半,测试才是灵魂。对于考勤系统,边界条件测试比功能测试更重要。 1. 单元测试:使用 JUnit 5 + Mockito 针对 TimeCalculator 和 AttendanceServiceImpl 的核心逻辑编写测试。 @Test public void testCrossDayCalculation() {LocalDateTime start = LocalDateTime.of(2023, 10, 1, 22, 0, 0);LocalDateTime end = LocalDateTime.of(2023, 10, 2, 6, 0, 0);long duration = TimeCalculator.calcDuration(start, end);long expectedHours = 8 * 60 * 60 * 1000; // 8小时assertEquals(expectedHours, duration); }@Test public void testDuplicatePunch() {// Mock Mapper 和 Rediswhen(punchMapper.selectByEmployeeAndDate(any(), any())).thenReturn(null);when(punchMapper.insert(any())).thenThrow(new DuplicateKeyException(Dup));when(punchMapper.selectByEmployeeAndDate(any(), any())).thenReturn(mockPunch);// 执行PunchResultDTO result = service.punchCard(emp001, System.currentTimeMillis());// 验证:不应该抛异常,而是返回数据库中的记录assertNotNull(result);assertEquals(Conflict, result.getMessage()); }2. 压力测试:模拟早高峰 使用 JMeter 或 Locust 模拟 500 个线程,在同一秒内调用 punchCard 接口。 观察指标:数据库死锁日志:检查 MySQL error log,是否有 Deadlock found。 Redis 内存:锁的 Key 是否及时清除。如果 finally 块中的 delete 失败,锁会保留 5 秒,这 5 秒内该员工无法打卡,这是可接受的降级。 响应时间:P99 延迟应控制在 200ms 以内。真实案例:我在某项目中发现,当 500 并发时,punchMapper.insert 成为瓶颈。原因是 MyBatis Plus 默认的批量插入策略未启用。解决方案:开启 rewriteBatchedStatements=true JDBC 参数。 或者在 Service 层做内存缓冲,每 10 条 flush 一次。但对于考勤这种强一致性要求,单条插入+唯一索引更可靠,牺牲一点吞吐量换取数据绝对准确。优化扩展:从“能用”到“好用” 系统上线后,HR 最常抱怨的是“统计报表慢”和“补卡流程繁琐”。 1. 报表性能优化 月度统计涉及 GROUP BY 和大量聚合计算。如果直接查主表,百万级数据量下耗时可能超过 10 秒。 方案:预计算表:每日凌晨 0 点,通过定时任务(Quartz/Xxl-Job)计算前一天的考勤结果,存入 daily_attendance_summary 表。 查询优化:前端查月度报表时,只查 daily_attendance_summary,再对日期进行聚合。 索引设计:employee_id, stat_date 建立联合索引。2. 补卡与异常申诉 工厂员工经常忘记打卡,或闸机故障。 流程设计:员工发起补卡申请,填写原因。 班组长审批。 审批通过后,不要直接修改原始打卡记录!而是新增一条 type=2 (补卡) 的记录,并标记原始记录为 void (作废)。 计算工时和工资时,优先读取有效记录。为什么不能修改原记录?审计合规:劳动法要求保留原始证据。 数据追溯:如果算错工资,能查到是谁、什么时候、因为什么原因修改了数据。小结与互动 工厂考勤系统看似简单,实则充满了并发陷阱、时间边界和业务流程的复杂耦合。 回顾一下今天的避坑指南核心:并发防重:Redis 锁 + DB 唯一索引双保险,捕获 DuplicateKeyException 而非抛错。 时间计算:使用 LocalDateTime,专门处理跨天逻辑,策略模式解耦班次规则。 数据一致性:补卡不覆盖,用“新增+作废”模式保留审计痕迹。 性能优化:预计算汇总表,避免实时复杂聚合。代码不是越多越好,而是越“稳”越好。在制造业场景,系统的稳定性远比功能炫酷重要。 互动时间: 在你过往的项目中,你是倾向于直接修改数据库记录来简化补卡逻辑,还是像我这样保留原始记录并新增补卡记录?虽然后者开发成本高,但数据更安全。你更常用哪种写法?评论区交流,看看大家是怎么平衡开发效率与数据安全的。

相关新闻

3天搞定企业公示信息查询系统避坑指南

3天搞定企业公示信息查询系统避坑指南

3天搞定企业公示信息查询系统避坑指南 你是不是也经历过这种绝望:教程刷了几十遍,LeetCode题也刷了几道,真让你独立从零手搓一个项目,脑子一片空白,连数据库表怎么建都犹豫不决?别慌,这正是绝大多数初级开发者的通病。今天这篇避坑指南,不灌…

2026/9/22 9:57:04 阅读更多 →
3个实战项目搞定vipl选型与避坑

3个实战项目搞定vipl选型与避坑

3个实战项目搞定vipl选型与避坑 看了一堆教程还是不会写项目?别急,这怪不了你。很多人卡在“vipl”这个概念上,觉得它高深莫测,其实只要拆解成 实战项目…

2026/9/22 9:57:04 阅读更多 →
小俊面试突击:搞定配置难题,从入门到精通

小俊面试突击:搞定配置难题,从入门到精通

小俊面试突击:搞定配置难题,从入门到精通 刚接手新项目,配置环境就卡半天?这是很多开发者,包括我们团队里的“小俊”,都遇到过的噩梦。依赖冲突、版本不对、环境变量丢失,光看报错信息就能让人头秃。…

2026/9/22 9:56:04 阅读更多 →

最新新闻

电脑软件性能优化避坑指南:3个核心技巧告别卡顿

电脑软件性能优化避坑指南:3个核心技巧告别卡顿

电脑软件性能优化避坑指南:3个核心技巧告别卡顿 刚跑完一个复杂的批处理任务,屏幕突然弹出一串红色的 StackTrace,满屏的 NullPointer 和 OutOfMemory…

2026/9/22 10:42:27 阅读更多 →
使命召唤16代码跑不通?3个性能优化坑让你效率翻倍

使命召唤16代码跑不通?3个性能优化坑让你效率翻倍

使命召唤16代码跑不通?3个性能优化坑让你效率翻倍 刚把网上抄的《使命召唤16》高并发战斗逻辑代码扔进项目,结果一运行就报错,或者跑起来卡顿得像个幻灯片。别急,这种情况我当年踩坑时比你还慌。别盯着那个红色的 TypeError 或…

2026/9/22 10:42:27 阅读更多 →
美狐踩坑实录:3个版本升级API陷阱与高频面试题解析

美狐踩坑实录:3个版本升级API陷阱与高频面试题解析

美狐踩坑实录:3个版本升级API陷阱与高频面试题解析 刚做完一个老项目重构,打开代码库那一刻,心里就咯噔一下。那些曾经熟记于心的 meihu.fetch() 和 fh666.request() 调用,在升级美狐框架至 3.2…

2026/9/22 10:42:27 阅读更多 →
mp4格式转换器免费下载背后3个坑与最佳实践

mp4格式转换器免费下载背后3个坑与最佳实践

mp4格式转换器免费下载背后3个坑与最佳实践 别再纠结那个“mp4格式转换器免费下载”的按钮了。我见过太多人下载了一堆带广告的软件,结果视频转出来音画不同步,或者文件直接损坏。看了一堆教程还是不会写项目,核心不是你手慢,而是你一直在用别人的…

2026/9/22 10:42:27 阅读更多 →
2026最新crossfire.exe进程卡死?3个底层坑位与修复方案

2026最新crossfire.exe进程卡死?3个底层坑位与修复方案

2026最新crossfire.exe进程卡死?3个底层坑位与修复方案 学会语法却不知怎么搭项目,这是很多开发者从教程走向实战时最头疼的问题。特别是当你的构建脚本或启动程序涉及 crossfire.exe…

2026/9/22 10:42:27 阅读更多 →
理优一对一性能调优:从入门到精通,面试不再露怯

理优一对一性能调优:从入门到精通,面试不再露怯

理优一对一性能调优:从入门到精通,面试不再露怯 面试被问底层原理时,你还能流畅答上来吗?很多开发者在 理优一对一 场景下,往往只盯着业务逻辑,忽略了性能瓶颈,导致系统一上量就卡顿。想从 入门到精通…

2026/9/22 10:41:27 阅读更多 →

日新闻

3台商务办公笔记本实测:手写实现环境配置,告别卡半天

3台商务办公笔记本实测:手写实现环境配置,告别卡半天

3台商务办公笔记本实测:手写实现环境配置,告别卡半天 配置环境就卡半天?别怪机器慢,多半是你没选对工具链。在Java、Go或Python的项目现场, 手写实现…

2026/9/22 0:00:41 阅读更多 →
剑帝加点速查手册:3分钟搞懂核心逻辑

剑帝加点速查手册:3分钟搞懂核心逻辑

剑帝加点速查手册:3分钟搞懂核心逻辑 面试被问原理答不上来,是不是常态?别慌。很多开发者对着 GitHub 开源仓库里的代码发呆,看似简单实则暗藏玄机。今天这份【剑帝加点】速查手册,直接带你拆解核心实现,把面试必考的原理讲透。…

2026/9/22 0:00:41 阅读更多 →
手写实现图片压缩网站核心:搞定WebP转换与质量调优

手写实现图片压缩网站核心:搞定WebP转换与质量调优

手写实现图片压缩网站核心:搞定WebP转换与质量调优 复制来的代码跑不通不知道怎么调?别慌,这种“复制粘贴地狱”在开发圈太常见了。尤其是做 图片压缩网站…

2026/9/22 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/22 4:32:41 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/22 4:38:57 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/22 8:51:04 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/21 15:36:51 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/21 15:36:51 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/22 2:43:42 阅读更多 →