From 34183cc40b938fa20f81aacc248ee84fc0aef169 Mon Sep 17 00:00:00 2001 From: zhouhefeng Date: Wed, 16 Sep 2026 19:52:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=BA=E5=9C=B0=E9=A2=84=E7=BA=A6=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/v/nutz/sys/models/SysHoliday.java | 24 +++++ .../nutz/sys/services/SysHolidayService.java | 28 ++++++ .../services/impl/SysHolidayServiceImpl.java | 96 +++++++++++++++++++ .../v/nutz/task/job/holiday/HolidayJob.java | 45 +++++++++ .../platform/activity/site/SiteManage.html | 6 +- 5 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/v/nutz/sys/models/SysHoliday.java create mode 100644 src/main/java/io/v/nutz/sys/services/SysHolidayService.java create mode 100644 src/main/java/io/v/nutz/sys/services/impl/SysHolidayServiceImpl.java create mode 100644 src/main/java/io/v/nutz/task/job/holiday/HolidayJob.java diff --git a/src/main/java/io/v/nutz/sys/models/SysHoliday.java b/src/main/java/io/v/nutz/sys/models/SysHoliday.java new file mode 100644 index 0000000..c2a23df --- /dev/null +++ b/src/main/java/io/v/nutz/sys/models/SysHoliday.java @@ -0,0 +1,24 @@ +package io.v.nutz.sys.models; + +import lombok.Data; +import org.nutz.dao.entity.annotation.*; + +/** 年度同步成功后保存的节假日;日期作为主键,防止重复同步产生重复记录。 */ +@Data +@Table("sys_holiday") +public class SysHoliday { + @Name + @Column + @ColDefine(type = ColType.VARCHAR, width = 10) + @Comment("节假日日期,yyyy-MM-dd") + private String day; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 100) + @Comment("节假日名称") + private String name; + + @Column + @Comment("星期,1至7分别表示周一至周日") + private Integer week; +} diff --git a/src/main/java/io/v/nutz/sys/services/SysHolidayService.java b/src/main/java/io/v/nutz/sys/services/SysHolidayService.java new file mode 100644 index 0000000..ea44b07 --- /dev/null +++ b/src/main/java/io/v/nutz/sys/services/SysHolidayService.java @@ -0,0 +1,28 @@ +package io.v.nutz.sys.services; + +import io.v.nutz.sys.models.SysHoliday; +import org.nutz.lang.util.NutMap; +import java.util.List; + +/** 节假日同步与本地日历查询,预约请求不访问外部接口。 */ +public interface SysHolidayService { + /** + * 从接口获取并验证整年度节假日,失败抛异常,不修改本地数据。 + * @param year 四位年份,例如2026 + * @return 该年度节假日实体列表,包含日期、名称及星期 + */ + List fetchYear(int year); + + /** + * 在事务内替换指定年度的日期,其他年份保持不变,保存失败回滚。 + * @param year 待更新年份 + * @param holidays 该年度完整且非空的节假日列表,日期不得重复 + */ + void saveYear(int year, List holidays); + + /** + * 获取预约使用的本地日历,不发起外部请求。 + * @return days为yyyy-MM-dd日期列表,years为已成功同步的整数年份列表 + */ + NutMap getCalendar(); +} diff --git a/src/main/java/io/v/nutz/sys/services/impl/SysHolidayServiceImpl.java b/src/main/java/io/v/nutz/sys/services/impl/SysHolidayServiceImpl.java new file mode 100644 index 0000000..8ce6206 --- /dev/null +++ b/src/main/java/io/v/nutz/sys/services/impl/SysHolidayServiceImpl.java @@ -0,0 +1,96 @@ +package io.v.nutz.sys.services.impl; + +import cn.hutool.http.HttpUtil; +import io.v.nutz.sys.models.SysHoliday; +import io.v.nutz.sys.services.SysHolidayService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.lang.Strings; +import org.nutz.lang.util.NutMap; +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Collectors; + +@IocBean +public class SysHolidayServiceImpl implements SysHolidayService { + @Inject + private Dao dao; + + @Override + public List fetchYear(int year) { + // 指定年份末尾保留斜杠,避免数据源将年度请求识别为其他路径;最多等待15秒。 + if (year < 2000 || year > 9999) { + throw new IllegalArgumentException("节假日年份不合法"); + } + String body = HttpUtil.get("https://timor.tech/api/holiday/year/" + year + "/?type=Y", 15000); + return parseYear(year, body); + } + + /** 验证整年接口响应后才交给事务保存,空数据和错误响应不能覆盖原数据。 */ + private List parseYear(int year, String body) { + NutMap response = Json.fromJson(NutMap.class, body); + if (response == null || !response.containsKey("code") || response.getInt("code") != 0) { + throw new IllegalStateException(year + "年节假日接口返回失败"); + } + NutMap types = response.getAs("type", NutMap.class); + if (types == null || types.isEmpty()) { + throw new IllegalStateException(year + "年节假日尚未发布或返回为空"); + } + List result = new ArrayList<>(); + for (String key : types.keySet()) { + NutMap item = types.getAs(key, NutMap.class); + if (item == null || !item.containsKey("type") || item.getInt("type") < 0 || item.getInt("type") > 3) { + throw new IllegalStateException("节假日接口日期类型异常"); + } + LocalDate date = LocalDate.parse(key); + if (date.getYear() != year) { + throw new IllegalStateException("节假日接口返回了其他年度的日期"); + } + // 只存节假日;周末由日期计算,调休补班的周末也按业务规则禁止预约。 + if (item.getInt("type") == 2) { + SysHoliday holiday = new SysHoliday(); + holiday.setDay(date.toString()); + holiday.setName(item.getString("name")); + holiday.setWeek(date.getDayOfWeek().getValue()); + result.add(holiday); + } + } + validateYear(year, result); + return result; + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public void saveYear(int year, List holidays) { + // 必须先验证再删除;事务内替换指定年度,保留其他年份,允许失败后安全重试。 + validateYear(year, holidays); + dao.clear(SysHoliday.class, Cnd.where("day", ">=", year + "-01-01").and("day", "<=", year + "-12-31")); + dao.insert(holidays); + } + + private void validateYear(int year, List holidays) { + if (year < 2000 || year > 9999 || holidays == null || holidays.isEmpty()) { + throw new IllegalArgumentException(year + "年节假日为空,保留已有数据"); + } + Set days = new HashSet<>(); + for (SysHoliday holiday : holidays) { + if (holiday == null || Strings.isBlank(holiday.getDay()) || Strings.isBlank(holiday.getName()) + || LocalDate.parse(holiday.getDay()).getYear() != year || !days.add(holiday.getDay())) { + throw new IllegalArgumentException("节假日日期、名称或年度不合法,保留已有数据"); + } + } + } + + @Override + public NutMap getCalendar() { + List holidays = dao.query(SysHoliday.class, Cnd.NEW().asc("day")); + List days = holidays.stream().map(SysHoliday::getDay).collect(Collectors.toList()); + List years = days.stream().map(day -> LocalDate.parse(day).getYear()).distinct().collect(Collectors.toList()); + return NutMap.NEW().addv("days", days).addv("years", years); + } +} diff --git a/src/main/java/io/v/nutz/task/job/holiday/HolidayJob.java b/src/main/java/io/v/nutz/task/job/holiday/HolidayJob.java new file mode 100644 index 0000000..a2cbf90 --- /dev/null +++ b/src/main/java/io/v/nutz/task/job/holiday/HolidayJob.java @@ -0,0 +1,45 @@ +package io.v.nutz.task.job.holiday; + +import io.v.nutz.sys.services.SysHolidayService; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.log.Log; +import org.nutz.log.Logs; +import org.quartz.*; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; + +/** + * 在现有任务管理配置此执行类。无参数时同步当年和下一年,参数 year 可指定补同步年份。 + * 建议每日凌晨重试,年度未发布时保留旧数据,避免一次失败导致全年缺失。 + */ +@IocBean +@DisallowConcurrentExecution +public class HolidayJob implements Job { + private static final Log log = Logs.get(); + @Inject + private SysHolidayService sysHolidayService; + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + int year = LocalDate.now(ZoneId.of("Asia/Shanghai")).getYear(); + Object specified = context.getMergedJobDataMap().get("year"); + int[] years = specified == null ? new int[]{year, year + 1} : new int[]{Integer.parseInt(specified.toString())}; + List failures = new ArrayList<>(); + for (int target : years) { + try { + // 通过注入服务的事务方法独立保存每年数据,下一年失败不回滚当年成功结果。 + sysHolidayService.saveYear(target, sysHolidayService.fetchYear(target)); + log.info(target + "年节假日同步成功"); + } catch (Exception e) { + failures.add(target + "年:" + e.getMessage()); + log.warn(target + "年节假日同步失败,保留已有数据", e); + } + } + if (!failures.isEmpty()) { + throw new JobExecutionException(String.join(";", failures)); + } + } +} diff --git a/src/main/resources/views/platform/activity/site/SiteManage.html b/src/main/resources/views/platform/activity/site/SiteManage.html index ecfee5f..5eb84f6 100644 --- a/src/main/resources/views/platform/activity/site/SiteManage.html +++ b/src/main/resources/views/platform/activity/site/SiteManage.html @@ -191,7 +191,7 @@ layout("/layouts/platform.html"){