导入下一年的年度节假日日历

从前同事手中接手的项目中,有个考勤系统,它有一个工作日历表,需要导入节假日数据。刚立项的时候因为没有找到稳定的公共接口,数据也不多,更新频率又是一年一次(下一年的节假日是在这一年年底才会公布),所以就直接从找到的某个接口获取到json,处理一下之后手动导入数据库。

临近元旦轮到我维护该系统,我发现之前留下的系统接口仅仅是实现了普通的批量插入,没有解析的逻辑,估计当时是直接导入数据库的。

于是我考虑在该系统的考勤模块中添加一份导入代码,后面如果用的第三方接口不再维护,换接口也只是改个解析逻辑。

接口

首先是找一份可以获取年度节假日的接口,我找到的如下:

其中,如下项目1.7k star,从2019年就开始维护,直到现在,应该可以长期使用:GitHub - NateScarlet/holiday-cn: 📅🇨🇳中国法定节假日数据 自动每日抓取国务院公告

导入

如果这个功能也写个接口写个页面按钮来触发就比较麻烦,也没有必要。我打算实现一个每年手动单次执行、无界面、与主项目功能隔离(平时不启动) 的日历同步工具。

第一个想到的是JUnit的@Test就可以直接执行,但这东西放测试目录里面不伦不类的,是很不好的写法。

接着想到写多一个utils模块,但再次否决,毕竟这比写个接口来调用更麻烦,项目代码里面又会多个冗余的模块。

最后想到可以在同一个模块内,写第二个启动类,这样我使用第二个启动类,就可以直接执行同步代码了,享受和主启动类一样的环境,平时不用的时候直接使用主启动类即可。

向AI了解了一下得知是可行的,于是开始开发。

修改POM.xml

第一步是修改模块内的POM.xml,添加mainClass标签,指定原本的启动类,这样打包之后默认启动的就是该启动类,避免被新的启动类干扰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<build>
<finalName>scheduling-server</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<!-- 添加mainClass标签,指定原本的启动类 -->
<configuration>
<mainClass>cn.iocoder.cloud.module.scheduling.server.SchedulingServerApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

访问接口

说是接口,实际上就是个静态文件。这里通过RestTemplate来进行访问,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(30000); // 连接超时:30秒
factory.setReadTimeout(60000); // 读取超时:60秒
RestTemplate restTemplate = new RestTemplate(factory);
HttpHeaders headers = new HttpHeaders();
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
headers.set("Accept", "application/json, text/plain, */*");
headers.set("Connection", "keep-alive");
HttpEntity<Void> requestEntity = new HttpEntity<>(null, headers);

// 使用exchange方法发送请求(带请求头)
System.out.println("发送请求到:" + HOLIDAY_API_URL.replace("{year}", String.valueOf(TARGET_YEAR)));
ResponseEntity<String> responseEntity = restTemplate.exchange(
HOLIDAY_API_URL,
HttpMethod.GET,
requestEntity,
String.class,
TARGET_YEAR
);

String jsonStr = responseEntity.getBody();

最终代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package cn.iocoder.yudao.module.scheduling;

