|
@@ -0,0 +1,324 @@
|
|
|
+package com.ruoyi.common.utils;
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+import com.ruoyi.common.utils.http.HttpUtils;
|
|
|
+import okhttp3.*;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.apache.commons.lang3.math.NumberUtils;
|
|
|
+import org.jsoup.Jsoup;
|
|
|
+import org.jsoup.nodes.Document;
|
|
|
+import org.jsoup.nodes.Element;
|
|
|
+import org.jsoup.select.Elements;
|
|
|
+import java.io.*;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.ConcurrentSkipListSet;
|
|
|
+import java.util.regex.Matcher;
|
|
|
+import java.util.regex.Pattern;
|
|
|
+
|
|
|
+/**
|
|
|
+ * @Author: tjf
|
|
|
+ * @Date: 2023/11/2 9:20
|
|
|
+ * @Describe:
|
|
|
+ */
|
|
|
+public class ChinaHolidaysUtils {
|
|
|
+ /**
|
|
|
+ * 国务院发布的节假日安排的通知 保存的文件路径
|
|
|
+ */
|
|
|
+ private static final String HOLIDAY_NOTICES_FILE_PATH = ChinaHolidaysUtils.class.getResource("/").getPath()+"国务院发布的节假日安排的通知/";
|
|
|
+ /**
|
|
|
+ * 国务院文件搜索地址
|
|
|
+ */
|
|
|
+ private static final String GOV_URL = "https://sousuo.www.gov.cn/sousuo/search.shtml?code=17da70961a7&dataTypeId=107&";
|
|
|
+
|
|
|
+ private static Set<String> publicHolidays = new ConcurrentSkipListSet<>();
|
|
|
+ private static Set<String> oxenHorseDays = new ConcurrentSkipListSet<>();
|
|
|
+
|
|
|
+ public static void main(String[] args) {
|
|
|
+ try {
|
|
|
+ //System.out.println(isOxenHorseDays("2023-12-30"));
|
|
|
+ System.out.println(isPublicHolidays("2024-12-30"));
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ System.out.println("publicHolidays: " + publicHolidays);
|
|
|
+ System.out.println("oxenHorseDays: " + oxenHorseDays);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 是否为调休补班日
|
|
|
+ * @param localDate
|
|
|
+ * @return
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ public static boolean isOxenHorseDays(String localDate) throws IOException {
|
|
|
+ if(oxenHorseDays.isEmpty()){
|
|
|
+ getDays(localDate, publicHolidays, oxenHorseDays);
|
|
|
+ }
|
|
|
+ return oxenHorseDays.contains(localDate);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 是否为法定节假日
|
|
|
+ * @param localDate
|
|
|
+ * @return
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ public static boolean isPublicHolidays(String localDate) throws IOException {
|
|
|
+ if(publicHolidays.isEmpty()){
|
|
|
+ getDays(localDate, publicHolidays, oxenHorseDays);
|
|
|
+ }
|
|
|
+ return publicHolidays.contains(localDate);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static synchronized void getDays(String localDate, Set<String> publicHolidays, Set<String> oxenHorseDays) throws IOException {
|
|
|
+ //获取xxx年的节假日数据
|
|
|
+ String year = null;
|
|
|
+ if(!StringUtils.isEmpty(localDate)){
|
|
|
+ year = String.valueOf(LocalDate.parse(localDate).getYear());
|
|
|
+ }else{
|
|
|
+ year = String.valueOf(LocalDate.now().getYear());
|
|
|
+ }
|
|
|
+ //先通过缓存文件,否则使用http获取
|
|
|
+ String html = getHtmlByCacheFiles(year);
|
|
|
+ if(html == null){
|
|
|
+ html = getHtmlByHttp(year);
|
|
|
+ }
|
|
|
+ Document doc = Jsoup.parse(html);
|
|
|
+ Element content = doc.select("div.b12c.pages_content").first();
|
|
|
+ Elements paragraphs = content.select("p");
|
|
|
+ for (Element p : paragraphs) {
|
|
|
+ String text = p.text();
|
|
|
+ if (text.contains("、") && text.contains(":") && text.contains("。") && text.contains("放假")) {
|
|
|
+ text = text.substring(text.indexOf(":")+1);
|
|
|
+ String[] sentences = text.split("。");
|
|
|
+ for (String sentence : sentences) {
|
|
|
+ if (sentence.contains("放假")) {
|
|
|
+ String t = sentence.split("放假")[0];
|
|
|
+ if (t.contains("至")) {
|
|
|
+ String start = t.split("至")[0];
|
|
|
+ String startDay=null, startMonth=null, startYear=null;
|
|
|
+ if(start.contains("日") || start.contains("月") || start.contains("年")){
|
|
|
+ startDay = getDigit(start, "日");
|
|
|
+ startMonth = getDigit(start, "月");
|
|
|
+ startYear = getDigit(start, "年");
|
|
|
+ }
|
|
|
+ LocalDate startDate = parseDate(startYear==null?year:startYear, startMonth, startDay);
|
|
|
+ String end = t.split("至")[1];
|
|
|
+ String endDay=null, endMonth=null, endYear=null;
|
|
|
+ if(end.contains("日") || end.contains("月") || end.contains("年")){
|
|
|
+ endDay = getDigit(end, "日");
|
|
|
+ endMonth = getDigit(end, "月");
|
|
|
+ endYear = getDigit(end, "年");
|
|
|
+ }
|
|
|
+ LocalDate endDate = parseDate(endYear==null?(startYear==null?year:startYear):endYear, endMonth==null?startMonth:endMonth, endDay);
|
|
|
+ for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
|
|
|
+ publicHolidays.add(date.toString());
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ String tDay=null, tMonth=null, tYear=null;
|
|
|
+ if(t.contains("日") || t.contains("月") || t.contains("年")){
|
|
|
+ tDay = getDigit(t, "日");
|
|
|
+ tMonth = getDigit(t, "月");
|
|
|
+ tYear = getDigit(t, "年");
|
|
|
+ }
|
|
|
+ LocalDate date = parseDate(tYear==null? year:tYear, tMonth, tDay);
|
|
|
+ publicHolidays.add(date.toString());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (sentence.contains("上班")) {
|
|
|
+ String t = sentence.split("上班")[0];
|
|
|
+ if (sentence.contains("、")) {
|
|
|
+ String[] dates = sentence.split("、");
|
|
|
+ for (String dateStr : dates) {
|
|
|
+ String tDay=null, tMonth=null, tYear=null;
|
|
|
+ if(dateStr.contains("日") || dateStr.contains("月") || dateStr.contains("年")){
|
|
|
+ tDay = getDigit(dateStr, "日");
|
|
|
+ tMonth = getDigit(dateStr, "月");
|
|
|
+ tYear = getDigit(dateStr, "年");
|
|
|
+ }
|
|
|
+ LocalDate date = parseDate(tYear==null? year:tYear, tMonth, tDay);
|
|
|
+ oxenHorseDays.add(date.toString());
|
|
|
+ }
|
|
|
+ }else{
|
|
|
+ String tDay=null, tMonth=null, tYear=null;
|
|
|
+ if(t.contains("日") || t.contains("月") || t.contains("年")){
|
|
|
+ tDay = getDigit(t, "日");
|
|
|
+ tMonth = getDigit(t, "月");
|
|
|
+ tYear = getDigit(t, "年");
|
|
|
+ }
|
|
|
+ LocalDate date = parseDate(tYear==null? year:tYear, tMonth, tDay);
|
|
|
+ oxenHorseDays.add(date.toString());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 模拟人为操作的参数
|
|
|
+ * @param url
|
|
|
+ */
|
|
|
+ private static Request getRequestSetUnifiedHead(String url){
|
|
|
+ Request.Builder builder = new Request.Builder();
|
|
|
+ builder.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36")
|
|
|
+ .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
|
|
|
+ .header("Host", "www.gov.cn")
|
|
|
+ .header("Referer", "http://sousuo.www.gov.cn/")
|
|
|
+// .header("Accept-Encoding", "gzip, deflate, br") 造成乱码问题
|
|
|
+ .header("Accept-Language", "zh-CN,zh;q=0.9")
|
|
|
+ .header("Cache-Control", "max-age=0")
|
|
|
+ .header("Connection", "keep-alive")
|
|
|
+ .header("Sec-Fetch-Dest", "document")
|
|
|
+ .header("Sec-Fetch-Mode", "navigate")
|
|
|
+ .header("Sec-Fetch-Site", "cross-site")
|
|
|
+ .header("Sec-Fetch-User", "?1")
|
|
|
+ .header("Upgrade-Insecure-Requests", "1")
|
|
|
+ .header("sec-ch-ua", "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"")
|
|
|
+ .header("sec-ch-ua-mobile", "?0")
|
|
|
+ .header("sec-ch-ua-platform", "Windows");
|
|
|
+ return builder.url(url).build();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * http get请求
|
|
|
+ * @param client
|
|
|
+ * @param url
|
|
|
+ * @return
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ private static String httpGet(OkHttpClient client, String url) throws IOException {
|
|
|
+ Request request = getRequestSetUnifiedHead(url);
|
|
|
+ Response response = client.newCall(request).execute();
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
+ throw new IOException("获取数据失败:" + url);
|
|
|
+ }
|
|
|
+ String html = response.body().string();
|
|
|
+ System.out.println("进行了一次http get请求:" + url);
|
|
|
+ return html;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 通过http获取国务院发布xxxx年的节假日安排的通知
|
|
|
+ * @return
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ private static String getHtmlByHttp(String year) throws IOException {
|
|
|
+ OkHttpClient client = new OkHttpClient();
|
|
|
+ String searchWord ="searchWord=" + year + "节假日";
|
|
|
+ String html = httpGet(client, GOV_URL+searchWord);
|
|
|
+ Document doc = Jsoup.parse(html);
|
|
|
+ Elements resList = doc.select("li.res-list");
|
|
|
+ if (!resList.isEmpty()) {
|
|
|
+ Optional<Element> optional = resList.stream().filter(res -> res.text().contains("国务院办公厅关于"+year+"年")).findFirst();
|
|
|
+ if (!optional.isPresent()) {
|
|
|
+ throw new IOException("未获取到"+ year +"年节假日安排的通知:" + GOV_URL);
|
|
|
+ }
|
|
|
+ Element element = optional.get();
|
|
|
+ String linkUrl = element.select("a[href]").attr("abs:href");
|
|
|
+ html = httpGet(client, linkUrl);
|
|
|
+ str2File(html, HOLIDAY_NOTICES_FILE_PATH, year+"节假日安排的通知-源数据", ".html");
|
|
|
+ Document resDoc = Jsoup.parse(html);
|
|
|
+ str2File(html, HOLIDAY_NOTICES_FILE_PATH, resDoc.title(), ".html");
|
|
|
+ return html;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 先通过缓存节假日通知的指定目录中获取 当年的 节假日通知文件
|
|
|
+ * @param year
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private static String getHtmlByCacheFiles(String year) {
|
|
|
+ String[] paths = new File(HOLIDAY_NOTICES_FILE_PATH).list();
|
|
|
+ if(paths != null && paths.length > 0){
|
|
|
+ Optional<String> yearPath = Arrays.stream(paths).filter(p -> p.contains(year)).findFirst();
|
|
|
+ if (yearPath.isPresent()){
|
|
|
+ return file2Str(HOLIDAY_NOTICES_FILE_PATH + yearPath.get());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据年月日字符转为 yyyy-M-d 格式的LocalDate
|
|
|
+ * @param year
|
|
|
+ * @param month
|
|
|
+ * @param day
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private static LocalDate parseDate(String year, String month, String day) {
|
|
|
+ String dateStr = year + "-" + month + "-" + day;
|
|
|
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-M-d");
|
|
|
+ return LocalDate.parse(dateStr, formatter);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将字符串内容转为文件存到指定路径下
|
|
|
+ * @param str
|
|
|
+ * @param filePath
|
|
|
+ */
|
|
|
+ public static void str2File(String str, String filePath, String fileName, String fileSuffix) throws IOException {
|
|
|
+ File file = new File(filePath + fileName + fileSuffix);
|
|
|
+ if (!file.getParentFile().exists()) {
|
|
|
+ boolean created = file.getParentFile().mkdirs();
|
|
|
+ if (!created) {
|
|
|
+ throw new IOException("文件路径创建失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ try (FileWriter writer = new FileWriter(file)) {
|
|
|
+ writer.write(str);
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将指定路径的文件转为字符串
|
|
|
+ * @param filePath
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private static String file2Str(String filePath) {
|
|
|
+ File file = new File(filePath);
|
|
|
+ try (FileReader reader = new FileReader(file)) {
|
|
|
+ char[] buffer = new char[(int) file.length()];
|
|
|
+ reader.read(buffer);
|
|
|
+ return new String(buffer);
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取指定字符串前的数字
|
|
|
+ */
|
|
|
+ private static final Pattern PATTERN = Pattern.compile("[^\\d]");
|
|
|
+ private static String getDigit(String content, String targetStr) {
|
|
|
+ if(!content.contains(targetStr)){
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ content = content.substring(0, content.indexOf(targetStr));
|
|
|
+ StringBuffer sb = new StringBuffer(content);
|
|
|
+ content = sb.reverse().toString();
|
|
|
+ //使用正则表达式匹配第一个非数字
|
|
|
+ Matcher matcher = PATTERN.matcher(content);
|
|
|
+ if (matcher.find()) {
|
|
|
+ content = content.substring(0, matcher.start());
|
|
|
+ sb = new StringBuffer(content);
|
|
|
+ return sb.reverse().toString();
|
|
|
+ }
|
|
|
+ //是否为数字
|
|
|
+ if(NumberUtils.isCreatable(content)){
|
|
|
+ return new StringBuffer(content).reverse().toString();
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+}
|