📌 为什么要计算工作日后的日期?

在电商、金融、OA 系统开发中,我们经常需要计算若干个工作日(跳过周末和法定节假日)之后的日期。典型场景包括:

  • 工单 SLA:承诺 5 个工作日内响应。
  • 付款到期日:合同约定 3 个工作日内付款。
  • 定时任务调度:只允许在交易日执行。

但问题在于:

  • 周末固定(周六、日)容易处理。
  • 法定节假日每年不同,需查阅国务院公告。
  • 调休补班(如国庆前后周六上班)让简单的“周六日休息”规则失效。

因此,数据 + 算法缺一不可。本文提供一套基于 Java 8 java.time.LocalDate 的完整解决方案,核心算法采用“逐日递增 + 动态懒加载”策略,支持任意跨年(哪怕 workDays=300 也不怕),并附赠 DateLocalDate 互转工具,完美兼容老旧系统。


🧠 核心算法思路(简单粗暴,但有效)

算法本质是“一天一天地数”,直到数满指定的工作日数量。

判定逻辑(优先级从高到低)

对于每一天,我们根据它的“日期属性”判断是否为工作日:

日期类型是否为工作日?
周一 ~ 周五默认是工作日,但如果该日期在“法定节假日黑名单”中,则不是工作日。
周六 ~ 周日默认不是工作日,但如果该日期在“调休补班白名单”中,则是工作日(即调休上班)。

算法伪代码

输入:起始日期 startDate, 工作日数 workDays
输出:目标日期

current = startDate
已累计工作日 = 0

while (已累计工作日 < workDays) {
    current = current + 1天
    获取 current 的日期属性(是否周末、是否节假日、是否调休补班)
    if (current 是工作日) {
        已累计工作日 += 1
    }
}
return current

这个算法简单、准确,且逐天递增的性能开销完全可以接受(即使 workDays=10000,也只是万次循环)。


⚡ 核心优化:动态懒加载(彻底解决跨年)

很多初学者会在代码中写:

if (startDate.getMonth() == DECEMBER && startDate.getDay() > 20) {
    // 加载下一年的数据
}

这种写法有两大致命缺陷:

  1. 如果 workDays 较大(比如从 6 月 1 日开始计算 30 个工作日),最终日期可能跨到次年,但上述条件不触发。
  2. 如果 workDays 极大(例如 300),甚至会跨到第三年,只加载下一年显然不够。

正确做法在循环中按需加载

维护一个 Map<Integer, Set<String>> 作为缓存,当 current 进入一个新的年份时,立即加载该年的节假日/补班数据。这样无论 workDays 多大,都能自动加载所有涉及的年份。


🌐 方案一:对接第三方节假日 API(零运维,开箱即用)

对于初创项目或不想维护数据表的场景,直接使用免费 API 是最快捷的方式。
这里以 ApiZero 为例(无需注册,每日 1000 次免费调用):

GET https://v1.apizero.cn/api/holiday?year=2026

返回 JSON 示例(字段说明):

{
  "code": 200,
  "data": {
    "list": [
      {
        "holiday": "2026-10-01",           // 法定假日(单日)
        "vacation": "2026-10-01~2026-10-07", // 假期范围(或单日)
        "remark": "2026-10-10"              // 调休补班日期(周六/日上班)
      }
    ]
  }
}

1. Maven 依赖(用于解析 JSON)

<dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
    <version>20230227</version>
</dependency>

2. 完整 Java 实现(包含动态懒加载)

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;

/**
 * 工作日计算器(基于第三方 API,自动跨年)
 */
public class WorkDayCalculatorByAPI {

    private static final String API_URL = "https://v1.apizero.cn/api/holiday";
    private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    // 懒加载缓存:Key=年份, Value=该年的日期字符串集合(yyyy-MM-dd)
    private static final Map<Integer, Set<String>> HOLIDAY_CACHE = new HashMap<>();
    private static final Map<Integer, Set<String>> WORK_WEEKEND_CACHE = new HashMap<>();

    /**
     * 计算从 startDate 开始,经过 workDays 个工作日后的日期
     *
     * @param startDate 起始日期(当天不算,从第二天开始)
     * @param workDays  需要累计的工作日数(正数)
     * @return 目标日期
     */
    public static LocalDate getDateAfterWorkDays(LocalDate startDate, int workDays) throws Exception {
        LocalDate current = startDate;
        int added = 0;

        while (added < workDays) {
            current = current.plusDays(1);
            int year = current.getYear();

            // 动态懒加载:当年数据尚未加载,则调用 API 拉取并放入缓存
            if (!HOLIDAY_CACHE.containsKey(year)) {
                HOLIDAY_CACHE.put(year, fetchHolidays(year));
                WORK_WEEKEND_CACHE.put(year, fetchWorkWeekends(year));
            }

            String dateStr = current.format(FMT);
            boolean isWeekend = current.getDayOfWeek().getValue() >= 6; // 6=周六, 7=周日
            boolean isHoliday = HOLIDAY_CACHE.get(year).contains(dateStr);
            boolean isWorkWeekend = WORK_WEEKEND_CACHE.get(year).contains(dateStr);

            // 核心判定:周末仅在补班时算工作日,平日仅在不属于节假日时算工作日
            boolean isWorkDay = isWeekend ? isWorkWeekend : !isHoliday;

            if (isWorkDay) {
                added++;
            }
        }
        return current;
    }

