一、主要功能總結

  • 自動獲取節假日:從 GitHub 開源項目 NateScarlet/holiday-cn 獲取權威節假日數據。
  • 智能合併:將法定節假日與週末合併,同時剔除調休補班日,生成準確的“放假日曆”。
  • 高可用設計:支持多數據源,失敗自動重試。
  • 定時執行:通過 @Scheduled 實現自動化任務。
  • 日誌記錄:使用 slf4j 記錄關鍵步驟和異常,便於排查問題。

二、代碼實現

package com.xfcy.service;

import cn.hutool.core.date.DateUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 節假日信息獲取與處理服務
 * 該類負責定時從外部數據源獲取指定年份的節假日安排(包括法定節假日、調休補班等),
 * 並結合週末信息,生成全年完整的“非工作日”列表(即放假日期)。
 *
 * 使用了 Spring 的定時任務功能,通過 HTTP 請求獲取 JSON 格式的節假日數據。
 */
@Configuration
@EnableScheduling  // 啓用 Spring 的定時任務支持
@Slf4j             // Lombok 註解,自動生成日誌記錄器(logger)
public class TaskPublicHoliday {

    /**
     * 獲取指定年份中所有的週六和週日日期列表
     *
     * @param year 目標年份
     * @return 包含所有周末日期的字符串列表,格式為 "yyyy-MM-dd"
     */
    public List<String> getYearAllWeekends(int year) {
        List<String> resultList = new ArrayList<>();
        SimpleDateFormat simdf = new SimpleDateFormat("yyyy-MM-dd");
        // 創建日曆實例,起始為指定年份的1月1日
        Calendar calendar = new GregorianCalendar(year, 1, 1); // 注意:月份從0開始,這裏1表示2月,但後續通過WEEK_OF_YEAR調整
        int i = 1;
        // 循環遍歷每年的每一週
        while (calendar.get(Calendar.YEAR) < year + 1) {
            calendar.set(Calendar.WEEK_OF_YEAR, i++); // 設置為第 i 周
            // 先獲取本週的星期日
            calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
            if (calendar.get(Calendar.YEAR) == year) {
                resultList.add(simdf.format(calendar.getTime()));
            }
            // 再獲取本週的星期六
            calendar.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
            if (calendar.get(Calendar.YEAR) == year) {
                resultList.add(simdf.format(calendar.getTime()));
            }
        }
        return resultList;
    }

    /**
     * 將指定日期轉換為中文星期幾的文本表示
     *
     * @param date 日期字符串,格式應為 "yyyy-MM-dd"
     * @return 對應的中文星期幾,如 "星期一"、"星期二" 等
     */
    public String getWeekNoText(String date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(DateUtil.parseDate(date)); // 使用 Hutool 工具類解析日期
        int wkDay = calendar.get(Calendar.DAY_OF_WEEK); // 獲取星期幾(1-7)
        switch (wkDay) {
            case Calendar.SUNDAY:
                return "星期日";
            case Calendar.MONDAY:
                return "星期一";
            case Calendar.TUESDAY:
                return "星期二";
            case Calendar.WEDNESDAY:
                return "星期三";
            case Calendar.THURSDAY:
                return "星期四";
            case Calendar.FRIDAY:
                return "星期五";
            case Calendar.SATURDAY:
                return "星期六";
            default:
                return "";
        }
    }

    /**
     * 定時任務:獲取下一年度的節假日安排並生成非工作日列表
     *
     * 原計劃每年12月30日凌晨2點執行,以便獲取次年的放假安排。
     * 當前為測試方便,設置為每1秒執行一次(fixedDelay = 1000 * 1)。
     *
     * 數據源來自 GitHub 上的開源項目,提供中國節假日 JSON 數據。
     */
    @Scheduled(cron = "0 0 2 30 12 *") // 原定計劃:每年12月30日 02:00:00 執行
//    @Scheduled(fixedDelay = 1000 * 1) // 測試用:每1秒執行一次
    public void getNextYearHolidays() {
        // 定義節假日數據源地址數組(支持多源,提高容錯性)
        String[] holidayInfoUrl = {
                "https://raw.githubusercontent.com/NateScarlet/holiday-cn/master/"
        };

        // 獲取當前年份
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        int year = calendar.get(Calendar.YEAR); // 當前年份(實際應為下一年,但此處為當前年用於測試)

        JSONArray holidayObjArry = null; // 存儲從接口獲取的節假日數據

        // 遍歷所有數據源,嘗試獲取節假日信息,直到成功
        for (int i = 0; i < holidayInfoUrl.length; i++) {
            String reqUrl = holidayInfoUrl[i] + year + ".json";
            try {
                log.info("開始獲取節日信息-" + i + ": url=" + reqUrl);
                // 發送 HTTP GET 請求獲取節假日 JSON 數據
                HttpResponse response = HttpRequest.get(reqUrl).execute();
                log.info("獲取節日信息返回" + i + ":" + response.body());

                // 解析返回的 JSON 響應
                JSONObject rtnObj = JSONUtil.parseObj(response.body());
                holidayObjArry = rtnObj.getJSONArray("days"); // 提取 "days" 數組

                // 成功獲取數據,跳出循環
                break;
            } catch (Exception e) {
                log.error("獲取節假日數據異常:", e);
                // 繼續嘗試下一個數據源
            }
        }

        // 解析獲取到的節假日數據
        List<String> notWorkDays = new ArrayList<>(); // 存儲法定節假日(放假日)
        List<String> workDays = new ArrayList<>();    // 存儲調休補班日(本應休息但需上班)

        if (holidayObjArry != null) {
            for (int i = 0; i < holidayObjArry.size(); i++) {
                JSONObject dayObj = holidayObjArry.getJSONObject(i);
                String date = dayObj.getStr("date"); // 日期,格式:yyyy-MM-dd

                // 只處理目標年份的數據,避免包含跨年數據
                if (date.startsWith(String.valueOf(year) + '-')) {
                    if (dayObj.getBool("isOffDay")) {
                        // 是休息日(放假)
                        notWorkDays.add(date);
                    } else {
                        // 不是休息日(即補班日)
                        workDays.add(date);
                    }
                }
            }
        }

        // 獲取該年份所有周末(週六、週日)的日期列表
        List<String> weekEndDays = getYearAllWeekends(year);

        // 從週末列表中剔除需要補班的日期(這些天實際要上班)
        Collection<String> realWeekEndDays = org.apache.commons.collections4.CollectionUtils.subtract(weekEndDays, workDays);
        List<String> realWeekEndDaysList = new ArrayList<>(realWeekEndDays);

        // 合併:法定節假日 + 實際週末 = 全年非工作日
        notWorkDays.addAll(realWeekEndDaysList);

        // 去重(可能存在節假日與週末重合的情況)
        Set<String> set = new LinkedHashSet<>(notWorkDays);
        List<String> notWorkDayList = new ArrayList<>(set);

        // 按日期排序(升序)
        Collections.sort(notWorkDayList);

        // 輸出最終的非工作日列表
        for (String oneDay : notWorkDayList) {
            try {
                Date date = DateUtils.parseDate(oneDay, "yyyy-MM-dd");
                log.info("今天不用上班: {}, 今天是{}", DateUtil.format(date, "yyyy-MM-dd"), getWeekNoText(oneDay));
            } catch (ParseException e) {
                log.error("日期解析失敗: " + oneDay, e);
                throw new RuntimeException(e);
            }
        }
    }
}