在企业级系统开发中,节假日逻辑是个常见但容易 “埋坑” 的需求 —— 比如印章预约、考勤打卡、工单截止日期计算等场景,都需要准确识别节假日。如果硬编码节假日(if (date == "2025-01-01")),每年调休都要改代码、重启服务,维护成本极高。

本文就以 “印章预约系统” 为例,手把手教你用 SpringBoot 对接阿里云节假日 API,实现节假日自动同步、库存动态适配,彻底告别硬编码!

一、前言:为什么要对接阿里云 API?

手动维护节假日的痛点:

  • 规则僵化:每年法定节假日 + 调休变化,需手动修改代码;
  • 易出错:漏改调休日期会导致系统逻辑错误(比如周末调休上班却不让预约);
  • 效率低:每次更新都要发版,影响系统可用性。

对接阿里云 API 的优势:

  • 实时同步:API 会实时更新官方节假日 / 调休规则,无需人工干预;
  • 稳定可靠:阿里云提供高可用服务,支持每秒百级调用;
  • 低成本:基础版 API 单次调用几分钱,中小系统年成本不足 100 元。

二、准备工作:阿里云 API 开通与配置

1. 开通阿里云节假日 API

  1. 第一步:登录 阿里云控制台,在顶部搜索 “万年历查询” 或 “节假日查询”,选择第三方 API 服务商(推荐 “易源数据”“极速数据”,文档清晰、性价比高)。
  2. 第二步:选择 “免费试用” 或 “按量付费”(新手建议先试用),点击 “立即购买”,完成开通。
  3. 第三步:获取 APPCODE(API 认证关键):
    开通后进入「我的 API--→对应 API 详情页--→调用配置--→复制APPCODE」( 后续项目中要用)。

2. 查看 API 文档,明确请求 / 响应格式

以 “易源数据 - 万年历查询 API” 为例,核心信息如下(不同服务商格式类似,需以实际文档为准):

  • 请求地址:https://ali-wannianli.showapi.com/showapi_wannianli
  • 请求方式:POST
  • 请求参数:
参数名说明示例值
date单个日期查询(可选)2025-01-01
startDate开始日期(批量查询)2025-01-01
endDate结束日期(批量查询)2025-03-31
  • 响应格式(JSON):
{
"showapi_res_code": 0,    // 0=成功,非0=失败
"showapi_res_error": "",   // 错误信息
"showapi_res_body": {
"list": [                // 日期列表
{
"date": "2025-01-01",// 日期
"isHoliday": "1",    // 是否节假日:1=是,0=否
"holidayName": "元旦",// 节假日名称
"isWorkday": "0"     // 是否调休上班:1=是,0=否
},
// ... 更多日期
]
}
}

三、项目实战:SpringBoot 对接 API 全流程

1. 项目环境准备

(1)核心依赖(pom.xml)

需引入 spring-boot-starter-web(HTTP 调用)、jackson-databind(JSON 解析)、mybatis-spring-boot-starter(数据库操作):

<dependencies>
  <!-- SpringBoot Web(含RestTemplate) -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- MySQL + MyBatis -->
      <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.32</version>
      </dependency>
      <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.3.0</version>
      </dependency>
      <!-- Redis 缓存 -->
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId> <!-- Redis连接池 -->
        </dependency>
        <!-- JSON解析(Jackson) -->
          <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          </dependency>
          <!-- Lombok(简化实体类) -->
            <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
            </dependency>
          </dependencies>

(2)配置文件(application.yml)

将 API 地址、APPCODE、同步天数配置化(避免硬编码):

# 服务端口
server:
port: 8080
spring:
# 数据库配置(存储节假日原始数据)
datasource:
url: jdbc:mysql://localhost:3306/seal_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root          # 你的MySQL用户名
password: 123456        # 你的MySQL密码
driver-class-name: com.mysql.cj.jdbc.Driver
# Redis配置(缓存可预约日期)
redis:
host: localhost         # Redis地址(本地环境)
port: 6379              # Redis默认端口
password:               # Redis密码(无则留空)
database: 0             # Redis数据库编号(默认0)
lettuce:
pool:
max-active: 8       # 最大连接数
max-idle: 8         # 最大空闲连接
min-idle: 2         # 最小空闲连接
max-wait: 1000ms    # 连接等待时间
# 阿里云节假日API配置
aliyun:
holiday:
api-url: "https://ali-wannianli.showapi.com/showapi_wannianli"  # 你的API地址
app-code: "abcdef1234567890"                                  # 你的APPCODE
sync-days: 90                                                  # 同步未来90天数据
# MyBatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml          # Mapper XML路径
type-aliases-package: com.seal.entity             # 实体类包(简化XML)
configuration:
map-underscore-to-camel-case: true              # 下划线转驼峰(如is_work→isWork)