    // ------------------- API 交互 -------------------

    /**
     * 获取指定年份的法定节假日日期集合
     */
    private static Set<String> fetchHolidays(int year) throws Exception {
        JSONObject data = fetchApiData(year);
        Set<String> holidays = new HashSet<>();
        JSONArray list = data.optJSONArray("list");
        if (list == null) return holidays;

        for (int i = 0; i < list.length(); i++) {
            JSONObject item = list.getJSONObject(i);
            // 有些 API 用 holiday 字段,有些用 vacation,都取并集
            String holiday = item.optString("holiday");
            String vacation = item.optString("vacation");
            if (holiday != null && !holiday.isEmpty()) {
                holidays.addAll(expandDateRange(holiday));
            }
            if (vacation != null && !vacation.isEmpty()) {
                holidays.addAll(expandDateRange(vacation));
            }
        }
        return holidays;
    }

    /**
     * 获取指定年份的调休补班日期集合(周六日上班)
     */
    private static Set<String> fetchWorkWeekends(int year) throws Exception {
        JSONObject data = fetchApiData(year);
        Set<String> workWeekends = new HashSet<>();
        JSONArray list = data.optJSONArray("list");
        if (list == null) return workWeekends;

        for (int i = 0; i < list.length(); i++) {
            JSONObject item = list.getJSONObject(i);
            // ApiZero 的补班字段为 remark,可能包含多个逗号分隔的日期
            String remark = item.optString("remark");
            if (remark != null && !remark.isEmpty()) {
                for (String d : remark.split(",")) {
                    String trimmed = d.trim();
                    if (!trimmed.isEmpty()) {
                        workWeekends.add(trimmed);
                    }
                }
            }
        }
        return workWeekends;
    }

    /**
     * 调用 API 获取原始 JSON 数据
     */
    private static JSONObject fetchApiData(int year) throws Exception {
        String urlStr = API_URL + "?year=" + year;
        HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection();
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(5000);
        conn.setReadTimeout(5000);

        int code = conn.getResponseCode();
        if (code != 200) {
            throw new RuntimeException("API 调用失败,HTTP 状态码:" + code);
        }

        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            JSONObject json = new JSONObject(sb.toString());
            if (json.optInt("code") != 200) {
                throw new RuntimeException("API 返回错误:" + json.optString("message"));
            }
            return json.getJSONObject("data");
        } finally {
            conn.disconnect();
        }
    }

    /**
     * 展开日期范围,例如 "2026-10-01~2026-10-07" 拆成每一天
     */
    private static Set<String> expandDateRange(String range) {
        Set<String> dates = new HashSet<>();
        if (range == null || range.isEmpty()) return dates;

        if (range.contains("~")) {
            String[] parts = range.split("~");
            if (parts.length == 2) {
                try {
                    LocalDate start = LocalDate.parse(parts[0].trim(), FMT);
                    LocalDate end = LocalDate.parse(parts[1].trim(), FMT);
                    while (!start.isAfter(end)) {
                        dates.add(start.format(FMT));
                        start = start.plusDays(1);
                    }
                } catch (Exception ignored) {
                    // 格式异常则忽略
                }
            }
        } else {
            dates.add(range);
        }
        return dates;
    }

    // ---------- 测试 ----------
    public static void main(String[] args) throws Exception {
        LocalDate start = LocalDate.of(2026, 6, 1);
        int workDays = 30;
        LocalDate result = getDateAfterWorkDays(start, workDays);
        System.out.println("从 " + start + " 起 " + workDays + " 个工作日后:" + result);
        // 无论跨多少年,都会自动加载对应年份的数据
    }
}

注意workDays=300 虽然理论上可行,但现实中国家只公布未来一年的节假日安排,如果跨年太远(例如 2026 年要跨到 2027 年底),API 可能没有数据,此时需要人工介入或使用数据库方案。


🗄️ 方案二:自建数据库表(企业级推荐)

对于金融、政务等对数据主权和稳定性要求极高的场景,建议将节假日数据存储在自己的数据库中,每年根据国务院公告手动维护。

1. MySQL 表结构设计

