一、业务场景与数据模型设计
在企业级应用中,工作日判断是考勤系统、金融结算、任务调度等场景的基础功能。核心需求包括:
区分法定工作日、周末与节假日
支持调休规则(如周末补班)
处理特殊行业休息日(如银行系统特定休市日)
基础数据模型
建议采用数据库存储节假日数据,表结构设计:
CREATE TABLE `work_calendar` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`calendar_date` date NOT NULL COMMENT '日期',
`work_type` tinyint(4) NOT NULL COMMENT '工作类型:0-工作日 1-周末 2-法定节假日 3-调休工作日',
`holiday_name` varchar(50) DEFAULT NULL COMMENT '节假日名称',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`calendar_date`),
KEY `idx_work_type` (`work_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作日历表';
work_type
枚举值设计覆盖四种场景,支持调休等复杂规则。
二、数据获取与维护方案
2.1 手动维护方案
适用于中小型企业或对实时性要求不高的场景:
初始化基础数据
-- 插入2024年元旦假期
INSERT INTO `work_calendar` (`calendar_date`, `work_type`, `holiday_name`)
VALUES
('2024-01-01', 2, '元旦'),
('2024-01-02', 2, '元旦调休'),
('2024-01-06', 3, '周六补班');
-- 插入周末数据(批量生成)
INSERT INTO `work_calendar` (`calendar_date`, `work_type`)
SELECT DATE_ADD('2024-01-01', INTERVAL (t4.i*1000 + t3.i*100 + t2.i*10 + t1.i) DAY) AS date, 1
FROM (SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
(SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
(SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,
(SELECT 0 i UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t4
WHERE WEEKDAY(DATE_ADD('2024-01-01', INTERVAL (t4.i*1000 + t3.i*100 + t2.i*10 + t1.i) DAY)) IN (5, 6)
AND DATE_ADD('2024-01-01', INTERVAL (t4.i*1000 + t3.i*100 + t2.i*10 + t1.i) DAY) BETWEEN '2024-01-01' AND '2024-12-31';
维护工具:开发后台管理页面,支持批量导入国务院放假通知Excel文件
2.2 第三方接口方案
适用于需要实时更新的场景,以聚合数据API为例:
// 调用聚合数据节假日API
public class HolidayApiClient {
private static final String API_KEY = "your_api_key";
private static final String API_URL = "http://v.juhe.cn/calendar/day";
public HolidayResponse getHolidayInfo(String date) throws IOException {
String url = API_URL + "?date=" + date + "&key=" + API_KEY;
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
if (conn.getResponseCode() == 200) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String response = reader.lines().collect(Collectors.joining());
returnnew ObjectMapper().readValue(response, HolidayResponse.class);
}
}
thrownew ApiException("API调用失败,状态码:" + conn.getResponseCode());
}
// 响应数据模型
static class HolidayResponse {
private int error_code;
private String reason;
private Result result;
static class Result {
private String date;
private int type; // 0-工作日 1-周末 2-节假日
private String holiday;
// getter/setter省略
}
}
}
注意事项:
免费接口通常有调用频率限制(如100次/天)
需处理API不可用场景的降级方案(如使用本地缓存)
2.3 开源数据方案
推荐使用cn-holiday
库,基于国务院公告维护:
<dependency>
<groupId>com.github.stuxuhai</groupId>
<artifactId>cn-holiday</artifactId>
<version>1.4.0</version>
</dependency>
import com.github.stuxuhai.jpinyin.HolidayUtil;
public class WorkDayChecker {
// 判断是否为工作日
public boolean isWorkDay(LocalDate date) {
// 先判断是否为周末
if (date.getDayOfWeek() == DayOfWeek.SATURDAY ||
date.getDayOfWeek() == DayOfWeek.SUNDAY) {
returnfalse;
}
// 再判断是否为调休工作日或法定节假日
int year = date.getYear();
int month = date.getMonthValue();
int day = date.getDayOfMonth();
// cn-holiday库返回1-节假日 0-工作日 2-调休工作日
int holidayType = HolidayUtil.isHoliday(year, month, day);
return holidayType == 2 || holidayType == 0;
}
}
三、工程化实现方案
3.1 缓存优化策略
为提升高频调用场景性能,建议添加缓存:
@Service
public class WorkDayService {
@Autowired
private WorkCalendarRepository repository;
// 基于Caffeine的本地缓存
private final LoadingCache<LocalDate, WorkDayInfo> cache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(24, TimeUnit.HOURS)
.build(this::loadWorkDayInfo);
private WorkDayInfo loadWorkDayInfo(LocalDate date) {
return repository.findByDate(date)
.orElseGet(() -> createDefaultWorkDayInfo(date));
}
// 判断是否为工作日
public boolean isWorkDay(LocalDate date) {
WorkDayInfo info = cache.get(date);
return info.getWorkType() == 0 || info.getWorkType() == 3;
}
// 批量获取工作日
public List<LocalDate> getWorkDays(LocalDate start, LocalDate end) {
return Stream.iterate(start, d -> d.isBefore(end), LocalDate::plusDays)
.filter(this::isWorkDay)
.collect(Collectors.toList());
}
}
3.2 分布式场景方案
在微服务架构中,建议采用Redis分布式缓存:
@Service
public class DistributedWorkDayService {
@Autowired
private RedisTemplate<String, WorkDayInfo> redisTemplate;
@Autowired
private WorkCalendarRepository repository;
private static final String CACHE_KEY_PREFIX = "work_day:";
private static final Duration CACHE_TTL = Duration.ofHours(24);
public boolean isWorkDay(LocalDate date) {
String key = CACHE_KEY_PREFIX + date.toString();
WorkDayInfo info = redisTemplate.opsForValue().get(key);
if (info == null) {
info = repository.findByDate(date)
.orElseGet(() -> createDefaultWorkDayInfo(date));
redisTemplate.opsForValue().set(key, info, CACHE_TTL);
}
return info.getWorkType() == 0 || info.getWorkType() == 3;
}
// 刷新缓存(可配合消息队列实现分布式通知)
public void refreshCache(LocalDate date) {
String key = CACHE_KEY_PREFIX + date.toString();
WorkDayInfo info = repository.findByDate(date)
.orElseGet(() -> createDefaultWorkDayInfo(date));
redisTemplate.opsForValue().set(key, info, CACHE_TTL);
}
}
3.3 扩展与测试方案
支持自定义休息日:
public class CustomWorkDayChecker extends WorkDayChecker {
private final Set<LocalDate> customHolidays;
private final Set<LocalDate> customWorkDays;
public CustomWorkDayChecker(Set<LocalDate> customHolidays,
Set<LocalDate> customWorkDays) {
this.customHolidays = customHolidays;
this.customWorkDays = customWorkDays;
}
@Override
public boolean isWorkDay(LocalDate date) {
if (customHolidays.contains(date)) {
returnfalse;
}
if (customWorkDays.contains(date)) {
returntrue;
}
returnsuper.isWorkDay(date);
}
}
单元测试示例:
@SpringBootTest
class WorkDayServiceTest {
@Autowired
private WorkDayService service;
@Test
void testIsWorkDay() {
// 元旦(节假日)
assertFalse(service.isWorkDay(LocalDate.of(2024, 1, 1)));
// 周六补班(调休工作日)
assertTrue(service.isWorkDay(LocalDate.of(2024, 1, 6)));
// 普通工作日
assertTrue(service.isWorkDay(LocalDate.of(2024, 1, 2)));
}
@Test
void testGetWorkDays() {
LocalDate start = LocalDate.of(2024, 1, 1);
LocalDate end = LocalDate.of(2024, 1, 10);
List<LocalDate> workDays = service.getWorkDays(start, end);
// 预期1月1日-3日为假期,6日补班,共7个工作日
assertEquals(7, workDays.size());
}
}
四、性能优化与最佳实践
批量查询优化:
// 一次性查询多个日期,减少数据库交互
public Map<LocalDate, WorkDayInfo> batchQueryWorkDays(List<LocalDate> dates) {
if (dates.isEmpty()) {
return Collections.emptyMap();
}
String datePattern = dates.stream()
.map(LocalDate::toString)
.collect(Collectors.joining("','", "('", "')"));
String sql = "SELECT calendar_date, work_type, holiday_name " +
"FROM work_calendar WHERE calendar_date IN " + datePattern;
List<WorkDayInfo> result = jdbcTemplate.query(sql, (rs, rowNum) -> {
WorkDayInfo info = new WorkDayInfo();
info.setDate(rs.getDate("calendar_date").toLocalDate());
info.setWorkType(rs.getInt("work_type"));
info.setHolidayName(rs.getString("holiday_name"));
return info;
});
return result.stream()
.collect(Collectors.toMap(WorkDayInfo::getDate, Function.identity()));
}
年度工作日计算:
public int countWorkDaysInYear(int year) {
LocalDate start = LocalDate.of(year, 1, 1);
LocalDate end = LocalDate.of(year, 12, 31);
return (int) Stream.iterate(start, d -> d.isBefore(end), LocalDate::plusDays)
.filter(this::isWorkDay)
.count();
}
工作日期间计算:
public long getWorkDayInterval(LocalDate start, LocalDate end) {
if (start.isAfter(end)) {
throw new IllegalArgumentException("开始日期不能晚于结束日期");
}
return Stream.iterate(start, d -> d.isBefore(end), LocalDate::plusDays)
.filter(this::isWorkDay)
.count();
}
五、部署与维护建议
数据更新机制:
每年12月前更新下一年节假日数据
国务院放假通知发布后24小时内完成数据同步
支持通过API自动同步(如监控gov.cn公告页面变化)
容灾方案:
本地缓存+数据库双备份
提供离线数据文件(CSV/Excel)用于应急恢复
节假日数据变更时发送消息通知相关系统
扩展方向:
支持多地区日历(如香港、澳门节假日)
集成行业特殊休息日(如证券市场休市日)
提供工作日计算API供外部系统调用
通过上述方案,可实现高效、可靠的工作日判断功能,满足企业级应用的各种复杂场景需求。实际应用中需根据业务规模和实时性要求选择合适的数据获取方式,并做好缓存与容灾设计。