|
@@ -0,0 +1,329 @@
|
|
|
|
|
+package cn.iocoder.yudao.module.system.service.appointment;
|
|
|
|
|
+
|
|
|
|
|
+import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
|
|
|
|
|
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.AppointmentConfigRespVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaOccupyReqVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaOccupyRespVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.AppointmentConfigDO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.AppointmentGroupDO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.dal.mysql.miniapp.JiashuVisitAppointmentMapper;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.dal.redis.appointment.AppointmentQuotaRedisDAO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.enums.ErrorCodeConstants;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.service.biz.AppointmentConfigService;
|
|
|
|
|
+import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import java.time.DayOfWeek;
|
|
|
|
|
+import java.time.LocalDate;
|
|
|
|
|
+import java.time.LocalTime;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Objects;
|
|
|
|
|
+
|
|
|
|
|
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 预约名额预占 Service 实现
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>校验维度:① 总开关 + 时间窗(begin_book_days / advance_days)
|
|
|
|
|
+ * ② 预约组日期匹配(dateType) + 时间段匹配(beginTime~endTime) + 每组 limitCount
|
|
|
|
|
+ * ③ 总名额(total_daily/weekly/monthly) ④ 单人名额(person_daily/weekly/monthly)</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <h4>双算处理(核心)</h4>
|
|
|
|
|
+ * <pre>
|
|
|
|
|
+ * 前端调用顺序:occupy → 实际预约接口(落库 visit_appointment) → release
|
|
|
|
|
+ *
|
|
|
|
|
+ * occupy 时 Redis 各维度 +1(预占);
|
|
|
|
|
+ * release 时仅总名额/单人名额 Redis -1(因 DB 可统计,DB+1 后 Redis 预占多余);
|
|
|
|
|
+ * 组维度不递减——visit_appointment 缺 groupId,DB 无法按组统计,Redis 即正式计数,次日过期自动释放。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 结果:
|
|
|
|
|
+ * - 总名额/单人名额:DB 正式 + Redis 预占 = 1 + 0 = 1(正确,不双算)
|
|
|
|
|
+ * - 组维度:Redis 即正式计数(不会双算),次日过期自动释放名额
|
|
|
|
|
+ * </pre>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @author system
|
|
|
|
|
+ */
|
|
|
|
|
+@Service
|
|
|
|
|
+public class AppAppointmentQuotaServiceImpl implements AppAppointmentQuotaService {
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 每次预约占用 1 个名额(订单维度)。若需按实际人数(1 + 随行)占用,改此处即可。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final int OCCUPY_COUNT = 1;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AppointmentConfigService appointmentConfigService;
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private JiashuVisitAppointmentMapper jiashuVisitAppointmentMapper;
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AppointmentQuotaRedisDAO appointmentQuotaRedisDAO;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public AppointmentQuotaOccupyRespVO occupy(AppointmentQuotaOccupyReqVO reqVO) {
|
|
|
|
|
+ Long tenantId = reqVO.getOrgId();
|
|
|
|
|
+ LocalDate visitDate = reqVO.getVisitDate();
|
|
|
|
|
+ String openId = reqVO.getVisitorOpenId();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 配置存在 & 开关
|
|
|
|
|
+ AppointmentConfigRespVO configResp = appointmentConfigService.getByTenant(tenantId);
|
|
|
|
|
+ AppointmentConfigDO config = configResp.getConfig();
|
|
|
|
|
+ if (config == null) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_CONFIG_NOT_FOUND);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Objects.equals(config.getEnabled(), 1)) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_DISABLED);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 时间窗(最早可约=今天+beginBookDays,最晚可约=今天+advanceDays)
|
|
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
|
|
+ LocalDate earliest = today.plusDays(config.getBeginBookDays() == null ? 0 : config.getBeginBookDays());
|
|
|
|
|
+ LocalDate latest = today.plusDays(config.getAdvanceDays() == null ? 0 : config.getAdvanceDays());
|
|
|
|
|
+ if (visitDate.isBefore(earliest) || visitDate.isAfter(latest)) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_DATE_NOT_IN_RANGE);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 预约组日期匹配 + 时间段匹配
|
|
|
|
|
+ LocalTime begin = parseTime(reqVO.getVisitBeginTime());
|
|
|
|
|
+ LocalTime end = parseTime(reqVO.getVisitEndTime());
|
|
|
|
|
+ if (!end.isAfter(begin)) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_TIME_NOT_ALLOWED);
|
|
|
|
|
+ }
|
|
|
|
|
+ AppointmentGroupDO matchedGroup = matchGroup(configResp.getGroup(), visitDate, begin, end);
|
|
|
|
|
+ if (matchedGroup == null) {
|
|
|
|
|
+ // 区分「日期不可约」与「时间段不可约」
|
|
|
|
|
+ if (hasDateMatchedGroup(configResp.getGroup(), visitDate)) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_TIME_NOT_ALLOWED);
|
|
|
|
|
+ }
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_DATE_NOT_ALLOWED);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 总名额(daily / weekly / monthly)
|
|
|
|
|
+ checkTotalLimit(config, tenantId, visitDate);
|
|
|
|
|
+ // 5. 单人名额(daily / weekly / monthly)
|
|
|
|
|
+ checkPersonLimit(config, tenantId, openId, visitDate);
|
|
|
|
|
+ // 6. 组名额(每组每天 limitCount)
|
|
|
|
|
+ checkGroupLimit(matchedGroup, visitDate);
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 校验通过,预占 +1(各维度)
|
|
|
|
|
+ appointmentQuotaRedisDAO.incrTotalDaily(tenantId, visitDate, OCCUPY_COUNT);
|
|
|
|
|
+ appointmentQuotaRedisDAO.incrTotalWeekly(tenantId, visitDate, OCCUPY_COUNT);
|
|
|
|
|
+ appointmentQuotaRedisDAO.incrTotalMonthly(tenantId, visitDate, OCCUPY_COUNT);
|
|
|
|
|
+ appointmentQuotaRedisDAO.incrPersonDaily(tenantId, openId, visitDate, OCCUPY_COUNT);
|
|
|
|
|
+ appointmentQuotaRedisDAO.incrPersonWeekly(tenantId, openId, visitDate, OCCUPY_COUNT);
|
|
|
|
|
+ appointmentQuotaRedisDAO.incrPersonMonthly(tenantId, openId, visitDate, OCCUPY_COUNT);
|
|
|
|
|
+ appointmentQuotaRedisDAO.incrGroupDaily(matchedGroup.getId(), visitDate, OCCUPY_COUNT);
|
|
|
|
|
+
|
|
|
|
|
+ return AppointmentQuotaOccupyRespVO.success(matchedGroup.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void release(AppointmentQuotaOccupyReqVO reqVO) {
|
|
|
|
|
+ Long tenantId = reqVO.getOrgId();
|
|
|
|
|
+ LocalDate visitDate = reqVO.getVisitDate();
|
|
|
|
|
+ String openId = reqVO.getVisitorOpenId();
|
|
|
|
|
+
|
|
|
|
|
+ /*
|
|
|
|
|
+ * release 调用时机:前端调 occupy 通过 → 调实际预约接口落库 → 立即调 release
|
|
|
|
|
+ *
|
|
|
|
|
+ * 维度处理差异:
|
|
|
|
|
+ * - 总名额 / 单人名额:DB(visit_appointment)可统计,实际预约 DB+1 后需要通过 release 把 Redis 预占 -1,
|
|
|
|
|
+ * 避免 DB+Redis 双算(最终计数 = DB 正式 + Redis 预占 = 1 + 0 = 1,正确)。
|
|
|
|
|
+ * - 组维度:visit_appointment 无 groupId 字段,DB 无法按组统计,因此 Redis 就是正式计数,
|
|
|
|
|
+ * release 时不做递减,保留到次日 TTL 过期自动释放。
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+ appointmentQuotaRedisDAO.decr(appointmentQuotaRedisDAO.formatTotalDailyKey(tenantId, visitDate), OCCUPY_COUNT);
|
|
|
|
|
+ appointmentQuotaRedisDAO.decr(appointmentQuotaRedisDAO.formatTotalWeeklyKey(tenantId, visitDate), OCCUPY_COUNT);
|
|
|
|
|
+ appointmentQuotaRedisDAO.decr(appointmentQuotaRedisDAO.formatTotalMonthlyKey(tenantId, visitDate), OCCUPY_COUNT);
|
|
|
|
|
+ appointmentQuotaRedisDAO.decr(appointmentQuotaRedisDAO.formatPersonDailyKey(tenantId, openId, visitDate), OCCUPY_COUNT);
|
|
|
|
|
+ appointmentQuotaRedisDAO.decr(appointmentQuotaRedisDAO.formatPersonWeeklyKey(tenantId, openId, visitDate), OCCUPY_COUNT);
|
|
|
|
|
+ appointmentQuotaRedisDAO.decr(appointmentQuotaRedisDAO.formatPersonMonthlyKey(tenantId, openId, visitDate), OCCUPY_COUNT);
|
|
|
|
|
+ // 注意:组维度不递减——visit_appointment 缺 groupId 无法从 DB 按组统计,Redis 即正式计数,到期自动释放
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 各维度校验 ==========
|
|
|
|
|
+
|
|
|
|
|
+ private void checkTotalLimit(AppointmentConfigDO config, Long tenantId, LocalDate date) {
|
|
|
|
|
+ if (config.getTotalDailyLimit() != null) {
|
|
|
|
|
+ Long db = countByOrgInMiniapp(tenantId, date, date);
|
|
|
|
|
+ Long redis = appointmentQuotaRedisDAO.getCount(appointmentQuotaRedisDAO.formatTotalDailyKey(tenantId, date));
|
|
|
|
|
+ if (db + redis >= config.getTotalDailyLimit()) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_TOTAL_LIMIT);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (config.getTotalWeeklyLimit() != null) {
|
|
|
|
|
+ Long db = countByOrgInMiniapp(tenantId, date.with(DayOfWeek.MONDAY), date.with(DayOfWeek.SUNDAY));
|
|
|
|
|
+ Long redis = appointmentQuotaRedisDAO.getCount(appointmentQuotaRedisDAO.formatTotalWeeklyKey(tenantId, date));
|
|
|
|
|
+ if (db + redis >= config.getTotalWeeklyLimit()) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_TOTAL_LIMIT);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (config.getTotalMonthlyLimit() != null) {
|
|
|
|
|
+ Long db = countByOrgInMiniapp(tenantId, date.withDayOfMonth(1), date.withDayOfMonth(date.lengthOfMonth()));
|
|
|
|
|
+ Long redis = appointmentQuotaRedisDAO.getCount(appointmentQuotaRedisDAO.formatTotalMonthlyKey(tenantId, date));
|
|
|
|
|
+ if (db + redis >= config.getTotalMonthlyLimit()) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_TOTAL_LIMIT);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void checkPersonLimit(AppointmentConfigDO config, Long tenantId, String openId, LocalDate date) {
|
|
|
|
|
+ if (config.getPersonDailyLimit() != null) {
|
|
|
|
|
+ Long db = countByOpenIdInMiniapp(openId, date, date);
|
|
|
|
|
+ Long redis = appointmentQuotaRedisDAO.getCount(appointmentQuotaRedisDAO.formatPersonDailyKey(tenantId, openId, date));
|
|
|
|
|
+ if (db + redis >= config.getPersonDailyLimit()) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_PERSON_LIMIT);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (config.getPersonWeeklyLimit() != null) {
|
|
|
|
|
+ Long db = countByOpenIdInMiniapp(openId, date.with(DayOfWeek.MONDAY), date.with(DayOfWeek.SUNDAY));
|
|
|
|
|
+ Long redis = appointmentQuotaRedisDAO.getCount(appointmentQuotaRedisDAO.formatPersonWeeklyKey(tenantId, openId, date));
|
|
|
|
|
+ if (db + redis >= config.getPersonWeeklyLimit()) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_PERSON_LIMIT);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (config.getPersonMonthlyLimit() != null) {
|
|
|
|
|
+ Long db = countByOpenIdInMiniapp(openId, date.withDayOfMonth(1), date.withDayOfMonth(date.lengthOfMonth()));
|
|
|
|
|
+ Long redis = appointmentQuotaRedisDAO.getCount(appointmentQuotaRedisDAO.formatPersonMonthlyKey(tenantId, openId, date));
|
|
|
|
|
+ if (db + redis >= config.getPersonMonthlyLimit()) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_PERSON_LIMIT);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void checkGroupLimit(AppointmentGroupDO group, LocalDate date) {
|
|
|
|
|
+ if (group.getLimitCount() == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 注意:visit_appointment 缺少 groupId 字段,组维度无法从 DB 统计;
|
|
|
|
|
+ // Redis 即正式计数(occupy 时 +1 后不再减,次日过期自动释放),因此不会双算。
|
|
|
|
|
+ Long redis = appointmentQuotaRedisDAO.getCount(appointmentQuotaRedisDAO.formatGroupDailyKey(group.getId(), date));
|
|
|
|
|
+ if (redis >= group.getLimitCount()) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_GROUP_LIMIT);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 数据源切换:visit_appointment 在 yanglao-jiashu 数据源 ==========
|
|
|
|
|
+
|
|
|
|
|
+ private Long countByOrgInMiniapp(Long orgId, LocalDate start, LocalDate end) {
|
|
|
|
|
+ DynamicDataSourceContextHolder.push("miniapp");
|
|
|
|
|
+ try {
|
|
|
|
|
+ return jiashuVisitAppointmentMapper.countByOrgAndDateRange(orgId, start, end);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ DynamicDataSourceContextHolder.poll();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Long countByOpenIdInMiniapp(String openId, LocalDate start, LocalDate end) {
|
|
|
|
|
+ DynamicDataSourceContextHolder.push("miniapp");
|
|
|
|
|
+ try {
|
|
|
|
|
+ return jiashuVisitAppointmentMapper.countByOpenIdAndDateRange(openId, start, end);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ DynamicDataSourceContextHolder.poll();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 预约组日期匹配 ==========
|
|
|
|
|
+
|
|
|
|
|
+ private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("HH:mm");
|
|
|
|
|
+
|
|
|
|
|
+ private AppointmentGroupDO matchGroup(List<AppointmentGroupDO> groups, LocalDate date, LocalTime begin, LocalTime end) {
|
|
|
|
|
+ if (groups == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (AppointmentGroupDO g : groups) {
|
|
|
|
|
+ if (matchDate(g, date) && matchTime(g, begin, end)) {
|
|
|
|
|
+ return g;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean hasDateMatchedGroup(List<AppointmentGroupDO> groups, LocalDate date) {
|
|
|
|
|
+ if (groups == null) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (AppointmentGroupDO g : groups) {
|
|
|
|
|
+ if (matchDate(g, date)) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean matchTime(AppointmentGroupDO g, LocalTime begin, LocalTime end) {
|
|
|
|
|
+ if (g.getBeginTime() == null || g.getEndTime() == null) {
|
|
|
|
|
+ return true; // 组未配置时间段,不做时间段限制
|
|
|
|
|
+ }
|
|
|
|
|
+ LocalTime gBegin = LocalTime.parse(g.getBeginTime(), TIME_FMT);
|
|
|
|
|
+ LocalTime gEnd = LocalTime.parse(g.getEndTime(), TIME_FMT);
|
|
|
|
|
+ // 请求时间段须完全落在组时间段内(子集)
|
|
|
|
|
+ return !begin.isBefore(gBegin) && !end.isAfter(gEnd);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private LocalTime parseTime(String time) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return LocalTime.parse(time, TIME_FMT);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw exception(ErrorCodeConstants.APPOINTMENT_TIME_NOT_ALLOWED);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean matchDate(AppointmentGroupDO g, LocalDate date) {
|
|
|
|
|
+ String type = g.getDateType();
|
|
|
|
|
+ if (type == null) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ switch (type) {
|
|
|
|
|
+ case "DAILY":
|
|
|
|
|
+ return true;
|
|
|
|
|
+ case "WEEKLY":
|
|
|
|
|
+ return matchInCommaList(g.getWeekDays(), date.getDayOfWeek().getValue());
|
|
|
|
|
+ case "MONTHLY":
|
|
|
|
|
+ return matchInCommaList(g.getMonthDays(), date.getDayOfMonth(), date.lengthOfMonth());
|
|
|
|
|
+ case "CUSTOM":
|
|
|
|
|
+ return matchCustomDates(g.getCustomDates(), date);
|
|
|
|
|
+ default:
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean matchInCommaList(String csv, int target) {
|
|
|
|
|
+ return matchInCommaList(csv, target, -1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean matchInCommaList(String csv, int target, int monthLength) {
|
|
|
|
|
+ if (csv == null || csv.trim().isEmpty()) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (String s : csv.split(",")) {
|
|
|
|
|
+ Integer v = Integer.valueOf(s.trim());
|
|
|
|
|
+ if (v > 0) {
|
|
|
|
|
+ if (v == target) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (monthLength > 0) { // 负数表示倒数日
|
|
|
|
|
+ if (monthLength + 1 + v == target) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean matchCustomDates(String customDates, LocalDate date) {
|
|
|
|
|
+ if (customDates == null || customDates.trim().isEmpty()) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ List<String> dates = JsonUtils.parseArray(customDates, String.class);
|
|
|
|
|
+ if (dates == null) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ String ds = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
|
|
|
|
|
+ return dates.contains(ds);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|