场地预约问题
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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<SysHoliday> fetchYear(int year);
|
||||
|
||||
/**
|
||||
* 在事务内替换指定年度的日期,其他年份保持不变,保存失败回滚。
|
||||
* @param year 待更新年份
|
||||
* @param holidays 该年度完整且非空的节假日列表,日期不得重复
|
||||
*/
|
||||
void saveYear(int year, List<SysHoliday> holidays);
|
||||
|
||||
/**
|
||||
* 获取预约使用的本地日历,不发起外部请求。
|
||||
* @return days为yyyy-MM-dd日期列表,years为已成功同步的整数年份列表
|
||||
*/
|
||||
NutMap getCalendar();
|
||||
}
|
||||
@@ -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<SysHoliday> 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<SysHoliday> 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<SysHoliday> 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<SysHoliday> 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<SysHoliday> holidays) {
|
||||
if (year < 2000 || year > 9999 || holidays == null || holidays.isEmpty()) {
|
||||
throw new IllegalArgumentException(year + "年节假日为空,保留已有数据");
|
||||
}
|
||||
Set<String> 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<SysHoliday> holidays = dao.query(SysHoliday.class, Cnd.NEW().asc("day"));
|
||||
List<String> days = holidays.stream().map(SysHoliday::getDay).collect(Collectors.toList());
|
||||
List<Integer> years = days.stream().map(day -> LocalDate.parse(day).getYear()).distinct().collect(Collectors.toList());
|
||||
return NutMap.NEW().addv("days", days).addv("years", years);
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,7 +191,7 @@ layout("/layouts/platform.html"){
|
||||
<!-- 截止日期含当天,清空表示不限制最晚预约日期。 -->
|
||||
<el-date-picker
|
||||
:value="formData.reserve_deadline"
|
||||
@input="value => this.$set(formData, 'reserve_deadline', value || '')"
|
||||
@input="changeReserveDeadline"
|
||||
type="date"
|
||||
value-format="yyyy-MM-dd"
|
||||
placeholder="不设置则不限制"
|
||||
@@ -302,6 +302,10 @@ layout("/layouts/platform.html"){
|
||||
'guava': httpVueLoader('/components/plugins/Guava.vue'),
|
||||
},
|
||||
methods: {
|
||||
// 日期组件传入yyyy-MM-dd或清空值;使用页面绑定的方法,避免插槽内this指向错误。
|
||||
changeReserveDeadline(value) {
|
||||
this.$set(this.formData, 'reserve_deadline', value || '')
|
||||
},
|
||||
doDelete(row) {
|
||||
this.$confirm('此操作将删除【' + row.name + '】场地的所有信息!', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
|
||||
Reference in New Issue
Block a user