资讯动态

Java工时计算:精准处理节假日与调休的实战指南

发布时间:2026/10/21 18:42:33 来源:尧图企业网站定制
简介这是一份面向Java开发者的实用工具类代码资源聚焦于「计算工作时间时排除节假日与双休日」这一常见业务需求适用于考勤统计、工时核算、项目排期等场景属于中初级开发者即可上手的技能层级。资源包内共1个PDF文件大小约74KB内容以代码讲解与实现思路为主围绕CalculateHours类展开涵盖上下班时间配置、每日与每周工时换算、完整星期数累加、工作日天数统计以及同天与跨天时段的差值处理等关键逻辑并给出节假日与双休日的排除方案。目前已有4558人学习下载说明该思路在实际开发中具有较高的参考价值。读者可从中获取可直接借鉴的工时计算实现代码、时间预处理与边界情况处理技巧以及将业务规则落地为Java方法的完整思路便于快速应用到考勤或项目管理系统之中。1. 工时计算为什么总在节假日上翻车做过考勤、OA、工单 SLA 或者项目排期的同行大概率都写过一段“算两个时间点之间有多少个工作日”的代码。第一版通常很朴素总时长减去周末天数乘个每天 8 小时收工。上线之后问题就来了——调休上班的周六被当成休息日扣掉了法定节假日没扣跨年的时候算法直接算错一天用户拿着考勤表来对账你只能一行行手算复现。这类需求看着简单本质是「日历数据 区间遍历 时段裁剪」三件事叠在一起任何一件偷懒都会在边界上翻车。这篇讲的就是用 Java 把「计算工作时间、除去节假日以及双休日」这件事做扎实怎么组织节假日数据、怎么处理调休、怎么把一天切成上午下午两段、跨天跨月跨年怎么算、半天请假怎么扣。适合正在写考勤/排期/SLA 的开发者也适合已经写了一版但被边界 case 反复打脸、想重构的人。下面按「数据怎么来 → 核心算法怎么写 → 坑在哪 → 怎么验证」的顺序推。2. 先把日历数据这层地基打对很多人一上来就写循环结果写到一半发现没有可靠的节假日数据源只能硬编码几个日期第二年就失效。工时计算的第一性问题不是算法是数据。这一章把数据模型和加载方式定下来后面所有计算都建立在这上面。2.1 为什么不能只用 WeekFields 判断周末java.time里的DayOfWeek和WeekFields能告诉你某天是周六还是周日但它不知道「这个周六是不是调休上班」。国内的工作日安排里法定节假日会通过调休把某些周末变成工作日也会把某些工作日变成休息日。只按DayOfWeek.SATURDAY/SUNDAY判断遇到调休必然算错。常见做法是维护一张「特殊日期表」只记录与默认规则不一致的日子而不是把全年 365 天都列出来。默认规则是「周一到周五上班周六周日休息」特殊表里只放两类法定节假日本该上班却放假和调休上班日本该休息却上班。这样数据量小人工维护成本低出错面也窄。数据来源上正规做法是从权威渠道获取每年发布的工作日安排落到自己的库里或配置文件里。不要依赖第三方接口实时查询接口挂了你的考勤就算不出来。落地成一张表最稳字段类型说明dateLocalDate日期主键day_typeint0 工作日1 休息日remarkvarchar备注如「春节调休」day_type用 0/1 而不是布尔是为了以后扩展半天班、特殊班次留余地。查询时按日期范围批量捞出来放进MapLocalDate, Integer避免在循环里逐天查库。2.2 用 Map 缓存特殊日期并做兜底加载逻辑要处理「查不到」的情况。如果某天既不在特殊表里也不是周末那它就是普通工作日如果是周末又不在特殊表里就是普通休息日。这个兜底规则必须写死否则数据缺失时算法行为不可预测。import java.time.DayOfWeek; import java.time.LocalDate; import java.util.HashMap; import java.util.Map; public class WorkCalendar { // key: 日期, value: 0 工作日 / 1 休息日 private final MapLocalDate, Integer specialDays new HashMap(); public void loadSpecialDays(MapLocalDate, Integer data) { specialDays.clear(); specialDays.putAll(data); } /** * 判断某天是否为工作日 * 优先查特殊表查不到再按周末规则兜底 */ public boolean isWorkday(LocalDate date) { Integer type specialDays.get(date); if (type ! null) { return type 0; } DayOfWeek dow date.getDayOfWeek(); return dow ! DayOfWeek.SATURDAY dow ! DayOfWeek.SUNDAY; } }这段代码的关键点是判断顺序先查特殊表命中就直接返回没命中才走周末兜底。顺序反了的话调休上班的周六会被周末规则判成休息日直接算错。specialDays用HashMap而不是TreeMap因为这里只做点查不需要范围遍历如果后面要做区间统计再换成TreeMap用subMap批量取。参数上唯一需要留意的是loadSpecialDays的入参。实际项目里它来自数据库或配置中心建议按年加载一次把当年数据全灌进来而不是每次计算都查一遍。跨年计算时要把两年的数据都加载否则 12 月 31 日到 1 月 1 日这段会漏掉特殊日期。3. 核心算法区间遍历与每日工时裁剪数据层稳了接下来是算法主体。这里要解决两个问题一是怎么遍历日期区间二是每天到底算几个小时。很多人只做了第一层把每天固定按 8 小时算结果遇到半天请假、上午入职、下午离职就全错。3.1 按天遍历并累加有效工时最直观也最不容易出错的思路是「逐天遍历每天单独算有效工时最后求和」。相比用总天数乘系数逐天算的好处是每个边界都能单独处理调试时能打印出每一天的判定结果出问题一眼看出是哪天错了。import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; public class WorkTimeCalculator { // 上午 09:00-12:00下午 13:30-18:00 private static final LocalTime AM_START LocalTime.of(9, 0); private static final LocalTime AM_END LocalTime.of(12, 0); private static final LocalTime PM_START LocalTime.of(13, 30); private static final LocalTime PM_END LocalTime.of(18, 0); private final WorkCalendar calendar; public WorkTimeCalculator(WorkCalendar calendar) { this.calendar calendar; } /** * 计算 [start, end] 区间内的工作时长分钟 * 区间为左闭右开end 不参与计算 */ public long calcMinutes(LocalDateTime start, LocalDateTime end) { if (!start.isBefore(end)) { return 0; } long total 0; LocalDate curDate start.toLocalDate(); LocalDate endDate end.toLocalDate(); while (!curDate.isAfter(endDate)) { if (calendar.isWorkday(curDate)) { // 当天有效区间的起点 LocalDateTime dayStart curDate.atStartOfDay(); LocalDateTime dayEnd curDate.plusDays(1).atStartOfDay(); // 与传入区间求交集 LocalDateTime from start.isAfter(dayStart) ? start : dayStart; LocalDateTime to end.isBefore(dayEnd) ? end : dayEnd; total workMinutesOfDay(from, to); } curDate curDate.plusDays(1); } return total; } /** * 计算某一天内 [from, to] 落在工作时段里的分钟数 */ private long workMinutesOfDay(LocalDateTime from, LocalDateTime to) { long minutes 0; minutes overlapMinutes(from, to, AM_START, AM_END); minutes overlapMinutes(from, to, PM_START, PM_END); return minutes; } /** * 求 [from, to] 与 [segStart, segEnd] 的重叠分钟数 */ private long overlapMinutes(LocalDateTime from, LocalDateTime to, LocalTime segStart, LocalTime segEnd) { LocalDate date from.toLocalDate(); LocalDateTime s date.atTime(segStart); LocalDateTime e date.atTime(segEnd); LocalDateTime realStart from.isAfter(s) ? from : s; LocalDateTime realEnd to.isBefore(e) ? to : e; if (!realStart.isBefore(realEnd)) { return 0; } return Duration.between(realStart, realEnd).toMinutes(); } }逻辑分三层。第一层calcMinutes负责按天推进把跨天区间拆成一天一段。第二层workMinutesOfDay把一天拆成上午、下午两个工作时段。第三层overlapMinutes做区间求交这是整个算法的核心——不管传入的from/to落在哪只要和某个工作时段有重叠就取重叠部分。参数上有几个点必须说清楚。AM_START/AM_END/PM_START/PM_END是硬编码的实际项目里应该做成配置因为不同公司、不同班次的上下班时间不一样。区间用左闭右开[start, end)这样相邻两段拼接时不会重复计算边界那一分钟。Duration.between(...).toMinutes()返回的是整数分钟如果业务需要精确到秒换成getSeconds()再除 60但考勤场景一般分钟级就够。3.2 半天请假和跨天场景怎么处理上面的算法天然支持半天场景。比如某人上午请假只算下午那传入的start就是当天 13:30overlapMinutes在上午段求交时realStart会大于realEnd返回 0下午段正常累加。不需要为半天写特殊分支这是区间求交带来的好处。跨天场景也一样。晚上加班到次日凌晨calcMinutes会先算当天 18:00 之前的部分再进入第二天第二天如果是工作日从 09:00 开始算到实际结束时间。如果第二天是休息日isWorkday返回 false直接跳过加班时长只算到当天 18:00。这里有个业务分歧点加班到休息日算不算工时取决于你们公司的规则算法层面只需要控制isWorkday的判定不用改遍历逻辑。跨月跨年不需要特殊处理因为LocalDate.plusDays(1)会自动进位isWorkday查的是具体日期只要特殊日期表加载了对应年份的数据就没问题。这也是为什么前面强调要按年加载、跨年时加载两年。3.3 用 TreeMap 优化大区间统计逐天遍历在区间跨度大时会有性能问题。算一个人一年的工时循环 365 次每次两次求交问题不大但如果要做全公司几千人、每人几年的批量统计逐天遍历就偏慢。这时候可以把特殊日期表换成TreeMap用subMap一次性取出区间内的所有特殊日期再结合总天数做批量计算。import java.time.temporal.ChronoUnit; import java.util.TreeMap; public long countWorkdaysFast(LocalDate start, LocalDate end) { long totalDays ChronoUnit.DAYS.between(start, end); // 先按周末规则粗算 long workdays 0; // 这里仍需要遍历但可以只遍历特殊日期做修正 // 思路总天数 - 周末天数 调休上班天数 - 法定节假日天数 return workdays; }这个优化思路是「先按默认规则算再用特殊日期做增减修正」避免逐天判断。但修正逻辑要小心调休上班的周六本来被周末规则扣掉了要加回来法定节假日本来被算成工作日要减掉。特殊日期表里day_type0的是调休上班day_type1的是法定节假日分别统计数量做加减。这个版本适合批量统计单次精确计算还是用逐天遍历更直观、更不容易错。4. 避坑与排查那些让工时算错的细节算法写完了不代表就对了。下面这几条是我在实际项目里踩过的每条都按「现象 → 原因 → 解决」说清楚遇到对不上账的时候可以照着排查。4.1 跨年计算少一天现象算 12 月 30 日到次年 1 月 3 日的工时结果比手算少一天。原因特殊日期表只加载了当年数据次年的 1 月 1 日没查到走了周末兜底但 1 月 1 日可能是周三兜底判成工作日反而多算如果 1 月 1 日是调休又会少算。解决加载数据时按区间年份加载start.getYear()到end.getYear()之间的每一年都要加载不要只加载起始年。4.2 边界分钟被重复计算现象上午 12:00 结束、下午 13:30 开始算出来比实际多一分钟或少一分钟。原因区间开闭没统一。如果上午段用[9:00, 12:00]闭区间下午段用[13:30, 18:00]中间不重叠没问题但如果有人把结束时间设成 12:00 又用闭区间和下一段的起点判断就会打架。解决全篇统一左闭右开overlapMinutes里用realStart.isBefore(realEnd)判断相等时返回 0边界那一分钟只归属一段。4.3 调休上班日被当成休息日现象某个周六调休上班工时算出来是 0。原因isWorkday里先判断了周末再查特殊表或者特殊表里这条数据day_type存成了 1。解决确认判断顺序是先查特殊表再走周末兜底同时检查数据导入时调休日的day_type是不是 0。建议在isWorkday里加一行日志把命中的特殊日期和判定结果打出来排查时直接看日志。4.4 时区导致日期偏移现象服务器在 UTC 时区用户在东八区晚上 8 点之后提交的工时算到了第二天。原因LocalDateTime不带时区如果从Instant或Date转换时没指定时区会按服务器默认时区解释。解决入口处统一用ZonedDateTime转成业务时区的LocalDateTime转换时显式指定ZoneId不要依赖ZoneId.systemDefault()因为服务器时区可能和业务时区不一致。4.5 半天请假的起止时间传反现象上午请假的人传入start13:30, end18:00算出来是 0。原因调用方把请假区间当成了工作区间传进来而算法期望的是「实际工作区间」。解决在接口层明确约定入参语义是「工作区间」还是「请假区间」两者是互补关系。如果传的是请假区间需要先做一次取反或者干脆提供两个方法calcWorkMinutes和calcLeaveMinutes避免调用方混淆。5. 用测试用例把边界钉死算法类代码最怕的是「看起来对」。工时计算涉及日期、时段、特殊规则三层组合光靠人工点几下测不出问题。我的习惯是先把边界用例列出来写成单元测试跑绿了再上线。下面这张表是我一般会覆盖的场景可以直接抄。场景起始时间结束时间预期同一天正常工作时段周一 10:00周一 15:004.5 小时跨午休周一 11:00周一 14:002.5 小时跨天周一 17:00周二 10:002.5 小时跨周末周五 17:00下周一 10:002.5 小时含法定节假日节前 17:00节后 10:002.5 小时含调休上班周六周五 17:00周六 10:002.5 小时起止相同周一 10:00周一 10:000起止颠倒周一 15:00周一 10:000写测试时有个技巧不要只断言总分钟数把每天的明细也断言上。比如跨天场景断言「周一贡献 60 分钟周二贡献 90 分钟」这样一旦算错测试失败信息能直接告诉你是哪天错了而不是只告诉你总数不对。实现上可以把calcMinutes拆一个返回每日明细的版本测试用明细版生产用汇总版。import org.junit.jupiter.api.Test; import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.assertEquals; public class WorkTimeCalculatorTest { Test void crossLunchBreak() { WorkCalendar calendar new WorkCalendar(); WorkTimeCalculator calc new WorkTimeCalculator(calendar); LocalDateTime start LocalDateTime.of(2025, 3, 3, 11, 0); // 周一 LocalDateTime end LocalDateTime.of(2025, 3, 3, 14, 0); // 11:00-12:00 计 60 分钟13:30-14:00 计 30 分钟 assertEquals(90, calc.calcMinutes(start, end)); } }这段测试的关键是注释里写清楚每一段贡献多少分钟后面有人改算法时一看注释就知道预期是怎么来的。日期选周一避开周末和节假日干扰让测试只验证「午休裁剪」这一个变量。如果要测节假日就在calendar.loadSpecialDays里塞一条对应日期的数据把外部依赖控制住测试不依赖数据库。上线之后也别把测试扔了。每年工作日安排发布后第一件事是把新数据导进去跑一遍全量测试用例确认跨年、调休这些场景没退化。我一般还会留一个「对账脚本」拿系统算出来的月工时和人工抽样的几天做比对连续几个月对得上才敢说这个模块稳了。工时计算这种事宁可上线前多花两小时写用例也别上线后被考勤对账追着跑。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