2. 数据库设计(sys_holiday 表)

存储 API 同步的节假日数据,字段与 API 响应对齐:

字段名类型允许空默认值主键/索引说明
idbigint(20)NO-PRIMARY主键ID
datedateNO-UNIQUE日期(yyyy-MM-dd)
is_holidaytinyint(1)NO0-是否节假日(0-否,1-是)
holiday_namevarchar(50)YES‘’-节假日名称(非节假日为空)
is_worktinyint(1)NO0-是否调休上班(0-否,1-是)

3. 核心代码实现

4.基础工具类

(1)日期工具(DateUtils.java)

处理日期格式转换与未来日期计算:

package com.seal.utils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/**
* 日期工具类:统一日期处理逻辑
*/
public class DateUtils {
// 标准日期格式(与API、数据库对齐)
public static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");
/**
* 日期转字符串(yyyy-MM-dd)
*/
public static String date2Str(Date date) {
return date != null ? SDF.format(date) : null;
}
/**
* 字符串转日期(yyyy-MM-dd)
*/
public static Date str2Date(String dateStr) throws ParseException {
return dateStr != null ? SDF.parse(dateStr) : null;
}
/**
* 获取未来N天的日期
*/
public static Date getFutureDate(int days) {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_YEAR, days);
return cal.getTime();
}
}
/**
* 工具方法:检查单个日期是否可预约
* @param dateStr 日期字符串(yyyy-MM-dd)
* @return true=可预约,false=不可预约
*/
private boolean isDateAvailable(String dateStr) {
// 从数据库查询该日期的信息
SysHoliday holiday = holidayMapper.selectByDate(dateStr); // 复用之前的按日期查询方法
if (holiday == null) {
// 数据库中无该日期数据(可能未同步到),默认视为可预约(或按业务规则改为false)
return true;
}
// 有数据:按规则判断(非节假日 或 调休上班)
return holiday.getIsHoliday() == 0 || holiday.getIsWork() == 1;
}

(2)统一返回结果(Result.java)

规范接口响应格式,便于前端处理:

package com.seal.utils;
import lombok.Data;
/**
* 接口统一返回工具类
*/
@Data
public class Result<T> {
  private Integer code;  // 状态码:200=成功,500=失败
  private String msg;    // 提示信息
  private T data;        // 返回数据
  // 成功(带数据)
  public static <T> Result<T> success(T data) {
    Result<T> result = new Result<>();
      result.setCode(200);
      result.setMsg("操作成功");
      result.setData(data);
      return result;
      }
      // 失败(带消息)
      public static <T> Result<T> fail(String msg) {
        Result<T> result = new Result<>();
          result.setCode(500);
          result.setMsg(msg);
          return result;
          }
          }

(3) Redis 配置(解决序列化问题)

默认 RedisTemplate 会导致 Key 带乱码,自定义配置确保 Key 清晰、Value 可解析:

package com.seal.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置:优化序列化,避免Key乱码
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
  RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(factory);
    // 1. Key 序列化:String格式(无乱码)
    StringRedisSerializer stringSerializer = new StringRedisSerializer();
    template.setKeySerializer(stringSerializer);
    template.setHashKeySerializer(stringSerializer);
    // 2. Value 序列化:JSON格式(支持对象存储,可读性高)
    GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
    template.setValueSerializer(jsonSerializer);
    template.setHashValueSerializer(jsonSerializer);
    template.afterPropertiesSet();
    return template;
    }
    }

5.实体类(SysHoliday.java)

与数据库表、API 响应字段对应:

package com.seal.entity;
import lombok.Data;
import java.util.Date;
/**
* 节假日实体类
*/
@Data
public class SysHoliday {
private Long id;                // 主键ID
private Date date;              // 日期
private Integer isHoliday;      // 是否节假日(0-否,1-是)
private String holidayName;     // 节假日名称
private Integer isWork;         // 是否调休上班(0-否,1-是)
}

