
简介这份资源面向Java后端开发者与需要处理日期排班逻辑的工程师提供将每年工作日、周末与法定节假日明细批量写入数据库表的完整实现思路。核心代码围绕java.time包中的LocalDate、DayOfWeek展开日期判定结合节假日库识别法定假日并通过JDBC完成数据库连接、SQL执行与事务管理同时涉及批量插入优化与表结构设计等要点。压缩包为7z格式内含1个java源文件整体约2KB文件精简便于直接阅读与二次改造。目前已有3738人学习下载说明该场景在排班、考勤、工时统计等业务中较为常见。读者可从中获取日期分类判断、数据库表字段设计、批量提交与事务回滚等可复用代码骨架并参考其单一职责与可配置性思路快速搭建或扩展自己的年度工作日历数据初始化模块。1. 节假日表不是日历翻版先把「每年重算一次」这件事讲透做排班、考勤、计费、工单 SLA 的 Java 后端几乎都会撞上同一个需求把某一年的每一天标清楚它到底是工作日、周末还是法定节假日然后落进数据库表里供后续查询和统计。听起来像是个体力活但真正动手就会发现坑不在写代码而在「数据从哪来、怎么算、怎么存、怎么更新」。我见过太多项目一开始用Calendar硬编码判断周末结果调休一出来全线翻车考勤算错、加班费算错最后只能靠人工补数据。这篇讲的就是用 Java 把「每年的节假日、周末、工作日详情」稳定地记录到数据库表里。核心思路是节假日数据不靠猜靠一份可维护的年度数据源工作日判定不靠单一规则靠「周末 法定节假日 调休上班」三者叠加。适合正在做考勤、排班、审批流、财务结算的 Java 工程师也适合想把这套逻辑做成通用基础服务的团队。下面从表结构设计一路讲到批量入库和年度更新中间该踩的坑我都会标出来。2. 表结构怎么设计一张日历表要能扛住调休和跨年查询2.1 为什么不用「日期 是否工作日」两个字段糊弄过去很多人第一反应是建一张表字段就date和is_workday简单直接。但真到业务里就会发现问题前端要显示「国庆节」这种名称财务要区分「法定节假日」和「普通周末」排班系统还要知道某天是不是调休上班。如果只有布尔值这些信息全得在代码里再判断一遍等于把逻辑散落到各处。我一般会把日历表设计成「一天一行字段尽量自解释」。核心字段包括日期本身、星期几、日期类型、是否工作日、节假日名称、所属年份。日期类型用一个枚举区分工作日、周末、法定节假日、调休上班日。这样查询「今年所有法定节假日」就是一句where day_type HOLIDAY不用再写一堆if。CREATE TABLE calendar_day ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, calendar_date DATE NOT NULL COMMENT 日期, year_num INT NOT NULL COMMENT 所属年份, week_day TINYINT NOT NULL COMMENT 星期几1周一 ... 7周日, day_type VARCHAR(16) NOT NULL COMMENT 类型WORKDAY/WEEKEND/HOLIDAY/MAKEUP_WORKDAY, is_workday TINYINT(1) NOT NULL COMMENT 是否工作日1 是 0 否, holiday_name VARCHAR(64) DEFAULT NULL COMMENT 节假日名称如 国庆节, remark VARCHAR(128) DEFAULT NULL COMMENT 备注如 调休上班, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_calendar_date (calendar_date), KEY idx_year_type (year_num, day_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT年度日历明细表;这里有几个参数值得说清楚。calendar_date上加唯一索引是为了让「按年重算」可以用INSERT ... ON DUPLICATE KEY UPDATE幂等执行重复跑不会产生脏数据。year_num单独存一列而不是靠YEAR(calendar_date)现算是因为跨年查询和分区裁剪时独立列能走索引性能差别在数据量大时很明显。day_type用字符串而不是数字是为了排查问题时肉眼可读代价是存储略大日历表一年才 365 行这点开销可以忽略。提示如果数据库是 MySQL 8.0 以上is_workday其实可以用生成列但我不建议因为调休规则会变生成列改起来麻烦不如老老实实写入。2.2 数据源从哪来别在代码里硬编码节假日表建好了数据从哪来这是整个方案里最容易被低估的一环。我见过有人把国庆、春节的日期写死在static final数组里结果每年都要改代码重新发版遇到临时调整直接傻眼。正确做法是把节假日数据外置成一份可维护的配置代码只负责读取和计算。常见做法是维护一份 JSON 或 YAML 文件按年份组织每个节假日包含名称、开始日期、结束日期以及调休上班的日期列表。这份文件可以放在resources目录下也可以放数据库或配置中心。放文件的好处是版本可控跟着代码走放配置中心的好处是不发版就能改。我一般先用文件等业务稳定了再考虑动态化。{ year: 2025, holidays: [ { name: 元旦, start: 2025-01-01, end: 2025-01-01 }, { name: 春节, start: 2025-01-28, end: 2025-02-04 }, { name: 清明节, start: 2025-04-04, end: 2025-04-06 }, { name: 劳动节, start: 2025-05-01, end: 2025-05-05 }, { name: 端午节, start: 2025-05-31, end: 2025-06-02 }, { name: 国庆节与中秋节, start: 2025-10-01, end: 2025-10-08 } ], makeupWorkdays: [2025-01-26, 2025-02-08, 2025-04-27, 2025-09-28, 2025-10-11] }这份结构里holidays描述放假的区间makeupWorkdays描述调休上班的日期。注意调休上班日通常落在周末所以判定逻辑里它的优先级要高于「周末」规则。数据源本身不参与计算只提供事实计算逻辑统一放在 Java 服务里这样职责清晰出问题也好定位。3. 用 Java 生成全年日历从 LocalDate 遍历到类型判定3.1 遍历一年的每一天先把基础属性填上有了表和数据源接下来就是生成。Java 8 之后处理日期我基本只用java.time包LocalDate比老的Calendar干净太多月份从 1 开始不用再被Calendar.JANUARY是 0 这种玄学折磨。生成逻辑很直白从 1 月 1 日遍历到 12 月 31 日每天构造一个对象先填日期、年份、星期几再根据规则判定类型。public ListCalendarDay buildYear(int year, HolidayConfig config) { ListCalendarDay result new ArrayList(366); LocalDate start LocalDate.of(year, 1, 1); LocalDate end LocalDate.of(year, 12, 31); // 预先把节假日区间和调休日展开成集合避免循环里反复解析 SetLocalDate holidaySet config.expandHolidayDates(); MapLocalDate, String holidayNameMap config.expandHolidayNames(); SetLocalDate makeupSet config.getMakeupWorkdays(); for (LocalDate d start; !d.isAfter(end); d d.plusDays(1)) { CalendarDay day new CalendarDay(); day.setCalendarDate(d); day.setYearNum(year); day.setWeekDay(d.getDayOfWeek().getValue()); // 1周一 ... 7周日 if (makeupSet.contains(d)) { // 调休上班优先级最高哪怕是周末也算工作日 day.setDayType(MAKEUP_WORKDAY); day.setWorkday(true); day.setRemark(调休上班); } else if (holidaySet.contains(d)) { day.setDayType(HOLIDAY); day.setWorkday(false); day.setHolidayName(holidayNameMap.get(d)); } else if (day.getWeekDay() 6) { day.setDayType(WEEKEND); day.setWorkday(false); } else { day.setDayType(WORKDAY); day.setWorkday(true); } result.add(day); } return result; }这段代码的关键在判定顺序调休上班 法定节假日 周末 普通工作日。顺序错了结果就错。比如某天既是周末又是调休上班日必须先判调休否则会被当成周末休息。expandHolidayDates负责把start到end的区间展开成一个个LocalDateexpandHolidayNames建立日期到名称的映射这样循环里只做集合查找复杂度是 O(1)一年 365 次循环毫无压力。参数上year决定生成范围config决定事实来源。我一般会把config做成按年加载buildYear只处理单年跨年就循环调用。这样每年数据独立某年配置错了不会污染其他年份。3.2 批量入库用 ON DUPLICATE KEY UPDATE 做幂等生成完对象列表接下来写库。逐条insert在 365 条数据面前不算慢但考虑到每年都要重跑、可能还要补历史年份我更推荐批量插入配合幂等更新。MySQL 的INSERT ... ON DUPLICATE KEY UPDATE正好适合这种场景日期已存在就更新类型和名称不存在就插入重复执行结果一致。Mapper public interface CalendarDayMapper { Insert(script INSERT INTO calendar_day (calendar_date, year_num, week_day, day_type, is_workday, holiday_name, remark) VALUES foreach collectionlist itemit separator, (#{it.calendarDate}, #{it.yearNum}, #{it.weekDay}, #{it.dayType}, #{it.workday}, #{it.holidayName}, #{it.remark}) /foreach ON DUPLICATE KEY UPDATE day_type VALUES(day_type), is_workday VALUES(is_workday), holiday_name VALUES(holiday_name), remark VALUES(remark) /script) int batchUpsert(Param(list) ListCalendarDay list); }这里用 MyBatis 的foreach拼批量 SQL一次提交 365 条比循环单条插入快一个数量级。ON DUPLICATE KEY UPDATE依赖前面建的唯一索引uk_calendar_date没有唯一索引这个语法不会报错但会一直插入重复行这是血泪教训。另外批量 SQL 别一次拼太多MySQL 默认max_allowed_packet是 4MB365 条完全够用但如果要一次补十年数据建议按年分批提交。注意is_workday字段在实体里用BooleanMyBatis 映射到TINYINT(1)时通常没问题但如果你的驱动版本较老可能写入的是true/false字符串导致报错稳妥做法是在实体里直接用Integer或加typeHandler。4. 查询与缓存让日历表真正被业务用起来4.1 三个高频查询场景和对应 SQL数据入库只是开始业务真正关心的是怎么查。我统计过日历表的使用场景基本逃不出三类判断某天是不是工作日、取某个月的工作日列表、统计某年工作日总数。这三类查询都能靠索引走得很顺。判断某天是不是工作日直接按日期查唯一索引SELECT is_workday, day_type, holiday_name FROM calendar_day WHERE calendar_date 2025-10-01;取某月工作日列表用年份和日期范围过滤idx_year_type在这里帮不上太多但日期范围本身能走唯一索引SELECT calendar_date, day_type, holiday_name FROM calendar_day WHERE calendar_date BETWEEN 2025-10-01 AND 2025-10-31 AND is_workday 1 ORDER BY calendar_date;统计某年工作日总数走idx_year_type覆盖索引SELECT COUNT(*) FROM calendar_day WHERE year_num 2025 AND is_workday 1;这三条 SQL 覆盖了绝大多数需求。如果业务还要算「两个日期之间有几个工作日」可以在应用层用BETWEEN查出区间再count也可以写一条聚合 SQL但要注意区间跨年时year_num条件不能写死。4.2 要不要加缓存先看 QPS 再决定日历数据的特点是「读多写极少、数据量小、变更频率低」天然适合缓存。但我不建议一上来就上 Redis先看实际 QPS。如果只是考勤系统每天几次查询直接查库完全够用365 行的表全表扫描都是毫秒级。只有当排班、计费这类高频服务每秒要查几百上千次时才值得加缓存。加缓存的话我一般按年缓存整个MapLocalDate, CalendarDaykey 用年份value 是整年的映射。这样一次加载全年查询都是内存操作。更新时按年失效重新加载即可。要注意的是缓存和数据库的一致性年度数据更新频率低用「更新数据库后主动删缓存」就够了不用搞复杂的双写。// 按年缓存整年日历key 为年份 private final MapInteger, MapLocalDate, CalendarDay yearCache new ConcurrentHashMap(); public CalendarDay getByDate(LocalDate date) { int year date.getYear(); MapLocalDate, CalendarDay map yearCache.computeIfAbsent(year, y - { ListCalendarDay list mapper.listByYear(y); return list.stream().collect(Collectors.toMap(CalendarDay::getCalendarDate, d - d)); }); return map.get(date); }computeIfAbsent保证并发下只加载一次ConcurrentHashMap保证线程安全。这个方案简单可靠适合单机如果多实例部署缓存各自维护更新时通过消息广播失效或者干脆用 Redis 统一存。5. 避坑与排查节假日表最容易翻车的五个地方5.1 调休日被当成周末考勤全错现象某年调休上班的周六考勤系统显示休息员工打卡被记为异常。原因判定逻辑里先判周末再判调休或者根本没处理调休集合。解决把调休上班日的判定放在最前面优先级高于节假日和周末并在数据源里显式维护makeupWorkdays列表不要靠「周末补班」这种模糊规则推断。5.2 跨年查询漏数据12 月底和 1 月初出错现象查「2025 年 12 月 30 日到 2026 年 1 月 3 日」的工作日结果只返回了 2025 年的部分。原因查询条件里写死了year_num 2025跨年区间被截断。解决区间查询只用calendar_date BETWEEN不要带year_num等值条件如果非要带改成year_num IN (2025, 2026)。5.3 重复执行生成任务数据翻倍现象年度更新脚本跑了两遍日历表里同一天出现两条记录。原因calendar_date没加唯一索引或者用了普通INSERT而不是幂等更新。解决唯一索引是底线配合ON DUPLICATE KEY UPDATE或先DELETE再INSERT事务内。我倾向幂等更新因为删除重建会短暂出现数据空洞影响并发查询。5.4 节假日名称对不上前端显示空白现象前端日历上国庆节那天没有名称只显示「休息」。原因holiday_name只在节假日区间第一天填了区间内其他天没填或者expandHolidayNames展开时漏了日期。解决展开时对区间内每一天都建立名称映射确保holidaySet和holidayNameMap的 key 完全一致写完后用一条SELECT COUNT(*) FROM calendar_day WHERE day_typeHOLIDAY AND holiday_name IS NULL自检。5.5 时区问题导致日期偏移一天现象服务器时区是 UTCLocalDate.now()拿到的日期比北京时间晚一天生成的数据整体偏移。原因LocalDate本身不带时区但now()依赖系统默认时区。解决生成任务里显式指定时区用LocalDate.now(ZoneId.of(Asia/Shanghai))数据库连接串也加上serverTimezoneAsia/Shanghai避免驱动层再转一次。6. 年度更新与自动化把「每年改一次代码」变成「每年改一份配置」这套方案真正省心的地方在于把「每年重算」变成了「每年更新一份配置 跑一次任务」。我一般会写一个命令行入口接收年份参数加载对应年份的 JSON生成并入库。这样每年年底运维或开发只需要新增一份holiday-2026.json然后执行一次任务不用改任何 Java 代码也不用重新发版。# 生成 2026 年日历数据 java -jar calendar-tool.jar --year2026 --configclasspath:holiday-2026.json更进一步可以把这个任务做成定时任务每年 12 月 1 日自动检查下一年配置是否存在存在就自动生成。配置的准确性仍然需要人工维护因为节假日安排是外部事实代码无法推断。但至少「计算」和「入库」这两步完全自动化了。验证数据是否正确我习惯用几个固定断言一年工作日总数应该在 240 到 250 之间不同年份略有浮动法定节假日天数在 11 天左右不含调休调休上班日通常 5 到 7 天。如果生成结果偏离这些范围大概率是配置写错了。还可以抽查几个已知日期比如国庆当天必须是HOLIDAY且is_workday 0调休的周六必须是MAKEUP_WORKDAY且is_workday 1。最后说个我自己的习惯每次更新完年度数据我都会把当年的日历导出一份 CSV 存档和配置一起提交到代码仓库。这样万一哪天数据库被误删或者需要回溯某年的判定结果有据可查。日历表这东西平时不起眼但一旦算错影响的是真金白银的考勤和结算多留一份后悔药不亏。希望帮到你。本文还有配套的精品资源点击获取