commit
This commit is contained in:
+14
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 新增/修改
|
||||
|
||||
+10
@@ -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<NutMap> listDailyQuota(String id);
|
||||
|
||||
/**
|
||||
* 复制上一年度目的地到目标年度,只复制目的地基础信息,不复制报名数据。
|
||||
*
|
||||
|
||||
+158
@@ -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<NutMap> 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<String, TheRapyRecuperationBaseDailyQuota> quotaMap = queryDailyQuotaMap(id);
|
||||
Map<String, Integer> enrollCountMap = countDailyQuotaByEnroll(management);
|
||||
Set<String> holidays = queryHolidaySet(management.getYear());
|
||||
List<Integer> 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<NutMap> 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<String, TheRapyRecuperationBaseDailyQuota> queryDailyQuotaMap(String id) {
|
||||
List<TheRapyRecuperationBaseDailyQuota> 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<String, Integer> 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<TheRapyRecuperationEnroll> enrolls = dao().query(TheRapyRecuperationEnroll.class, cnd);
|
||||
Map<String, Integer> 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<String> queryHolidaySet(Integer year) {
|
||||
List<SysHoliday> holidayList = dao().query(SysHoliday.class, Cnd.where("year(day)", "=", year).asc("day"));
|
||||
Set<String> holidays = holidayList.stream().map(SysHoliday::getDay).collect(Collectors.toCollection(HashSet::new));
|
||||
Map<Integer, List<SysHoliday>> 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<String> holidays, List<Integer> 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<LocalDate> 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<LocalDate> days = new ArrayList<>();
|
||||
LocalDate current = start;
|
||||
while (!current.isAfter(end)) {
|
||||
days.add(current);
|
||||
current = current.plusDays(1);
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制上一年度目的地到目标年度,排序号从当前最大排序号后递增,避免复用历史目的地排序号。
|
||||
* 复制时只生成目的地基础数据,不复制报名记录,防止目标年度业务挂到旧年度报名数据。
|
||||
|
||||
@@ -1,56 +1,74 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="panel panel-default mt20" style="border: none">
|
||||
<div class="panel-heading" style="background: #fafafa;border:1px solid #ebeef5;border-bottom: none">
|
||||
<h3 class="panel-title">
|
||||
基地信息
|
||||
</h3>
|
||||
</div>
|
||||
<el-descriptions :column="2" border class="viewTable">
|
||||
<el-descriptions-item label="年度">{{ viewData.year }}</el-descriptions-item>
|
||||
<el-descriptions-item label="排序编号">{{ viewData.sortNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="酒店名称">{{ viewData.baseName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="区域性质">{{ viewData.regionalNature }}</el-descriptions-item>
|
||||
<!-- (校工会创建AND自由模式) OR (分工会创建) -->
|
||||
<!-- <template v-if="(viewData.createMode === 2 && viewData.signUpMode === 2 ) || viewData.createMode === 1">-->
|
||||
<!--<el-descriptions-item label="出行天数">{{ viewData.regionalNature=5?"五天四夜":"三天两夜" }}</el-descriptions-item>-->
|
||||
<el-descriptions-item label="酒店联系人">{{ viewData.baseContactPerson }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人电话">{{ viewData.baseContactNumber }}</el-descriptions-item>
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<el-tab-pane label="基地信息" name="baseInfo">
|
||||
<div class="panel panel-default mt20" style="border: none">
|
||||
<div class="panel-heading" style="background: #fafafa;border:1px solid #ebeef5;border-bottom: none">
|
||||
<h3 class="panel-title">
|
||||
基地信息
|
||||
</h3>
|
||||
</div>
|
||||
<el-descriptions :column="2" border class="viewTable">
|
||||
<el-descriptions-item label="年度">{{ viewData.year }}</el-descriptions-item>
|
||||
<el-descriptions-item label="排序编号">{{ viewData.sortNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="酒店名称">{{ viewData.baseName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="区域性质">{{ viewData.regionalNature }}</el-descriptions-item>
|
||||
<!-- (校工会创建AND自由模式) OR (分工会创建) -->
|
||||
<!-- <template v-if="(viewData.createMode === 2 && viewData.signUpMode === 2 ) || viewData.createMode === 1">-->
|
||||
<!--<el-descriptions-item label="出行天数">{{ viewData.regionalNature=5?"五天四夜":"三天两夜" }}</el-descriptions-item>-->
|
||||
<el-descriptions-item label="酒店联系人">{{ viewData.baseContactPerson }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人电话">{{ viewData.baseContactNumber }}</el-descriptions-item>
|
||||
|
||||
<!--</template>-->
|
||||
<!--</template>-->
|
||||
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div class="panel panel-default mt20" style="border: none">
|
||||
<div class="panel-heading" style="background: #fafafa;border:1px solid #ebeef5;border-bottom: none">
|
||||
<h3 class="panel-title">
|
||||
旅行社信息
|
||||
</h3>
|
||||
</div>
|
||||
<el-descriptions :column="2" border class="viewTable">
|
||||
<el-descriptions-item :span="2" label="旅行社名称">
|
||||
{{ viewData.travelAgency.travelAgencyName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人">{{ viewData.travelAgency.contact }}</el-descriptions-item>
|
||||
<el-descriptions-item label="电话">{{ viewData.travelAgency.contactMobileNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ viewData.travelAgency.email }}</el-descriptions-item>
|
||||
<el-descriptions-item label="官网">
|
||||
<a :href="viewData.travelAgency.officialWebsite" class="text-primary" target="_blank">
|
||||
{{ viewData.travelAgency.officialWebsite }}
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div class="panel panel-default mt20" style="border-color: #ebeef5">
|
||||
<div class="panel-heading" style="background: #fafafa;border-color: #ebeef5">
|
||||
<h3 class="panel-title">
|
||||
详细信息
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div v-html="viewData.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div class="panel panel-default mt20" style="border: none">
|
||||
<div class="panel-heading" style="background: #fafafa;border:1px solid #ebeef5;border-bottom: none">
|
||||
<h3 class="panel-title">
|
||||
旅行社信息
|
||||
</h3>
|
||||
</div>
|
||||
<el-descriptions :column="2" border class="viewTable">
|
||||
<el-descriptions-item :span="2" label="旅行社名称">
|
||||
{{ viewData.travelAgency.travelAgencyName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人">{{ viewData.travelAgency.contact }}</el-descriptions-item>
|
||||
<el-descriptions-item label="电话">{{ viewData.travelAgency.contactMobileNumber }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ viewData.travelAgency.email }}</el-descriptions-item>
|
||||
<el-descriptions-item label="官网">
|
||||
<a :href="viewData.travelAgency.officialWebsite" class="text-primary" target="_blank">
|
||||
{{ viewData.travelAgency.officialWebsite }}
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div class="panel panel-default mt20" style="border-color: #ebeef5">
|
||||
<div class="panel-heading" style="background: #fafafa;border-color: #ebeef5">
|
||||
<h3 class="panel-title">
|
||||
详细信息
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div v-html="viewData.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="房间剩余情况" name="dailyQuota">
|
||||
<el-table :data="quotaRows" v-loading="quotaLoading" border class="mt20" size="small" style="width: 100%">
|
||||
<el-table-column align="center" label="可报名日期" prop="quotaDate"></el-table-column>
|
||||
<el-table-column align="center" label="每日最高人数" prop="maxSignCount"></el-table-column>
|
||||
<el-table-column align="center" label="已占用人数" prop="usedSignCount"></el-table-column>
|
||||
<el-table-column align="center" label="剩余人数" prop="remainingText"></el-table-column>
|
||||
<el-table-column align="center" label="状态">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.full ? 'danger' : 'success'" size="small">{{ scope.row.statusText }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user