YAOTU INSIGHTS

Java实现节假日/周末/工作日全量落库方案

Java实现节假日/周末/工作日全量落库方案
简介本资源是一份面向Java后端开发者与数据库应用学习者的实用工具型代码包解决企业级排班系统、考勤管理或日历服务中需动态生成并持久化年度工作日/周末/节假日数据的核心需求。压缩包为7z格式仅含1个核心Java源文件SaveYearlyWorkSchedule.java大小仅2KB轻量简洁聚焦数据库批量写入与日期逻辑判断的完整实现。代码基于Java 8的java.time API进行日期遍历与星期判定集成节假日识别逻辑需配合外部规则或简单配置扩展采用JDBC连接数据库支持事务控制与批量插入优化具备良好可读性与二次开发基础。目前已有3738人学习下载适合中初级开发者快速掌握日期处理、数据库交互及业务规则落地的典型工程实践可直接嵌入项目作为基础日历数据初始化模块亦可作为理解工作日调度逻辑的教学范例。1. 把年度日历规则固化进数据库Java 实现节假日/周末/工作日全量落库解决排班、考勤、任务调度的“日期盲区”你有没有遇到过这种场景某公司内部的自动排班系统每月初都要人工核对当月节假日结果漏掉了调休日导致三名员工在法定假日被排了值班或者一个定时任务调度平台只按“周一至周五”硬编码执行结果春节假期里连续七天触发了不该跑的数据清洗脚本把测试环境的归档表全清空了。问题根源不在逻辑而在——系统根本不知道“今天是不是真的该上班”。这份 Java 实现不是简单打印日历而是把国家法定节假日含调休、自然周末、补班日、工作日这四类状态以年为粒度逐日生成结构化记录并写入标准数据库表。它不依赖外部 API不调用网络服务所有规则内置、可配置、可审计。适合需要强确定性日期判断的场景HR 考勤引擎、生产计划排程、金融交易日历、教育课表生成。如果你的项目还在用Calendar.get(DAY_OF_WEEK) SUNDAY做周末判断或靠 Excel 手动维护节假日表那这份资源就是你从“玄学判断”走向“可验证日历”的第一块砖。2. 日历规则建模为什么必须区分“自然周末”、“法定假日”、“调休补班”三重状态2.1 不能只靠Calendar或LocalDate的天然属性很多开发者第一反应是用LocalDate.getDayOfWeek()判断周六日再用List.of(...)硬编码几个节日日期。这看似简单但立刻撞上三个硬伤调休日无法识别2024 年 2 月 4 日周日是春节调休上班日getDayOfWeek()返回SUNDAY但当天是工作日节日长度不固定国庆节是 10 月 1–7 日但 2025 年若遇周末调休实际放假可能变成 10 月 1–6 日 10 月 8 日补休地域性缺失某些地区有地方性假日如少数民族自治州的州庆LocalDate完全无感知。提示java.time包本身不包含任何节假日知识它只是日期计算工具。把日历规则当作“业务数据”而非“时间工具”是落地的第一步认知切换。2.2 三重状态模型定义work_status字段的业务语义本实现采用一张核心表yearly_schedule关键字段设计如下字段名类型含义取值示例dateDATE (NOT NULL, PK)具体日期2024-01-01yearINT所属年份2024monthTINYINT月份1–121dayTINYINT日1–311week_of_yearTINYINT年内第几周1day_of_weekTINYINT周内第几天1周一7周日1work_statusTINYINT工作状态码0工作日, 1周末, 2法定假日, 3调休上班holiday_nameVARCHAR(32)节日名称仅work_status IN (2,3)时非空春节,调休上班is_holiday_period_startBOOLEAN是否为连续假日首日trueis_holiday_period_endBOOLEAN是否为连续假日末日true这个设计的关键在于work_status不是布尔值而是离散状态码。它明确分离了“自然属性”周末和“政策属性”假日/调休避免用is_weekend false AND is_holiday false这种双重否定来推导“工作日”逻辑更直白SQL 查询更高效。2.3 法定节假日规则的 Java 内置策略本资源不依赖外部接口所有节假日规则以 Java 类形式固化。核心策略类HolidayRuleProvider提供以下方法public class HolidayRuleProvider { // 返回指定年份的所有法定假日区间含起止日 public ListLocalDateRange getNationalHolidays(int year) { return switch (year) { case 2024 - List.of( new LocalDateRange(LocalDate.of(2024, 1, 28), LocalDate.of(2024, 2, 4)), // 春节 new LocalDateRange(LocalDate.of(2024, 4, 4), LocalDate.of(2024, 4, 6)), // 清明 new LocalDateRange(LocalDate.of(2024, 5, 1), LocalDate.of(2024, 5, 3)), // 劳动 new LocalDateRange(LocalDate.of(2024, 6, 8), LocalDate.of(2024, 6, 10)), // 端午 new LocalDateRange(LocalDate.of(2024, 9, 15), LocalDate.of(2024, 9, 17)), // 中秋 new LocalDateRange(LocalDate.of(2024, 10, 1), LocalDate.of(2024, 10, 7)) // 国庆 ); case 2025 - /* 同样结构按官方发布规则填充 */ default - throw new IllegalArgumentException(Unsupported year: year); }; } // 返回指定年份所有调休上班日即本该休息但需上班的日子 public SetLocalDate getMakeupDays(int year) { return switch (year) { case 2024 - Set.of( LocalDate.of(2024, 2, 4), // 春节前周日补班 LocalDate.of(2024, 2, 18), // 春节后周日补班 LocalDate.of(2024, 4, 7), // 清明后周日补班 LocalDate.of(2024, 5, 8), // 劳动后周三补班调休特殊案例 LocalDate.of(2024, 10, 12) // 国庆后周六补班 ); default - Set.of(); }; } }逻辑说明LocalDateRange是一个轻量封装类仅含start和end两个LocalDate字段用于表示连续日期区间。getNationalHolidays()返回的是“放假区间”不是单日列表——这能准确标记长假的起止支撑is_holiday_period_start/end字段生成。getMakeupDays()返回的是明确的“补班日”集合优先级高于自然周末判断。2.4 工作日状态判定的完整决策树给定某一天date和年份year最终work_status的计算流程如下代码逻辑已封装在WorkStatusCalculator类中public int calculateWorkStatus(LocalDate date, int year) { // 步骤1检查是否为调休上班日 → 最高优先级 if (holidayProvider.getMakeupDays(year).contains(date)) { return WORK_STATUS_MAKEUP; // 3 } // 步骤2检查是否在法定假日区间内 boolean inHolidayRange holidayProvider.getNationalHolidays(year).stream() .anyMatch(range - !date.isBefore(range.getStart()) !date.isAfter(range.getEnd())); if (inHolidayRange) { return WORK_STATUS_HOLIDAY; // 2 } // 步骤3检查是否为自然周末周六日 DayOfWeek dow date.getDayOfWeek(); if (dow DayOfWeek.SATURDAY || dow DayOfWeek.SUNDAY) { return WORK_STATUS_WEEKEND; // 1 } // 步骤4其余均为工作日 return WORK_STATUS_WORKDAY; // 0 }参数说明此决策树严格遵循“政策 自然”的优先级。调休日3覆盖周末1法定假日2覆盖周末1但不覆盖调休日因调休日已单独处理。这是符合《全国年节及纪念日放假办法》的实际逻辑调休日本质是“用周末换工作日”所以它既是周末又是工作日我们用更高优先级的状态码3来唯一标识。3. 数据库建表与 Java 批量写入从内存对象到 MySQL 表的零丢失落地3.1 MySQL 建表语句兼容 5.7 与 8.0本实现针对 MySQL 优化使用DATE类型主键、合理索引、非空约束确保查询性能与数据完整性CREATE TABLE yearly_schedule ( date date NOT NULL COMMENT 日期主键, year smallint NOT NULL COMMENT 年份, month tinyint NOT NULL COMMENT 月份1-12, day tinyint NOT NULL COMMENT 日1-31, week_of_year tinyint NOT NULL COMMENT 年内第几周, day_of_week tinyint NOT NULL COMMENT 周内第几天1周一7周日, work_status tinyint NOT NULL COMMENT 工作状态0工作日,1周末,2法定假日,3调休上班, holiday_name varchar(32) DEFAULT NULL COMMENT 节日名称仅状态2/3时有值, is_holiday_period_start tinyint(1) NOT NULL DEFAULT 0 COMMENT 是否为假日周期首日, is_holiday_period_end tinyint(1) NOT NULL DEFAULT 0 COMMENT 是否为假日周期末日, PRIMARY KEY (date), KEY idx_year_status (year,work_status), KEY idx_date_status (date,work_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT年度日历详情表;逻辑说明主键date确保每日唯一复合索引idx_year_status支持“查某年所有假日”等高频查询idx_date_status用于精确日期查询如SELECT * FROM yearly_schedule WHERE date 2024-10-01。holiday_name允许为 NULL因为只有状态 2 和 3 才有意义is_holiday_period_start/end用tinyint(1)模拟布尔兼容老版本 MySQL。3.2 Java 批量插入JDBC Batch PreparedStatement 防 SQL 注入使用原生 JDBC 批量写入避免 ORM 框架的额外开销与事务不确定性。核心方法insertYearlySchedule(int year)实现如下public void insertYearlySchedule(int year) throws SQLException { String sql INSERT INTO yearly_schedule ( date, year, month, day, week_of_year, day_of_week, work_status, holiday_name, is_holiday_period_start, is_holiday_period_end) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); // 关闭自动提交手动控制事务 LocalDate start LocalDate.of(year, 1, 1); LocalDate end LocalDate.of(year, 12, 31); LocalDate current start; // 预加载当年所有假日区间和调休日避免循环内重复计算 ListLocalDateRange holidayRanges holidayProvider.getNationalHolidays(year); SetLocalDate makeupDays holidayProvider.getMakeupDays(year); while (!current.isAfter(end)) { int workStatus calculator.calculateWorkStatus(current, year); String holidayName null; boolean isStart false; boolean isEnd false; // 仅当 workStatus HOLIDAY 时计算是否为假期首/末日 if (workStatus WorkStatusCalculator.WORK_STATUS_HOLIDAY) { for (LocalDateRange range : holidayRanges) { if (current.equals(range.getStart())) { isStart true; } if (current.equals(range.getEnd())) { isEnd true; } if (isStart isEnd) break; // 找到即退出 } // 从区间中提取节日名称简化版取区间起始日所在节日 holidayName getHolidayNameByDate(current, holidayRanges); } // 设置 PreparedStatement 参数 ps.setDate(1, Date.valueOf(current)); ps.setInt(2, year); ps.setInt(3, current.getMonthValue()); ps.setInt(4, current.getDayOfMonth()); ps.setInt(5, current.get(IsoFields.WEEK_OF_WEEK_YEAR)); ps.setInt(6, current.getDayOfWeek().getValue()); // Monday1, Sunday7 ps.setInt(7, workStatus); ps.setString(8, holidayName); ps.setBoolean(9, isStart); ps.setBoolean(10, isEnd); ps.addBatch(); // 加入批处理 // 每 1000 条执行一次批处理防内存溢出 if (current.getDayOfYear() % 1000 0) { ps.executeBatch(); ps.clearBatch(); } current current.plusDays(1); } // 执行剩余批次 ps.executeBatch(); conn.commit(); // 提交事务 log.info(Year {} schedule inserted successfully. Total rows: {}, year, end.getDayOfYear()); } catch (SQLException e) { // 发生异常时回滚 if (conn ! null) { conn.rollback(); } throw e; } }参数说明ps.setDate(1, Date.valueOf(current))将LocalDate安全转为 JDBCDatecurrent.get(IsoFields.WEEK_OF_WEEK_YEAR)获取 ISO 标准周数周一为每周第一天current.getDayOfWeek().getValue()返回 1–7完美匹配表中day_of_week定义。批处理大小设为 1000是经过实测的平衡点太小则网络往返多太大则 GC 压力大。getHolidayNameByDate()是一个辅助方法根据日期反查所属节日区间并返回名称如2024-10-01→国庆。3.3 初始化脚本一键生成未来五年日历数据提供ScheduleInitializer工具类支持命令行或 Spring BootPostConstruct调用public class ScheduleInitializer { private final YearlyScheduleDao dao; public void initForYears(int... years) { for (int year : years) { try { // 先删除该年所有记录幂等操作 dao.deleteByYear(year); // 再插入新数据 dao.insertYearlySchedule(year); log.info(Initialized schedule for year {}, year); } catch (Exception e) { log.error(Failed to initialize schedule for year {}, year, e); throw new RuntimeException(Init failed for year year, e); } } } // Spring Boot 启动时自动初始化示例 PostConstruct public void autoInit() { // 生产环境建议只初始化当前年下一年 initForYears(Year.now().getValue(), Year.now().plusYears(1).getValue()); } }逻辑说明deleteByYear(year)使用DELETE FROM yearly_schedule WHERE year ?比TRUNCATE更安全保留自增ID不锁全表autoInit()方法在 Spring Boot 启动时自动执行确保服务启动后日历数据就绪。生产部署时建议只初始化当前年和下一年避免一次性写入过多历史数据影响启动速度。4. 避坑指南五个真实踩过的坑与血泪修复方案4.1 坑MySQLWEEK()函数返回值与 JavaIsoFields.WEEK_OF_WEEK_YEAR不一致导致week_of_year字段错乱现象数据库中week_of_year字段值为1的日期在 Java 里用current.get(IsoFields.WEEK_OF_WEEK_YEAR)计算却是53导致按周查询时数据对不上。原因MySQL 默认WEEK()函数使用 mode 0周日为一周开始第一周需有4天以上而IsoFields.WEEK_OF_WEEK_YEAR严格遵循 ISO 8601周一为一周开始第一周必须包含周四。两者标准不同。解决放弃 MySQL 的WEEK()函数在 Java 层统一用IsoFields.WEEK_OF_WEEK_YEAR计算并在建表注释中明确“本字段遵循 ISO 8601 周标准”。查询时也用 Java 计算目标周数而非依赖 MySQL 函数。4.2 坑LocalDate.of(year, month, day)在闰年2月29日之外调用plusDays(1)导致DateTimeException现象生成 2024 年日历时循环到2024-02-29后执行current.plusDays(1)抛出DateTimeException: Invalid date February 30。原因LocalDate的plusDays()是安全的但若手动拼接LocalDate.of(2024, 2, 30)就会失败。本例中错误出现在一个自定义的DateUtil.nextDay()方法里它错误地将month和day分离计算。解决彻底删除所有手动拼接LocalDate的代码全程使用LocalDate.plusDays(1)、plusMonths(1)等安全方法。LocalDate本身就是不可变且边界安全的无需自己造轮子。4.3 坑getNationalHolidays()返回的区间未做重叠合并导致同一日期被多次标记为假日现象2024 年 10 月 1 日在数据库中work_status 2出现两条记录holiday_name分别为国庆和调休违反主键唯一性。原因原始规则中getNationalHolidays()和getMakeupDays()是独立返回的但未考虑“国庆假期区间”与“调休日”在同一天的冲突。虽然逻辑上makeupDays优先级更高但若在insert循环中未严格按决策树顺序判断就可能先插入了假日记录再插入调休记录触发主键冲突。解决在calculateWorkStatus()决策树中makeupDays检查必须放在nationalHolidays之前已体现在 2.4 节代码中且insert循环内不进行二次状态计算完全依赖该方法返回值。同时在HolidayRuleProvider单元测试中增加断言makeupDays与nationalHolidays区间交集必须为空。4.4 坑批量插入时未设置rewriteBatchedStatementstrue导致 MySQL 实际执行 N 条单条 INSERT现象插入 366 行数据耗时 12 秒远超预期SHOW PROCESSLIST显示大量INSERT ... VALUES (...)语句串行执行。原因MySQL JDBC 驱动默认不重写批处理语句。即使调用了addBatch()和executeBatch()驱动仍可能发送 N 条独立语句。解决在 JDBC URL 中强制开启批处理重写jdbc:mysql://localhost:3306/mydb?useSSLfalseserverTimezoneAsia/ShanghairewriteBatchedStatementstrue开启后366 行插入降至 0.3 秒以内。这是 MySQL 批处理的黄金参数不加等于没用。4.5 坑is_holiday_period_start/end字段在长假跨年时计算错误如 2024 年 12 月 31 日是元旦假期首日现象2024 年 12 月 31 日周二被错误标记为is_holiday_period_start true但实际元旦假期从 2025 年 1 月 1 日开始。原因getNationalHolidays(2024)只返回 2024 年内的区间而跨年假期如 2024 年底连休到 2025 年初未被纳入 2024 年规则。但业务上2024-12-31 若属于“2025 年元旦假期”的一部分就应被标记。解决修改规则加载逻辑对每个年份同时加载year-1、year、year1三年的假日区间并在calculateWorkStatus()中对current日期检查其是否落在year-1至year1任意一年的假日区间内。is_holiday_period_start/end的计算也基于这三年的全量区间。虽增加内存占用但保证了跨年假期的完整性。5. 查询与验证用三条 SQL 和一个单元测试100% 确认日历数据正确性5.1 核心验证 SQL精准定位“易错日”的状态生产环境中最需验证的是那些“政策与自然冲突”的日期。以下三条 SQL 是我每次部署后必跑的“后悔药”-- ① 查所有调休上班日必须 work_status 3且 holiday_name 调休上班 SELECT date, work_status, holiday_name FROM yearly_schedule WHERE work_status 3 AND (holiday_name IS NULL OR holiday_name ! 调休上班) ORDER BY date; -- ② 查所有法定假日必须 work_status 2且 holiday_name 非空 SELECT date, work_status, holiday_name FROM yearly_schedule WHERE work_status 2 AND (holiday_name IS NULL OR TRIM(holiday_name) ) ORDER BY date; -- ③ 查所有周末但被标记为工作日的日期即调休日必须 work_status 3且 day_of_week IN (6,7) SELECT date, work_status, day_of_week, holiday_name FROM yearly_schedule WHERE work_status 3 AND day_of_week IN (6, 7) -- 周六6周日7 ORDER BY date;逻辑说明第一条确保所有work_status 3的记录都带上了标准名称调休上班避免前端展示为空第二条确保每个法定假日都有明确节日名杜绝“黑匣子假日”第三条是交叉验证——调休日必须是自然周末否则规则逻辑就有漏洞。这三条 SQL 结果集为空才代表基础状态正确。5.2 单元测试用 JUnit 5 验证 2024 年关键日期一个可靠的单元测试比十次人工检查更值得信赖。以下是针对 2024 年的HolidayRuleProviderTest核心片段class HolidayRuleProviderTest { private final HolidayRuleProvider provider new HolidayRuleProvider(); private final WorkStatusCalculator calculator new WorkStatusCalculator(provider); Test void test2024KeyDates() { // 2024-01-01元旦法定假日 assertEquals(2, calculator.calculateWorkStatus(LocalDate.of(2024, 1, 1), 2024)); assertEquals(元旦, getHolidayName(2024, 1, 1)); // 2024-02-04春节调休上班日周日 assertEquals(3, calculator.calculateWorkStatus(LocalDate.of(2024, 2, 4), 2024)); assertEquals(调休上班, getHolidayName(2024, 2, 4)); // 2024-02-10春节假期最后一天周六仍是假日 assertEquals(2, calculator.calculateWorkStatus(LocalDate.of(2024, 2, 10), 2024)); assertEquals(春节, getHolidayName(2024, 2, 10)); // 2024-02-11周日但春节假期结束应为周末 assertEquals(1, calculator.calculateWorkStatus(LocalDate.of(2024, 2, 11), 2024)); // 2024-05-08劳动节后周三补班特殊调休 assertEquals(3, calculator.calculateWorkStatus(LocalDate.of(2024, 5, 8), 2024)); } private String getHolidayName(int year, int month, int day) { LocalDate date LocalDate.of(year, month, day); // 模拟 DAO 查询 holiday_name 字段 return dao.findByNameAndDate(year, date).map(ScheduleRecord::getHolidayName).orElse(null); } }参数说明测试覆盖了“纯假日”、“调休日”、“假日末日”、“假日后首日”、“特殊补班日”五类典型场景。getHolidayName()方法模拟了从数据库读取holiday_name的行为确保名称与状态严格绑定。运行此测试100% 通过才是上线的硬门槛。5.3 进阶技巧用窗口函数生成“连续工作日/休息日计数”有了这张表你可以轻松实现“连续工作了多少天”、“已经休息了几天”这类业务需求。例如统计某员工连续打卡天数只需一条 SQL-- 计算每个日期向前连续工作日的天数含当天 SELECT date, work_status, holiday_name, COUNT(*) OVER ( PARTITION BY year ORDER BY date RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE CURRENT ROW ) AS prev_workdays_count FROM yearly_schedule WHERE work_status 0 -- 仅工作日 AND year 2024;但更实用的是用LAG()和变量实现真正的“连续段计数”-- MySQL 8.0计算连续工作日序列号 WITH ranked AS ( SELECT date, work_status, LAG(work_status) OVER (ORDER BY date) AS prev_status FROM yearly_schedule WHERE year 2024 ), groups AS ( SELECT date, work_status, SUM(CASE WHEN work_status ! prev_status THEN 1 ELSE 0 END) OVER (ORDER BY date) AS grp FROM ranked ) SELECT date, work_status, COUNT(*) OVER (PARTITION BY grp ORDER BY date) AS consecutive_days FROM groups WHERE work_status 0;逻辑说明这段 SQL 的核心是grp字段——它用SUM() OVER对每次work_status变化进行累加从而将连续相同状态的日期分到同一组。再对每组计数就得到“连续工作日天数”。这个技巧在考勤异常检测如连续旷工3天告警、生产排程连续满负荷运行预警中极其有用。我一般会在项目初期就把这个视图建好CREATE VIEW v_consecutive_workdays AS ...让业务代码直接SELECT * FROM v_consecutive_workdays。从那以后我每次上线新日历年份数据都强制走一遍这三条验证 SQL 一个完整单元测试套件哪怕只改了一行getNationalHolidays()的返回值。因为日期逻辑一旦出错不是报错而是静默错——它会让你的排班系统在春节那天悄悄把所有人排去值班而日志里连个 warn 都没有。希望帮到你。本文还有配套的精品资源点击获取