在企业级系统开发中,节假日逻辑是个常见但容易 “埋坑” 的需求 —— 比如印章预约、考勤打卡、工单截止日期计算等场景,都需要准确识别节假日。如果硬编码节假日(if (date == "2025-01-01")),每年调休都要改代码、重启服务,维护成本极高。
本文就以 “印章预约系统” 为例,手把手教你用 SpringBoot 对接阿里云节假日 API,实现节假日自动同步、库存动态适配,彻底告别硬编码!
一、前言:为什么要对接阿里云 API?
手动维护节假日的痛点:
- 规则僵化:每年法定节假日 + 调休变化,需手动修改代码;
- 易出错:漏改调休日期会导致系统逻辑错误(比如周末调休上班却不让预约);
- 效率低:每次更新都要发版,影响系统可用性。
对接阿里云 API 的优势:
- 实时同步:API 会实时更新官方节假日 / 调休规则,无需人工干预;
- 稳定可靠:阿里云提供高可用服务,支持每秒百级调用;
- 低成本:基础版 API 单次调用几分钱,中小系统年成本不足 100 元。
二、准备工作:阿里云 API 开通与配置
1. 开通阿里云节假日 API
- 第一步:登录 阿里云控制台,在顶部搜索 “万年历查询” 或 “节假日查询”,选择第三方 API 服务商(推荐 “易源数据”“极速数据”,文档清晰、性价比高)。
- 第二步:选择 “免费试用” 或 “按量付费”(新手建议先试用),点击 “立即购买”,完成开通。
- 第三步:获取
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 响应对齐:
| 字段名 | 类型 | 允许空 | 默认值 | 主键/索引 | 说明 |
|---|---|---|---|---|---|
| id | bigint(20) | NO | - | PRIMARY | 主键ID |
| date | date | NO | - | UNIQUE | 日期(yyyy-MM-dd) |
| is_holiday | tinyint(1) | NO | 0 | - | 是否节假日(0-否,1-是) |
| holiday_name | varchar(50) | YES | ‘’ | - | 节假日名称(非节假日为空) |
| is_work | tinyint(1) | NO | 0 | - | 是否调休上班(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%;
- 高可靠:事务保证同步一致性,缓存过期兜底旧数据;
- 易扩展:可复用到考勤、工单等所有需节假日逻辑的场景。
无论是印章预约系统,还是其他企业级应用,这套方案都能彻底解决节假日维护的痛点,让开发人员专注于核心业务
浙公网安备 33010602011771号