diff --git a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/controller/baseManage/TheRapyRecuperationBaseManagerController.java b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/controller/baseManage/TheRapyRecuperationBaseManagerController.java index 24ca9c8..bd8bcaf 100644 --- a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/controller/baseManage/TheRapyRecuperationBaseManagerController.java +++ b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/controller/baseManage/TheRapyRecuperationBaseManagerController.java @@ -24,6 +24,7 @@ import io.v.nutz.web.commons.utils.ShiroUtil; import io.v.nutz.zhgh.therapyRecuperation.constant.TheRapyRecuperationState; import io.v.nutz.zhgh.therapyRecuperation.model.*; import io.v.nutz.zhgh.therapyRecuperation.service.TheRapyRecuperationConfigService; +import io.v.nutz.zhgh.therapyRecuperation.service.TheRapyRecuperationEnrollService; import io.v.nutz.zhgh.therapyRecuperation.service.baseManage.TheRapyRecuperationBaseManagerService; import org.apache.poi.ss.usermodel.Workbook; import org.apache.shiro.authz.annotation.Logical; @@ -79,6 +80,9 @@ public class TheRapyRecuperationBaseManagerController { @Inject private TheRapyRecuperationConfigService configService; + @Inject + private TheRapyRecuperationEnrollService enrollService; + @At("") @Ok("beetl:/platform/theRapyRecuperation/baseManage/baseManagement.html") @RequiresPermissions("theRapyRecuperation.TheBaseManagement") @@ -260,6 +264,7 @@ public class TheRapyRecuperationBaseManagerController { } dao.delete(TheRapyRecuperationBaseManagement.class, id); dao.clear(TheRapyRecuperationEnroll.class, Cnd.where("takePartInBaseManagementId", "=", id)); + dao.clear(TheRapyRecuperationBaseDailyQuota.class, Cnd.where("baseManagementId", "=", id)); return null; } @@ -338,7 +343,7 @@ public class TheRapyRecuperationBaseManagerController { }); List weeks = management.getWeekCheckIn(); - Set fullDays = getFullBaseManagementDays(management); + Set fullDays = enrollService.getFullBaseManagementDays(management); // 解析日期 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); @@ -393,25 +398,7 @@ public class TheRapyRecuperationBaseManagerController { * @return 已达到每日最高报名人数的日期集合 */ private Set getFullBaseManagementDays(TheRapyRecuperationBaseManagement management) { - if (management == null || management.getMaxSignCount() == null || management.getMaxSignCount() <= 0) { - return Collections.emptySet(); - } - List enrolls = dao.query(TheRapyRecuperationEnroll.class, - Cnd.where("takePartInBaseManagementId", "=", management.getId()) - .and("specificTime", "is not", null) - .and("isNormal", "=", true) - .and("stateId", "in", Lang.array(TheRapyRecuperationState.UNIT, TheRapyRecuperationState.LINEUNIT, TheRapyRecuperationState.SCHOOL, TheRapyRecuperationState.PASS)) - .and("YEAR(signingUptime)", "=", DateUtil.thisYear())); - Map dayCountMap = new HashMap<>(); - for (TheRapyRecuperationEnroll enroll : enrolls) { - for (String day : parseSpecificTimeDays(enroll.getSpecificTime())) { - dayCountMap.put(day, dayCountMap.getOrDefault(day, 0) + 1); - } - } - return dayCountMap.entrySet().stream() - .filter(entry -> entry.getValue() >= management.getMaxSignCount()) - .map(Map.Entry::getKey) - .collect(Collectors.toSet()); + return enrollService.getFullBaseManagementDays(management); } /** diff --git a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/controller/process/TheRapyRecuperationEnrollController.java b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/controller/process/TheRapyRecuperationEnrollController.java index c04ebd1..732453e 100644 --- a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/controller/process/TheRapyRecuperationEnrollController.java +++ b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/controller/process/TheRapyRecuperationEnrollController.java @@ -203,10 +203,14 @@ public class TheRapyRecuperationEnrollController { if (resultMap.containsKey(false)) { return Result.error(resultMap.get(false)); } + String signUpError; if (StrUtil.isBlank(enrollInfo.getId())) { - enrollService.doSignUpForHotel(enrollInfo); + signUpError = enrollService.doSignUpForHotel(enrollInfo); } else { - enrollService.updateSignUpHotel(enrollInfo); + signUpError = enrollService.updateSignUpHotel(enrollInfo); + } + if (StrUtil.isNotBlank(signUpError)) { + return Result.error(signUpError); } return null; } diff --git a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/controller/statistics/TheRapyRecuperationLineFragmentController.java b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/controller/statistics/TheRapyRecuperationLineFragmentController.java index 7cf9e46..35e15a8 100644 --- a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/controller/statistics/TheRapyRecuperationLineFragmentController.java +++ b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/controller/statistics/TheRapyRecuperationLineFragmentController.java @@ -304,10 +304,12 @@ public class TheRapyRecuperationLineFragmentController { List enrollIds = enrolls.stream().map(TheRapyRecuperationEnroll::getId).collect(Collectors.toList()); List bedIds = enrolls.stream().map(TheRapyRecuperationEnroll::getBedInfoId).collect(Collectors.toList()); dao.clear(TheRapyRecuperationEnroll.class, Cnd.where("takePartInBaseManagementId", "=", baseId)); + dao.clear(TheRapyRecuperationBaseDailyQuota.class, Cnd.where("baseManagementId", "=", baseId)); dao.clear(TheRapyRecuperationEnrollCompanion.class, Cnd.where("trreId", "in", enrollIds)); dao.clear(TheRapyRecuperationEnrollBed.class, Cnd.where("id", "in", bedIds)); } else { List idList = enrolls.stream().map(TheRapyRecuperationEnroll::getId).collect(Collectors.toList()); + enrolls.forEach(enrollService::releaseBaseManagementDailyQuota); dao.update(TheRapyRecuperationEnroll.class, Chain.make("isNormal", false), Cnd.where("id", "in", idList)); } return null; diff --git a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/model/TheRapyRecuperationBaseDailyQuota.java b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/model/TheRapyRecuperationBaseDailyQuota.java new file mode 100644 index 0000000..b95413a --- /dev/null +++ b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/model/TheRapyRecuperationBaseDailyQuota.java @@ -0,0 +1,49 @@ +package io.v.nutz.zhgh.therapyRecuperation.model; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +/** + * 分段疗休养酒店每日名额占用表 + */ +@Data +@Table +@TableIndexes({ + @Index(name = "INDEX_TRR_BASE_DAILY_QUOTA_BASE_DATE", fields = {"baseManagementId", "quotaDate"}, unique = true) +}) +@Accessors(chain = true) +public class TheRapyRecuperationBaseDailyQuota { + + @Name + @Comment("id") + @PrevInsert(uu32 = true) + private String id; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + @Comment("酒店/目的地Id") + private String baseManagementId; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 10) + @Comment("占用日期") + private String quotaDate; + + @Column + @ColDefine(type = ColType.INT) + @Comment("年度") + private Integer year; + + @Column + @ColDefine(type = ColType.INT) + @Comment("每日最高报名人数") + private Integer maxSignCount; + + @Column + @ColDefine(type = ColType.INT) + @Default("0") + @Comment("已占用报名人数") + private Integer usedSignCount; +} diff --git a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/TheRapyRecuperationEnrollService.java b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/TheRapyRecuperationEnrollService.java index ad16425..11d5bd7 100644 --- a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/TheRapyRecuperationEnrollService.java +++ b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/TheRapyRecuperationEnrollService.java @@ -5,6 +5,7 @@ import cn.wizzer.framework.base.Result; import io.v.nutz.base.query.PageForm; import io.v.nutz.base.service.ViService; import io.v.nutz.sys.models.Sys_union; +import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationBaseManagement; import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationEnroll; import org.nutz.dao.Cnd; import org.nutz.lang.util.NutMap; @@ -12,6 +13,7 @@ import org.nutz.lang.util.NutMap; import java.io.File; import java.util.List; import java.util.Map; +import java.util.Set; /** * 疗休养报名service @@ -62,9 +64,24 @@ public interface TheRapyRecuperationEnrollService extends ViService getFullBaseManagementDays(TheRapyRecuperationBaseManagement management); + + /** + * 释放报名记录占用的酒店每日名额。 + * + * @param enrollInfo 报名记录 + */ + void releaseBaseManagementDailyQuota(TheRapyRecuperationEnroll enrollInfo); /** * 变更报名旅行社 diff --git a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/impl/TheRapyRecuperationEnrollServiceImpl.java b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/impl/TheRapyRecuperationEnrollServiceImpl.java index 647460d..dcd116d 100644 --- a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/impl/TheRapyRecuperationEnrollServiceImpl.java +++ b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/impl/TheRapyRecuperationEnrollServiceImpl.java @@ -25,6 +25,7 @@ import io.v.nutz.zhgh.therapyRecuperation.service.TheRapyRecuperationConfigServi import io.v.nutz.zhgh.therapyRecuperation.service.TheRapyRecuperationEnrollService; import io.v.nutz.zhgh.therapyRecuperation.service.TheRapyRecuperationLineService; import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Chain; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; @@ -498,7 +499,13 @@ public class TheRapyRecuperationEnrollServiceImpl extends ViServiceImpl getFullBaseManagementDays(TheRapyRecuperationBaseManagement management) { + if (management == null || management.getMaxSignCount() == null || management.getMaxSignCount() <= 0) { + return Collections.emptySet(); + } + Integer year = management.getYear() == null ? DateUtil.thisYear() : management.getYear(); + Cnd quotaCnd = Cnd.where("baseManagementId", "=", management.getId()) + .and("year", "=", year) + .and("maxSignCount", ">", 0) + .and(new Static("usedSignCount >= maxSignCount")); + List quotaList = dao().query(TheRapyRecuperationBaseDailyQuota.class, quotaCnd); + Set fullDays = quotaList.stream() + .map(TheRapyRecuperationBaseDailyQuota::getQuotaDate) + .collect(Collectors.toSet()); + fullDays.addAll(getFullBaseManagementDaysByEnroll(management, year)); + return fullDays; + } + + /** + * 从历史报名记录兜底统计已满日期,避免每日占用表尚未初始化时日历漏放已满日期。 + * + * @param management 酒店/目的地信息 + * @param year 年度 + * @return 已满日期集合,格式 yyyy-MM-dd + */ + private Set getFullBaseManagementDaysByEnroll(TheRapyRecuperationBaseManagement management, Integer year) { + List enrolls = dao().query(TheRapyRecuperationEnroll.class, + Cnd.where("takePartInBaseManagementId", "=", management.getId()) + .and("specificTime", "is not", null) + .and("isNormal", "=", true) + .and("stateId", "in", Lang.array(TheRapyRecuperationState.UNIT, TheRapyRecuperationState.LINEUNIT, TheRapyRecuperationState.SCHOOL, TheRapyRecuperationState.PASS)) + .and("YEAR(signingUptime)", "=", year)); + Map dayCountMap = new HashMap<>(); + DateTimeFormatter targetFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + for (TheRapyRecuperationEnroll enroll : enrolls) { + for (LocalDate day : parseSpecificTimeDays(enroll.getSpecificTime(), year)) { + String dayText = day.format(targetFormatter); + dayCountMap.put(dayText, dayCountMap.getOrDefault(dayText, 0) + 1); + } + } + return dayCountMap.entrySet().stream() + .filter(entry -> entry.getValue() >= management.getMaxSignCount()) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + /** + * 占用分段疗休养酒店每日名额,任意一天名额不足都会释放本次已占用日期。 + * + * @param currentEnroll 当前报名信息 + * @return 占用失败提示;返回 null 表示占用成功或未配置每日上限 + */ + private String occupyBaseManagementDailyQuota(TheRapyRecuperationEnroll currentEnroll) { + if (currentEnroll == null || StrUtil.isBlank(currentEnroll.getTakePartInBaseManagementId()) + || StrUtil.isBlank(currentEnroll.getSpecificTime())) { + return null; + } + TheRapyRecuperationBaseManagement management = dao().fetch(TheRapyRecuperationBaseManagement.class, currentEnroll.getTakePartInBaseManagementId()); + if (management == null || management.getMaxSignCount() == null || management.getMaxSignCount() <= 0) { + return null; + } + + Integer year = management.getYear() == null ? DateUtil.thisYear() : management.getYear(); + List currentDays = parseSpecificTimeDays(currentEnroll.getSpecificTime(), year); + List occupiedDays = new ArrayList<>(); + for (LocalDate currentDay : currentDays) { + ensureBaseDailyQuota(management, currentDay, year, currentEnroll.getId()); + String quotaDate = currentDay.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + int updateCount = dao().update(TheRapyRecuperationBaseDailyQuota.class, + Chain.make("maxSignCount", management.getMaxSignCount()).addSpecial("usedSignCount", "usedSignCount + 1"), + Cnd.where("baseManagementId", "=", management.getId()) + .and("quotaDate", "=", quotaDate) + .and("usedSignCount", "<", management.getMaxSignCount())); + if (updateCount == 0) { + releaseBaseManagementDailyQuota(management.getId(), management.getMaxSignCount(), occupiedDays); + return "该酒店%s报名人数已满,请重新选择出行时间".formatted(currentDay.format(DateTimeFormatter.ofPattern("MM月dd日"))); + } + occupiedDays.add(currentDay); + } + return null; + } + + /** + * 初始化单天库存行。首次创建时会从历史报名记录统计已占用人数,后续并发报名只做原子加一。 + * + * @param management 酒店/目的地信息 + * @param currentDay 需要初始化的日期 + * @param year 年度 + * @param excludeEnrollId 初始化历史占用时需要排除的报名记录Id + */ + private void ensureBaseDailyQuota(TheRapyRecuperationBaseManagement management, LocalDate currentDay, Integer year, String excludeEnrollId) { + String quotaDate = currentDay.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + Cnd quotaCnd = Cnd.where("baseManagementId", "=", management.getId()).and("quotaDate", "=", quotaDate); + TheRapyRecuperationBaseDailyQuota quota = dao().fetch(TheRapyRecuperationBaseDailyQuota.class, quotaCnd); + if (quota != null) { + dao().update(TheRapyRecuperationBaseDailyQuota.class, + Chain.make("maxSignCount", management.getMaxSignCount()).add("year", year), + quotaCnd); + return; + } + + synchronized ((management.getId() + ":" + quotaDate).intern()) { + quota = dao().fetch(TheRapyRecuperationBaseDailyQuota.class, quotaCnd); + if (quota != null) { + dao().update(TheRapyRecuperationBaseDailyQuota.class, + Chain.make("maxSignCount", management.getMaxSignCount()).add("year", year), + quotaCnd); + return; + } + TheRapyRecuperationBaseDailyQuota dailyQuota = new TheRapyRecuperationBaseDailyQuota() + .setBaseManagementId(management.getId()) + .setQuotaDate(quotaDate) + .setYear(year) + .setMaxSignCount(management.getMaxSignCount()) + .setUsedSignCount(countBaseManagementSignUserByDay(management.getId(), year, currentDay, excludeEnrollId)); + try { + dao().insert(dailyQuota); + } catch (Exception e) { + if (dao().fetch(TheRapyRecuperationBaseDailyQuota.class, quotaCnd) == null) { + throw Lang.wrapThrow(e); + } + } + } + } + + /** + * 释放报名记录占用的每日名额,用于修改报名和撤销报名。 + * + * @param enrollInfo 报名记录 + */ + @Override + public void releaseBaseManagementDailyQuota(TheRapyRecuperationEnroll enrollInfo) { + if (enrollInfo == null || StrUtil.isBlank(enrollInfo.getTakePartInBaseManagementId()) + || StrUtil.isBlank(enrollInfo.getSpecificTime())) { + return; + } + Integer year = fetchEnrollYear(enrollInfo); + TheRapyRecuperationBaseManagement management = dao().fetch(TheRapyRecuperationBaseManagement.class, enrollInfo.getTakePartInBaseManagementId()); + Integer maxSignCount = management == null ? null : management.getMaxSignCount(); + releaseBaseManagementDailyQuota(enrollInfo.getTakePartInBaseManagementId(), maxSignCount, parseSpecificTimeDays(enrollInfo.getSpecificTime(), year)); + } + + /** + * 恢复旧报名占用,用于修改报名失败后的状态回补,不再判断每日上限。 + * + * @param enrollInfo 旧报名记录 + */ + private void restoreBaseManagementDailyQuota(TheRapyRecuperationEnroll enrollInfo) { + if (enrollInfo == null || StrUtil.isBlank(enrollInfo.getTakePartInBaseManagementId()) + || StrUtil.isBlank(enrollInfo.getSpecificTime())) { + return; + } + TheRapyRecuperationBaseManagement management = dao().fetch(TheRapyRecuperationBaseManagement.class, enrollInfo.getTakePartInBaseManagementId()); + if (management == null || management.getMaxSignCount() == null || management.getMaxSignCount() <= 0) { + return; + } + Integer year = management.getYear() == null ? DateUtil.thisYear() : management.getYear(); + for (LocalDate day : parseSpecificTimeDays(enrollInfo.getSpecificTime(), year)) { + ensureBaseDailyQuota(management, day, year, enrollInfo.getId()); + dao().update(TheRapyRecuperationBaseDailyQuota.class, + Chain.make("maxSignCount", management.getMaxSignCount()).addSpecial("usedSignCount", "usedSignCount + 1"), + Cnd.where("baseManagementId", "=", management.getId()) + .and("quotaDate", "=", day.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))); + } + } + + /** + * 按日期释放酒店每日名额,释放时限定已占用人数大于 0,避免出现负数。 + * + * @param baseManagementId 酒店/目的地Id + * @param maxSignCount 当前每日最高人数 + * @param days 需要释放的日期集合 + */ + private void releaseBaseManagementDailyQuota(String baseManagementId, Integer maxSignCount, List days) { + if (StrUtil.isBlank(baseManagementId) || Lang.isEmpty(days)) { + return; + } + for (LocalDate day : days) { + String quotaDate = day.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + Chain chain = Chain.makeSpecial("usedSignCount", "usedSignCount - 1"); + if (maxSignCount != null && maxSignCount > 0) { + chain.add("maxSignCount", maxSignCount); + } + dao().update(TheRapyRecuperationBaseDailyQuota.class, + chain, + Cnd.where("baseManagementId", "=", baseManagementId) + .and("quotaDate", "=", quotaDate) + .and("usedSignCount", ">", 0)); + } + } + /** * 校验分段疗休养同一酒店每日最高报名人数,报名时间段中任意一天满员都不能继续报名。 * @@ -862,7 +1069,8 @@ public class TheRapyRecuperationEnrollServiceImpl extends ViServiceImpl currentDays = parseSpecificTimeDays(currentEnroll.getSpecificTime()); + Integer year = management.getYear() == null ? DateUtil.thisYear() : management.getYear(); + List currentDays = parseSpecificTimeDays(currentEnroll.getSpecificTime(), year); for (LocalDate currentDay : currentDays) { int signedCount = countBaseManagementSignUserByDay(currentEnroll, currentDay); if (signedCount + 1 > management.getMaxSignCount()) { @@ -880,18 +1088,36 @@ public class TheRapyRecuperationEnrollServiceImpl extends ViServiceImpl