如何实现Java工作日与节假日判断

demongao
4
2025-08-27

一、业务场景与数据模型设计

在企业级应用中,工作日判断是考勤系统、金融结算、任务调度等场景的基础功能。核心需求包括:

  • 区分法定工作日、周末与节假日

  • 支持调休规则(如周末补班)

  • 处理特殊行业休息日(如银行系统特定休市日)

基础数据模型

建议采用数据库存储节假日数据,表结构设计:

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';


  1. 维护工具:开发后台管理页面,支持批量导入国务院放假通知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 扩展与测试方案

  1. 支持自定义休息日:

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);
    }
}
  1. 单元测试示例:

@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());
    }
}

四、性能优化与最佳实践

  1. 批量查询优化:

// 一次性查询多个日期,减少数据库交互
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()));
}
  1. 年度工作日计算:

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();
}
  1. 工作日期间计算:

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();
}

五、部署与维护建议

  1. 数据更新机制:

    • 每年12月前更新下一年节假日数据

    • 国务院放假通知发布后24小时内完成数据同步

    • 支持通过API自动同步(如监控gov.cn公告页面变化)

  2. 容灾方案:

    • 本地缓存+数据库双备份

    • 提供离线数据文件(CSV/Excel)用于应急恢复

    • 节假日数据变更时发送消息通知相关系统

  3. 扩展方向:

    • 支持多地区日历(如香港、澳门节假日)

    • 集成行业特殊休息日(如证券市场休市日)

    • 提供工作日计算API供外部系统调用

通过上述方案,可实现高效、可靠的工作日判断功能,满足企业级应用的各种复杂场景需求。实际应用中需根据业务规模和实时性要求选择合适的数据获取方式,并做好缓存与容灾设计。