CREATE TABLE `holiday` (
    `id` INT PRIMARY KEY AUTO_INCREMENT,
    `date` DATE NOT NULL COMMENT '日期(yyyy-MM-dd)',
    `is_work_day` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0=休息日(法定节假日),1=工作日(调休补班)',
    `year` INT NOT NULL COMMENT '年份(便于索引)',
    `description` VARCHAR(100) DEFAULT NULL COMMENT '描述,例如“国庆节”、“调休补班”',
    UNIQUE KEY `uk_date` (`date`),
    KEY `idx_year` (`year`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 示例数据(2026 年部分)

dateis_work_daydescription
2026-01-010元旦
2026-01-020元旦调休
2026-01-030元旦调休
2026-02-150春节
2026-02-160春节调休
2026-10-010国庆节
2026-10-020国庆调休
2026-10-070国庆调休
2026-10-101调休补班(周六上班)

特别说明

  • 对于普通的周一至周五,无需插入任何记录,系统默认它们为工作日。
  • 仅需插入两类“例外”:法定节假日is_work_day=0)和调休补班is_work_day=1,通常是周六日)。

3. Java 访问数据库(只写逻辑,不写具体连接代码)

在 Service 层,我们可以这样设计(省略 JDBC 细节,只展示核心流程):

public class WorkDayCalculatorByDB {

    // 同样使用懒加载缓存
    private static final Map<Integer, Set<String>> HOLIDAY_CACHE = new HashMap<>();
    private static final Map<Integer, Set<String>> WORK_WEEKEND_CACHE = new HashMap<>();

    public static LocalDate getDateAfterWorkDays(LocalDate startDate, int workDays) {
        LocalDate current = startDate;
        int added = 0;

        while (added < workDays) {
            current = current.plusDays(1);
            int year = current.getYear();

            // 如果缓存中没有该年的数据,则从数据库加载
            if (!HOLIDAY_CACHE.containsKey(year)) {
                HOLIDAY_CACHE.put(year, loadHolidaysFromDB(year));
                WORK_WEEKEND_CACHE.put(year, loadWorkWeekendsFromDB(year));
            }

            String dateStr = current.format(DateTimeFormatter.ISO_LOCAL_DATE);
            boolean isWeekend = current.getDayOfWeek().getValue() >= 6;
            boolean isHoliday = HOLIDAY_CACHE.get(year).contains(dateStr);
            boolean isWorkWeekend = WORK_WEEKEND_CACHE.get(year).contains(dateStr);

            if (isWeekend ? isWorkWeekend : !isHoliday) {
                added++;
            }
        }
        return current;
    }

    // 从数据库加载指定年份的节假日(is_work_day=0)
    private static Set<String> loadHolidaysFromDB(int year) {
        // 执行 SQL: SELECT date FROM holiday WHERE year = ? AND is_work_day = 0
        // 将结果集转为 Set<String> 并返回
        return new HashSet<>(); // 伪代码
    }

    // 从数据库加载指定年份的调休补班(is_work_day=1)
    private static Set<String> loadWorkWeekendsFromDB(int year) {
        // 执行 SQL: SELECT date FROM holiday WHERE year = ? AND is_work_day = 1
        return new HashSet<>(); // 伪代码
    }
}

使用建议

  • 每年 11~12 月国务院公布次年节假日安排后,通过数据导入工具(如 Excel 导入或脚本)将数据插入 holiday 表。
  • 应用启动时可将全量数据加载到内存缓存中,避免每次计算都查询数据库(数据量极小,每年不到 50 条)。

🔄 Date ↔ LocalDate 互转工具(兼容老旧系统)

如果你的项目仍在使用 java.util.Date,可以封装以下工具类无缝集成:

import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;

public class DateConverter {

    /**
     * java.util.Date → java.time.LocalDate
     * 使用系统默认时区,如需指定时区可传入 ZoneId.of("Asia/Shanghai")
     */
    public static LocalDate toLocalDate(Date date) {
        return Instant.ofEpochMilli(date.getTime())
                .atZone(ZoneId.systemDefault())
                .toLocalDate();
    }

    /**
     * java.time.LocalDate → java.util.Date
     */
    public static Date toDate(LocalDate localDate) {
        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
}

使用示例

Date oldStart = new Date();
LocalDate startLocal = DateConverter.toLocalDate(oldStart);
LocalDate resultLocal = WorkDayCalculatorByAPI.getDateAfterWorkDays(startLocal, 5);
Date resultDate = DateConverter.toDate(resultLocal);

📝 总结与最佳实践

关键点建议
跨年处理务必使用本文的动态懒加载,在循环中按需加载年份数据,杜绝硬编码判断。
数据源选择互联网项目可用 API(零维护),企业内网或对数据可控性要求高的项目必须用数据库。
缓存策略节假日数据极少,建议使用 HashMap 做 JVM 内存缓存,避免重复调用 API 或查询数据库。
数据更新API 方案无需维护;数据库方案需每年根据国务院公告导入数据,并注意调休补班(标记 is_work_day=1)。
年份超限问题workDays 过大(如 300),可能跨到未发布数据的未来年份。此时 API 可能返回空数据,数据库也可能无记录。建议业务上限制最大计算范围,或返回错误提示。
性能逐日循环在 workDays 为几百时性能无影响,若达到数万次循环也仅需毫秒级,无需过度优化。

希望这篇博客能帮助你彻底解决“工作日计算”这一经典业务难题。 如果你有更好的思路或疑问,欢迎在评论区交流!


发布日期:2026-08-07

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