6.Mapper 层(SysHolidayMapper.java + XML)

负责数据库操作:批量插入、按日期范围删除(同步前删旧数据)、查询所有节假日。
SysHolidayMapper.java:

package com.seal.mapper;
import com.seal.entity.SysHoliday;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 节假日Mapper:操作sys_holiday表
*/
@Mapper
public interface SysHolidayMapper {
/**
* 批量插入/更新:存在则更新,不存在则插入(基于date唯一索引)
*/
void batchUpsert(@Param("list") List<SysHoliday> holidayList);
  /**
  * 按日期范围删除旧数据:同步前清理,避免重复
  */
  void deleteByDateRange(@Param("startDate") String startDate, @Param("endDate") String endDate);
  /**
  * 查询所有节假日数据:用于生成可预约日期
  */
  List<SysHoliday> selectAll();
    }

SysHolidayMapper.xml(放在 resources/mapper 目录下):

<?xml version="1.0" encoding="UTF-8"?>
  <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.seal.mapper.SysHolidayMapper">
      <!-- 批量插入/更新节假日 -->
          <insert id="batchUpsert">
          INSERT INTO sys_holiday (date, is_holiday, holiday_name, is_work)
          VALUES
            <foreach collection="list" item="item" separator=",">
            (#{item.date}, #{item.isHoliday}, #{item.holidayName}, #{item.isWork})
          </foreach>
          ON DUPLICATE KEY UPDATE
          is_holiday = VALUES(is_holiday),
          holiday_name = VALUES(holiday_name),
          is_work = VALUES(is_work)
        </insert>
        <!-- 按日期范围删除旧数据 -->
            <delete id="deleteByDateRange">
            DELETE FROM sys_holiday
            WHERE date BETWEEN #{startDate} AND #{endDate}
          </delete>
          <!-- 查询所有节假日 -->
              <select id="selectAll" resultType="com.seal.entity.SysHoliday">
              SELECT id, date, is_holiday, holiday_name, is_work
              FROM sys_holiday
              ORDER BY date ASC
            </select>
          </mapper>

7.Service 层(核心业务逻辑)

整合 API 调用、数据库同步、Redis 缓存,是方案的核心:

package com.seal.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.seal.entity.SysHoliday;
import com.seal.mapper.SysHolidayMapper;
import com.seal.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 节假日服务:API同步 + 缓存管理 + 可预约日期计算
*/
@Service
public class SysHolidayService {
// ------------------- 缓存配置 -------------------
private static final String AVAILABLE_DATE_CACHE_KEY = "seal:holiday:available"; // 可预约日期缓存Key
private static final long CACHE_EXPIRE_DAYS = 1; // 缓存过期时间(24小时)
// ------------------- 阿里云API配置 -------------------
@Value("${aliyun.holiday.api-url}")
private String apiUrl;
@Value("${aliyun.holiday.app-code}")
private String appCode;
@Value("${aliyun.holiday.sync-days}")
private int syncDays;
// ------------------- 依赖注入 -------------------
@Autowired
private SysHolidayMapper holidayMapper;
@Autowired
private RestTemplate restTemplate; // HTTP工具(调用API)
@Autowired
private RedisTemplate<String, Object> redisTemplate; // Redis缓存工具
  private final ObjectMapper objectMapper = new ObjectMapper(); // JSON解析工具
  /**
  * 核心方法:调用阿里云API,同步节假日到数据库+更新缓存
  */
  @Transactional(rollbackFor = Exception.class) // 事务:同步失败全回滚
  public void syncHolidayFromAliyun() {
  // 1. 计算同步日期范围:今天 → 未来N天
  String startDate = DateUtils.date2Str(new Date());
  String endDate = DateUtils.date2Str(DateUtils.getFutureDate(syncDays));
  System.out.println("=== 开始同步阿里云节假日:" + startDate + " ~ " + endDate + " ===");
  // 2. 构建API请求(含APPCODE认证)
  HttpHeaders headers = new HttpHeaders();
  headers.set("Authorization", "APPCODE " + appCode); // 阿里云固定认证格式
  headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); // 表单提交
  String params = String.format("startDate=%s&endDate=%s", startDate, endDate);
  HttpEntity<String> requestEntity = new HttpEntity<>(params, headers);
    // 3. 发起API请求并解析响应
    JsonNode responseNode = null;
    try {
    ResponseEntity<String> response = restTemplate.exchange(
      apiUrl,
      HttpMethod.POST,
      requestEntity,
      String.class
      );
      // 校验HTTP响应状态
      if (response.getStatusCode() != HttpStatus.OK) {
      throw new RuntimeException("API请求失败,HTTP状态码:" + response.getStatusCode());
      }
      // 解析JSON响应(按API文档格式)
      responseNode = objectMapper.readTree(response.getBody());
      } catch (HttpClientErrorException e) {
      // 客户端错误(4xx:APPCODE无效、参数错误)
      throw new RuntimeException("API客户端错误:" + e.getResponseBodyAsString(), e);
      } catch (Exception e) {
      // 其他错误(网络超时、JSON解析失败)
      throw new RuntimeException("API调用异常:" + e.getMessage(), e);
      }
      // 4. 校验API业务状态(按文档:0=成功)
      int resCode = responseNode.get("showapi_res_code").asInt();
      String resMsg = responseNode.get("showapi_res_error").asText();
      if (resCode != 0) {
      throw new RuntimeException("API业务失败:code=" + resCode + ", msg=" + resMsg);
      }
      // 5. 提取节假日数据,映射为实体类
      JsonNode dataNode = responseNode.get("showapi_res_body");
      JsonNode holidayListNode = dataNode.get("list");
      if (holidayListNode == null || !holidayListNode.isArray()) {
      throw new RuntimeException("API返回格式异常:无节假日列表");
      }
      List<SysHoliday> holidayList = new ArrayList<>();
        for (JsonNode node : holidayListNode) {
        SysHoliday holiday = new SysHoliday();
        // 日期:API字符串 → Date
        try {
        holiday.setDate(DateUtils.str2Date(node.get("date").asText()));
        } catch (Exception e) {
        throw new RuntimeException("日期解析失败:" + node.get("date").asText(), e);
        }
        // 是否节假日:API字符串 → Integer
        holiday.setIsHoliday(Integer.parseInt(node.get("isHoliday").asText()));
        // 节假日名称
        holiday.setHolidayName(node.get("holidayName").asText());
        // 是否调休上班:API字符串 → Integer
        holiday.setIsWork(Integer.parseInt(node.get("isWorkday").asText()));
        holidayList.add(holiday);
        }
        // 6. 同步到数据库(先删旧数据,再插新数据)
        if (holidayList.isEmpty()) {
        System.err.println("API返回节假日列表为空,同步终止");
        return;
        }
        holidayMapper.deleteByDateRange(startDate, endDate); // 清理旧数据
        holidayMapper.batchUpsert(holidayList); // 插入新数据
        System.out.println("数据库同步完成,共" + holidayList.size() + "条数据");
        // 7. 更新Redis缓存(可预约日期)
        List<String> availableDates = getAvailableDatesFromDb();
          redisTemplate.opsForValue().set(
          AVAILABLE_DATE_CACHE_KEY,
          availableDates,
          CACHE_EXPIRE_DAYS,
          TimeUnit.DAYS
          );
          System.out.println("Redis缓存更新完成,可预约日期共" + availableDates.size() + "个");
          }
          /**
          * 对外查询接口:返回7个可预约日期(跳过不可预约的,从今天开始往后凑)
          */
          public List<String> get7AvailableDates() {
            // 缓存Key:专门缓存7个可预约日期
            String cacheKey = "seal:holiday:7available";
            // 先查缓存
            Object cacheObj = redisTemplate.opsForValue().get(cacheKey);
            if (cacheObj != null) {
            System.out.println("从Redis缓存获取7个可预约日期");
            return (List<String>) cacheObj;
              }
              // 缓存未命中:查库计算
              System.out.println("Redis缓存未命中,计算7个可预约日期");
              List<String> availableDates = get7AvailableDates();
                // 写入缓存(24小时过期,每天同步后会更新)
                redisTemplate.opsForValue().set(cacheKey, availableDates, 1, TimeUnit.DAYS);
                return availableDates;
                }
                /**
                * 内部方法:从今天开始,筛选可预约日期,凑够7个为止(跳过不可预约的)
                * 规则:
                * 1. 从今天开始往后查,找到第一个可预约的日期,加入结果
                * 2. 继续往后查,直到结果中有7个日期
                * 3. 可预约条件:非节假日(isHoliday=0) 或 调休上班(isWork=1)
                */
                private List<String> get7AvailableDates() {
                  List<String> availableDates = new ArrayList<>(7); // 最终返回7个
                    Calendar cal = Calendar.getInstance();
                    Date today = new Date();
                    int maxCheckDays = 30; // 最多查未来30天(避免无限循环,确保能找到7个)
                    int checkedDays = 0;   // 已检查的天数
                    // 循环检查日期,直到凑够7个或超过最大检查天数
                    while (availableDates.size() < 7 && checkedDays < maxCheckDays) {
                    // 当前检查的日期:今天 + checkedDays天
                    cal.setTime(today);
                    cal.add(Calendar.DAY_OF_YEAR, checkedDays);
                    Date currentDate = cal.getTime();
                    String dateStr = DateUtils.date2Str(currentDate);
                    // 检查该日期是否可预约
                    if (isDateAvailable(dateStr)) {
                    availableDates.add(dateStr); // 可预约,加入结果
                    }
                    checkedDays++; // 无论是否可预约,都往后检查一天
                    }
                    // 校验是否凑够7个(避免因maxCheckDays内不足7个导致的问题)
                    if (availableDates.size() < 7) {
                    throw new RuntimeException("未来" + maxCheckDays + "天内可预约日期不足7个,请检查API同步数据");
                    }
                    return availableDates;
                    }

8. 定时任务(自动同步)

每天 20:00 自动触发 API 同步,无需人工干预:

package com.seal.task;
import com.seal.service.SysHolidayService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 节假日同步定时任务:每天20:00执行
*/
@Component
public class SysHolidayTask {
@Autowired
private SysHolidayService holidayService;
/**
* cron表达式:分 时 日 月 周 → 0 0 20 * * ? 表示每天20:00
*/
@Scheduled(cron = "0 0 20 * * ?")
public void syncHolidayTask() {
try {
System.out.println("=== 启动节假日同步定时任务 ===");
holidayService.syncHolidayFromAliyun();
System.out.println("=== 节假日同步定时任务执行完成 ===");
} catch (Exception e) {
System.err.println("=== 节假日同步定时任务失败 ===");
e.printStackTrace();
// 可选:添加告警(如短信、邮件),及时发现同步问题
}
}
}

9. Controller 层(对外接口)

提供 “可预约日期查询” 接口,供前端调用:

package com.seal.controller;
import com.seal.service.SysHolidayService;
import com.seal.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 节假日Controller:对外提供可预约日期查询接口
*/
@RestController
@RequestMapping("/api/holiday")
public class SysHolidayController {
@Autowired
private SysHolidayService holidayService;
/**
* 查询可预约日期(供前端日期选择器使用)
* 请求URL:http://localhost:8080/api/holiday/available-dates
* 请求方式:GET
* 返回示例:
* {
*   "code": 200,
*   "msg": "操作成功",
*   "data": ["2024-10-06", "2024-10-08", "2024-10-09",。。。。。。]
* }
*/
@GetMapping("/available-dates")
public Result<List<String>> getAvailableDates() {
  try {
  List<String> availableDates = holidayService.getAvailableDates();
    return Result.success(availableDates);
    } catch (Exception e) {
    return Result.fail("查询可预约日期失败:" + e.getMessage());
    }
    }
    }

看到这里附带一下整体的逻辑
在这里插入图片描述

四、总结

这套方**案实现了节假日管理的 “全自动、高性能、低维护”:

  • 全自动:每天定时同步 API,无需人工改代码;
  • 高性能:Redis 缓存支撑高频查询,数据库压力降低 99%;
  • 高可靠:事务保证同步一致性,缓存过期兜底旧数据;
  • 易扩展:可复用到考勤、工单等所有需节假日逻辑的场景。

无论是印章预约系统,还是其他企业级应用,这套方案都能彻底解决节假日维护的痛点,让开发人员专注于核心业务

posted on 2025-10-20 22:09  ycfenxi  阅读(8)  评论(0)    收藏  举报