import cn.iocoder.yudao.module.scheduling.controller.admin.calendar.vo.CalendarSaveReqVO;
import cn.iocoder.yudao.module.scheduling.enums.shift.CalendarTypeEnum;
import cn.iocoder.yudao.module.scheduling.service.calendar.CalendarService;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* 每年手动执行一次,同步日历数据到数据库
*
* @author yangxiao
*/
@SpringBootApplication
public class CalendarSyncApplication {

// 节假日接口地址
private static final String HOLIDAY_API_URL = "https://cdn.jsdelivr.net/gh/NateScarlet/holiday-cn@master/{year}.json";
// 目标年份
private static final int TARGET_YEAR = 2026;

// ==================== 内部静态类:JSON解析实体 ====================
/**
* 年份节假日整体实体
*/
@Data
private static class YearHoliday {
private int year;
private HolidayDay[] days;
}

/**
* 单日节假日实体
*/
@Data
private static class HolidayDay {
private String name;
private String date;
// 关键注解:指定JSON字段名对应当前属性,不想给布尔类型改名就这样写,不过建议还是不要is开头
@JsonProperty("isOffDay")
private boolean isOffDay;
}

public static void main(String[] args) {
// 1. 启动Spring容器,获取上下文
ApplicationContext context = SpringApplication.run(CalendarSyncApplication.class, args);
System.out.println("Spring容器启动成功,开始同步" + TARGET_YEAR + "年节假日数据...");

try {
// 2. 获取CalendarService Bean (注意:静态方法中不能直接@Autowired依赖注入,需要从ApplicationContext获取)
CalendarService calendarService = context.getBean(CalendarService.class);

// 3. 获取节假日JSON数据并解析
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(30000); // 连接超时:30秒
factory.setReadTimeout(60000); // 读取超时:60秒
RestTemplate restTemplate = new RestTemplate(factory);
HttpHeaders headers = new HttpHeaders();
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
headers.set("Accept", "application/json, text/plain, */*");
headers.set("Connection", "keep-alive");
HttpEntity<Void> requestEntity = new HttpEntity<>(null, headers);

// 使用exchange方法发送请求(带请求头)
System.out.println("发送请求到:" + HOLIDAY_API_URL.replace("{year}", String.valueOf(TARGET_YEAR)));
ResponseEntity<String> responseEntity = restTemplate.exchange(
HOLIDAY_API_URL,
HttpMethod.GET,
requestEntity,
String.class,
TARGET_YEAR
);

String jsonStr = responseEntity.getBody();

ObjectMapper objectMapper = new ObjectMapper();
// 关键配置:忽略目标类中不存在的JSON字段
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
YearHoliday yearHoliday = objectMapper.readValue(jsonStr, YearHoliday.class);
System.out.println("成功解析JSON数据,年份:" + yearHoliday.getYear());

// 4. 构建日期→节假日信息的映射(方便快速查询)
Map<LocalDate, HolidayDay> holidayDateMap = new HashMap<>();
for (HolidayDay day : yearHoliday.getDays()) {
LocalDate date = LocalDate.parse(day.getDate());
holidayDateMap.put(date, day);
}

// 5. 遍历指定年份所有日期,构建批量插入列表
LocalDate startDate = LocalDate.of(TARGET_YEAR, 1, 1);
LocalDate endDate = LocalDate.of(TARGET_YEAR, 12, 31);
LocalDate currentDate = startDate;
List<CalendarSaveReqVO> calendarList = new ArrayList<>();

while (!currentDate.isAfter(endDate)) {
// 构建CalendarSaveReqVO
CalendarSaveReqVO reqVO = new CalendarSaveReqVO();
HolidayDay holidayDay = holidayDateMap.get(currentDate);

if (holidayDay != null) {
// 节假日或补班日
if (holidayDay.isOffDay()) {
// 节假日:类型2,备注为节假日名称
reqVO.setType(CalendarTypeEnum.HOLIDAY.getCode());
reqVO.setRemark(holidayDay.getName());
} else {
// 补班日:类型0,备注为"补班+节假日名称"
reqVO.setType(CalendarTypeEnum.WORKDAY.getCode());
reqVO.setRemark("补班(" + holidayDay.getName() + ")");
}
} else {
// 非节假日/补班日:判断是否为周末
DayOfWeek dayOfWeek = currentDate.getDayOfWeek();
if (DayOfWeek.SATURDAY.equals(dayOfWeek) || DayOfWeek.SUNDAY.equals(dayOfWeek)) {
// 周末休息:类型1
reqVO.setType(CalendarTypeEnum.WEEKEND.getCode());
// reqVO.setRemark("周末");
} else {
// 正常上班:类型0
reqVO.setType(CalendarTypeEnum.WORKDAY.getCode());
// reqVO.setRemark("工作日");
}
}

// 公共参数设置
reqVO.setId(null); // 主键自增,设为null
reqVO.setDate(LocalDateTime.of(currentDate, LocalTime.MIDNIGHT)); // 转为当天0点

calendarList.add(reqVO);

// 切换到下一天
currentDate = currentDate.plusDays(1);
}

// 6. 批量调用Service插入数据库
Integer result = calendarService.batchCreateCalendars(calendarList);
System.out.println(TARGET_YEAR + "年节假日数据同步完成!共处理 " + calendarList.size() + " 天数据,成功插入 " + result + " 条");

} catch (Exception e) {
System.err.println("数据同步失败,异常信息:" + e.getMessage());
e.printStackTrace();
} finally {
// 7. 执行完毕,关闭Spring容器
System.out.println("开始关闭Spring容器...");
int exitCode = SpringApplication.exit(context, () -> 0);
System.exit(exitCode);
}
}

}

导入下一年的年度节假日日历

https://yxchangingself.xyz/posts/2dc010a/

作者

憧憬少

发布于

2025-12-23

更新于

2025-12-23

许可协议