在很多系统中,我们都会遇到这样一个问题:某一天到底是不是工作日?
初看之下,这似乎是一个不值得讨论的问题——只要判断是不是周一到周五就行了。但当系统真正进入复杂业务场景后,这种简单判断往往会频繁“翻车”。
例如:
- 定时任务只在工作日执行,却在节假日误触发
- 消息通知要求“下一个工作日发送”,结果碰到调休全部算错
- 财务结算、工单 SLA 计算,因为节假日未被正确识别而出现偏差
本质上,工作日并不是一个纯粹的时间问题,而是一个业务规则问题。它不仅受到星期的影响,还依赖于法定节假日、调休安排,甚至是企业内部自定义规则。
因此,想要在系统中正确地判定工作日与休息日,必须跳出“简单日期判断”的思路,从规则建模和工程实现两个层面来重新审视这个问题。本文将围绕这一目标,逐步拆解工作日判定背后的设计思路与实现方式。
需求分析
需求分析
- 休息日
- 判定某天是否为周末、节假日
- 计算某天的N 个休息日前、后的日期。例如:3个休息日后。应用场景不是很多
- 计算某天开始,到下N个休息日的日期。例如:下一个休息日
- 计算两个日期之间的休息日的天数
- 工作日
- 判定某天是否为法定工作日
- 计算某天的N 个工作日前、后的日期。例如:5个工作日后
- 计算某天开始,到下N个工作日的日期。例如:下一个工作日
- 计算两个日期之间的工作日的天数
- 批量判定
- 管理端
技术方案
技术方案
- 纯代码方案:不可行。因为每年的节假日安排都不一样,受国务院管理(中国大陆,不含港澳台)
- 使用表记录
- 只记录休息日(周六周日、节假日)
- 需要注意的是,不一定周六周日一定是休息日
- 判定时如果在表中查到了数据,就是休息日,否则是工作日。
- 记录所有日期
- 通过type区分(工作日、周末、法定节假日、调休工作日)。
- 如果是调休工作日,可以记录该个工作日调休的是哪一天
怎么更新休息日信息:
中国大陆(不含港澳台)每年的节假日信息,由国务院在上一年的12月前更新公示
- 手工录入节假日信息
- 定时任务监控cn公告页面变化,自动解析节假日信息
只记录休息日
主要思路:
- 用一张表专门记录每年的休息日
- 需要注意的是,不一定周六周日一定是休息日
- 判定时如果在表中查到了数据,就是休息日,否则是工作日
- 数据几乎不会变化,在项目初始化时从数据库读取后放入缓存
项目Demo:https://github.com/HackyleShawe/JavaBackEndDemos/tree/master/BusinessCommonSolution/workday-holiday-demo
表定义:前缀_holiday。工作日节假日判定一般为系统级的功能,所以表命名为:sys_holiday- CREATE TABLE sys_holiday
- (
- id BIGINT PRIMARY KEY AUTO_INCREMENT,
- year INT DEFAULT 0 COMMENT '年份,便于快速获取某年的所有节假日',
- holiday DATE DEFAULT NULL COMMENT '节假日期',
- description VARCHAR(512) DEFAULT '' COMMENT '节假日说明,例如:周六、周日、春节、国庆节',
- region VARCHAR(16) DEFAULT '' COMMENT '国家区域代码,例如CN、TW、HK',
- deleted BIT DEFAULT b'0' COMMENT '0-False-未删除, 1-True-已删除',
- create_by BIGINT DEFAULT 0,
- create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
- update_by BIGINT DEFAULT 0,
- update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- INDEX idx_holiday (holiday)
- ) COMMENT '休息日信息表';
复制代码 数据初始化时机?
- 在节假日安排出来的时候(当年的国庆节后),由人工、辅助工具、API导入
- 如果是人工导入,可以考虑在界面上提供一个日期选择器,按月为一个基本单位,选择出休息日,提交到后端保存;如果没有该月份的数据提交,则默认该月按照标准的周六周日休息来初始化
周六周日为什么也需要保存到表中?
- 虽然周六周日可以通过Java提供的API实现(LocalDate#getDayOfWeek)
- 但是涉及一个小长假的附近,可能出现周六周日也需要工作的场景,所以必须记录周六周日是休息日的情况
判定是否为休息日(周六周日、节假日)?
- 日期在sys_holiday中是否存在?存在则立即返回即为是节假日(休息日)
- 查库没有,一定不是节假日?是的,现阶段设计只能如此
- 查往年的日期,没找到,无法判定不是节假日?再按照年份去查库,还是没有则抛出异常:节假日未初始化,请先初始化xx年的节假日信息
- 查未来的日期,没找到,无法判定不是节假日?再按照年份去查库,还是没有则抛出异常:节假日未初始化,请先初始化xx年的节假日信息
如何判定指定日期是否为工作日?该日期不在节假日表中,则为工作日
缓存设计
- 数据结构:hash
- Key:sys_holiday:国家区域代码:今年
- HKey: yyyy-MM-dd (String),使用时需要判断给定日期是否在map中,如果在就是休息日,如果不在就不是
- HVal: HolidayCacheDto (JSON)
- QA
- 缓存三年?虽然key是今年,但是缓存3年的,防止在今年的年初年尾判断时查库,提升命中率
- 年份交界处,怎么删除旧的和初始化新的?
- 旧的过期自动删除,新的根据key自动查库放入缓存
- 会有短暂这种情况:今年的key中,含有明年的的节假日。但随着过期自动删除,到达明年自动恢复
- 注意:不要使用LocalDate作为Key,因为:Redis序列化时不支持LocalDate,需要额外的配置,可能与其他的序列化配置起冲突
方案评价
- 优势
- 只记录休息日,数据体量稍微少一些
- 初始化数据时比较麻烦
- 劣势
- 无法表达调休工作日的信息
- 查库没有,无法判定不是节假日,因为也有可能是没有初始化该年的节假日信息
- 结论:存在较大的设计缺陷,实现复杂度麻烦,不建议使用
记录所有日期
主要思路
- 用一张表记录所有日期
- 通过type区分(工作日、周末、法定节假日、调休工作日)。
- 判定时根据在表中查到的数据,进行判断,以及获取具体信息
- 如果是调休工作日,可以记录该个工作日调休的是哪一天
- 数据几乎不会变化,在项目初始化时从数据库读取后放入缓存
项目Demo:https://github.com/HackyleShawe/JavaBackEndDemos/tree/master/BusinessCommonSolution/workday-holiday-demo
表定义:前缀_calendar。工作日节假日判定一般为系统级的功能,所以表命名为:sys_calendar- CREATE TABLE sys_calendar
- (
- id BIGINT PRIMARY KEY AUTO_INCREMENT,
- year INT DEFAULT 0 COMMENT '年份,便于快速获取某年的所有节假日',
- calendar_date DATE DEFAULT NULL COMMENT '日期',
- workday BIT DEFAULT b'1' COMMENT '0-False-非工作日, 1-True-是工作日',
- type VARCHAR(20) DEFAULT '' COMMENT '类型:HOLIDAY / WEEKEND / WORKDAY',
- description VARCHAR(512) DEFAULT '' COMMENT '节假日说明,例如:周六、周日、春节、国庆节',
- region VARCHAR(16) DEFAULT '' COMMENT '国家区域代码,例如CN、TW、HK',
- deleted BIT DEFAULT b'0' COMMENT '0-False-未删除, 1-True-已删除',
- create_by BIGINT DEFAULT 0,
- create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
- update_by BIGINT DEFAULT 0,
- update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- INDEX idx_calendar_date (calendar_date)
- ) COMMENT '日历信息表';
复制代码 判定是否为节假日?日期在sys_calendar中是否存在?并且根据workday字段协同判断
数据初始化时机?
- 在节假日安排出来的时候(当年的国庆节后),由人工、辅助工具、API导入
- 如果是人工导入,可以考虑在界面上提供一个日期选择器,按月为一个基本单位,选择出休息日,提交到后端保存;如果没有该月份的数据提交,则默认该月按照标准的周六周日休息来初始化
如何判定指定日期是否为工作日?该日期是否在日历表中,以及结合是否工作日(workday)判断
判定是否为休息日(周六周日、节假日)?
- 该日期是否在日历表中,以及结合是否工作日(workday)判断
- 查库没有,怎么判断是否为休息日?不能判断,直接抛出异常:日历未初始化,请先初始化xx年的节假日信息
- 查往年的日期,没找到,怎么判断?不能判断,直接抛出异常:日历未初始化,请先初始化xx年的节假日信息
- 查未来的日期,没找到,怎么判断?不能判断,直接抛出异常:日历未初始化,请先初始化xx年的节假日信息
缓存设计
- 数据结构:hash
- Key:sys_calendar:国家区域代码:年份
- HKey: yyyy-MM-dd (String)
- HVal: HolidayCacheDto (JSON)
- 注意:不要使用LocalDate作为Key,因为:Redis序列化时不支持LocalDate,需要额外的配置,可能与其他的序列化配置起冲突
方案评价
- 优势
- 记录全量日期,方便直接判断
- 可以表达调休工作日的信息
- 结论:推荐
第三方开源工具
https://github.com/NateScarlet/holiday-cn
主要思路
- 把所有的节假日都记录在一个json文件中,后续使用时解析
- 每年发布新的更新日期,需要手动升级依赖版本
- 自定义表并维护节假日更好
- <dependency>
- <groupId>com.github.stuxuhai</groupId>
- cn-holiday</artifactId>
- <version>1.4.0</version>
- </dependency>
复制代码 总结
工作日与休息日的判定,是一个系统的重要基础设施,一旦判定逻辑不严谨,就可能影响任务调度、消息推送、结算周期、SLA 计算等核心流程,甚至在节假日集中爆发问题,增加系统风险和运维成本。
因此,在系统设计阶段就明确工作日的判定规则,并将其作为一项基础能力进行统一建设,而不是在各个业务代码中零散实现,往往能带来更高的稳定性和可维护性。
本文提供了两种的技术方案
- 再次强调:纯代码方案:不可行。因为每年的节假日安排都不一样,受国务院管理(中国大陆,不含港澳台)
- 只记录休息日(周六周日、节假日):不建议使用
- 记录所有日期,通过type区分(工作日、周末、法定节假日、调休工作日):建议使用
项目Demo:https://github.com/HackyleShawe/JavaBackEndDemos/tree/master/BusinessCommonSolution/workday-holiday-demo
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |