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 bd8bcaf..07a163d 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 @@ -205,6 +205,20 @@ public class TheRapyRecuperationBaseManagerController { return dao.fetchLinks(management, "travelAgency"); } + /** + * 查询酒店每日名额占用和剩余情况。 + * + * @param id 酒店/目的地Id + * @return 每日名额列表 + */ + @At("/listDailyQuota") + @ViReturn + @RequiresAuthentication + public Object listDailyQuota(@Param("id") String id) { + Assert.notBlank(id); + return theRapyRecuperationBaseManagerService.listDailyQuota(id); + } + /** * 新增/修改 diff --git a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/baseManage/TheRapyRecuperationBaseManagerService.java b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/baseManage/TheRapyRecuperationBaseManagerService.java index a9895bc..93cc544 100644 --- a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/baseManage/TheRapyRecuperationBaseManagerService.java +++ b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/baseManage/TheRapyRecuperationBaseManagerService.java @@ -2,6 +2,8 @@ package io.v.nutz.zhgh.therapyRecuperation.service.baseManage; import org.nutz.lang.util.NutMap; +import java.util.List; + /** * @Author JyuHsin * @Date 2023/6/7 @@ -11,6 +13,14 @@ public interface TheRapyRecuperationBaseManagerService { NutMap selectBaseAllInfo(String id); + /** + * 查询酒店每日名额占用和剩余情况。 + * + * @param id 酒店/目的地Id + * @return 每日名额列表 + */ + List listDailyQuota(String id); + /** * 复制上一年度目的地到目标年度,只复制目的地基础信息,不复制报名数据。 * diff --git a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/impl/baseManage/TheRapyRecuperationBaseManagerServiceImpl.java b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/impl/baseManage/TheRapyRecuperationBaseManagerServiceImpl.java index a217fcf..72c3e09 100644 --- a/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/impl/baseManage/TheRapyRecuperationBaseManagerServiceImpl.java +++ b/src/main/java/io/v/nutz/zhgh/therapyRecuperation/service/impl/baseManage/TheRapyRecuperationBaseManagerServiceImpl.java @@ -4,9 +4,13 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import io.v.nutz.base.service.impl.BaseServiceImpl; import io.v.nutz.base.utils.Vi; +import io.v.nutz.sys.models.SysHoliday; import io.v.nutz.web.commons.utils.ShiroUtil; +import io.v.nutz.zhgh.therapyRecuperation.constant.TheRapyRecuperationState; +import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationBaseDailyQuota; import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationBaseManagement; import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationConfig; +import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationEnroll; import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationLot; import io.v.nutz.zhgh.therapyRecuperation.service.TheRapyRecuperationConfigService; import io.v.nutz.zhgh.therapyRecuperation.service.baseManage.TheRapyRecuperationBaseManagerService; @@ -18,11 +22,21 @@ import org.nutz.dao.sql.Sql; import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Lang; import org.nutz.lang.util.NutMap; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; /** * @Author JyuHsin @@ -63,6 +77,150 @@ public class TheRapyRecuperationBaseManagerServiceImpl extends BaseServiceImpl i return nutMap; } + /** + * 查询酒店每日名额占用和剩余情况,优先使用每日名额占用表,未初始化的历史日期按报名表兜底统计。 + * + * @param id 酒店/目的地Id + * @return 每日名额列表 + */ + @Override + public List listDailyQuota(String id) { + TheRapyRecuperationBaseManagement management = dao().fetch(TheRapyRecuperationBaseManagement.class, id); + if (management == null) { + return Collections.emptyList(); + } + TheRapyRecuperationConfig config = configService.requireByYear(management.getYear()); + if (config == null || config.getFragmentPlayStartTime() == null || config.getFragmentPlayEndTime() == null) { + return Collections.emptyList(); + } + + Map quotaMap = queryDailyQuotaMap(id); + Map enrollCountMap = countDailyQuotaByEnroll(management); + Set holidays = queryHolidaySet(management.getYear()); + List weeks = management.getWeekCheckIn(); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDate startDate = LocalDate.parse(DateUtil.formatDate(config.getFragmentPlayStartTime()), formatter); + LocalDate endDate = LocalDate.parse(DateUtil.formatDate(config.getFragmentPlayEndTime()), formatter); + List rows = new ArrayList<>(); + LocalDate currentDate = startDate; + while (!currentDate.isAfter(endDate)) { + String currentDateString = currentDate.format(formatter); + if (isDisabledQuotaDate(currentDate, currentDateString, holidays, weeks)) { + currentDate = currentDate.plusDays(1); + continue; + } + TheRapyRecuperationBaseDailyQuota quota = quotaMap.get(currentDateString); + int maxSignCount = quota != null && quota.getMaxSignCount() != null ? quota.getMaxSignCount() : Objects.requireNonNullElse(management.getMaxSignCount(), 0); + int usedSignCount = quota != null && quota.getUsedSignCount() != null ? quota.getUsedSignCount() : enrollCountMap.getOrDefault(currentDateString, 0); + rows.add(buildDailyQuotaRow(currentDateString, maxSignCount, usedSignCount)); + currentDate = currentDate.plusDays(1); + } + return rows; + } + + private Map queryDailyQuotaMap(String id) { + List quotas = dao().query(TheRapyRecuperationBaseDailyQuota.class, + Cnd.where("baseManagementId", "=", id).asc("quotaDate")); + return quotas.stream().collect(Collectors.toMap(TheRapyRecuperationBaseDailyQuota::getQuotaDate, quota -> quota, (oldValue, newValue) -> newValue)); + } + + /** + * 报名表兜底统计只用于未生成每日占用表的日期,保证历史数据在查看页仍然可见。 + * + * @param management 酒店/目的地 + * @return key为日期,value为覆盖该日期的报名人数 + */ + private Map countDailyQuotaByEnroll(TheRapyRecuperationBaseManagement management) { + Cnd cnd = 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)", "=", management.getYear()); + List enrolls = dao().query(TheRapyRecuperationEnroll.class, cnd); + Map countMap = new HashMap<>(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + for (TheRapyRecuperationEnroll enroll : enrolls) { + for (LocalDate day : parseSpecificTimeDays(enroll.getSpecificTime(), management.getYear())) { + String dayString = day.format(formatter); + countMap.put(dayString, countMap.getOrDefault(dayString, 0) + 1); + } + } + return countMap; + } + + private Set queryHolidaySet(Integer year) { + List holidayList = dao().query(SysHoliday.class, Cnd.where("year(day)", "=", year).asc("day")); + Set holidays = holidayList.stream().map(SysHoliday::getDay).collect(Collectors.toCollection(HashSet::new)); + Map> listMap = holidayList.stream().collect(Collectors.groupingBy(o -> DateUtil.month(DateUtil.parse(o.getDay())))); + listMap.forEach((k, v) -> { + SysHoliday sysHoliday = v.get(0); + Date date = DateUtil.parse(sysHoliday.getDay()); + holidays.add(DateUtil.formatDate(date)); + }); + return holidays; + } + + private boolean isDisabledQuotaDate(LocalDate currentDate, String currentDateString, Set holidays, List weeks) { + if (holidays.contains(currentDateString)) { + return true; + } + int dayOfWeek = currentDate.getDayOfWeek().getValue() % 7; + return Lang.isNotEmpty(weeks) && !weeks.contains(dayOfWeek); + } + + private NutMap buildDailyQuotaRow(String quotaDate, int maxSignCount, int usedSignCount) { + int remainingSignCount = maxSignCount <= 0 ? 0 : Math.max(maxSignCount - usedSignCount, 0); + boolean full = maxSignCount > 0 && usedSignCount >= maxSignCount; + String statusText = maxSignCount <= 0 ? "不限制" : full ? "已满" : "未满"; + return new NutMap() + .setv("quotaDate", quotaDate) + .setv("maxSignCount", maxSignCount) + .setv("usedSignCount", usedSignCount) + .setv("remainingSignCount", remainingSignCount) + .setv("remainingText", maxSignCount <= 0 ? "不限制" : String.valueOf(remainingSignCount)) + .setv("statusText", statusText) + .setv("full", full); + } + + /** + * 把“06月01日-06月03日”格式的报名时间拆成指定年度的逐日日期。 + * + * @param specificTime 报名时间段 + * @param year 年度 + * @return 报名时间段包含的所有日期 + */ + private List parseSpecificTimeDays(String specificTime, Integer year) { + if (StrUtil.isBlank(specificTime) || !specificTime.contains("-")) { + return Collections.emptyList(); + } + String[] times = specificTime.split("-"); + if (times.length != 2 || StrUtil.isBlank(times[0]) || StrUtil.isBlank(times[1])) { + return Collections.emptyList(); + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日"); + LocalDate start; + LocalDate end; + try { + Integer targetYear = year == null ? DateUtil.thisYear() : year; + start = LocalDate.parse(targetYear + "年" + times[0].trim(), formatter); + end = LocalDate.parse(targetYear + "年" + times[1].trim(), formatter); + } catch (Exception e) { + return Collections.emptyList(); + } + if (end.isBefore(start)) { + end = end.plusYears(1); + } + + List days = new ArrayList<>(); + LocalDate current = start; + while (!current.isAfter(end)) { + days.add(current); + current = current.plusDays(1); + } + return days; + } + /** * 复制上一年度目的地到目标年度,排序号从当前最大排序号后递增,避免复用历史目的地排序号。 * 复制时只生成目的地基础数据,不复制报名记录,防止目标年度业务挂到旧年度报名数据。 diff --git a/src/main/resources/static/components/theRapyRecuperation/BaseManageInfo.vue b/src/main/resources/static/components/theRapyRecuperation/BaseManageInfo.vue index ad9197f..8d8b1ad 100644 --- a/src/main/resources/static/components/theRapyRecuperation/BaseManageInfo.vue +++ b/src/main/resources/static/components/theRapyRecuperation/BaseManageInfo.vue @@ -1,56 +1,74 @@ @@ -63,17 +81,46 @@ module.exports = { travelAgency: {} }, modifyBdList: [], + activeTab: 'baseInfo', + currentBaseId: '', + quotaLoading: false, + quotaLoaded: false, + quotaRows: [], } }, methods: { openView(id, obj) { - this.modifyBdList = obj + this.$set(this, 'modifyBdList', obj) + this.$set(this, 'currentBaseId', id) + this.$set(this, 'activeTab', 'baseInfo') + this.$set(this, 'quotaLoaded', false) + this.$set(this, 'quotaRows', []) this.findOne(id) }, async findOne(id) { const res = await $.get('/platform/theRapyRecuperation/baseManagement/selectBaseManageById/' + id) if (res.code === 0) { - this.viewData = res.data + this.$set(this, 'viewData', res.data) + } + }, + handleTabClick(tab) { + if (tab.name === 'dailyQuota' && !this.quotaLoaded) { + this.findDailyQuota() + } + }, + async findDailyQuota() { + if (!this.currentBaseId) { + return + } + this.$set(this, 'quotaLoading', true) + try { + const res = await $.get('/platform/theRapyRecuperation/baseManagement/listDailyQuota', {id: this.currentBaseId}) + if (res.code === 0) { + this.$set(this, 'quotaRows', res.data || []) + this.$set(this, 'quotaLoaded', true) + } + } finally { + this.$set(this, 'quotaLoading', false) } }, },