package com.qmth.exam.reserve.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.qmth.boot.core.concurrent.service.ConcurrentService; import com.qmth.boot.core.exception.StatusException; import com.qmth.exam.reserve.bean.applytask.CurrentApplyTaskVO; import com.qmth.exam.reserve.bean.examsite.ExamSiteCacheBean; import com.qmth.exam.reserve.bean.login.LoginUser; import com.qmth.exam.reserve.bean.org.OrgInfo; import com.qmth.exam.reserve.bean.timeperiod.TimePeriodExamSiteBean; import com.qmth.exam.reserve.bean.timeperiod.TimePeriodExamSiteInfo; import com.qmth.exam.reserve.bean.timeperiod.TimePeriodExamSiteReq; import com.qmth.exam.reserve.bean.timeperiod.TimePeriodExamSiteVo; import com.qmth.exam.reserve.cache.CacheConstants; import com.qmth.exam.reserve.cache.impl.ApplyTaskCacheService; import com.qmth.exam.reserve.cache.impl.ExamSiteCacheService; import com.qmth.exam.reserve.cache.impl.OrgCacheService; import com.qmth.exam.reserve.dao.TimePeriodExamRoomDao; import com.qmth.exam.reserve.entity.ExamRoomEntity; import com.qmth.exam.reserve.entity.TimePeriodEntity; import com.qmth.exam.reserve.entity.TimePeriodExamRoomEntity; import com.qmth.exam.reserve.enums.Role; import com.qmth.exam.reserve.service.*; import com.qmth.exam.reserve.util.DateUtil; import com.qmth.exam.reserve.util.UnionUtil; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.redisson.api.RLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.*; import java.util.stream.Collectors; @Service public class TimePeriodExamRoomServiceImpl extends ServiceImpl implements TimePeriodExamRoomService { private static final Logger log = LoggerFactory.getLogger(TimePeriodExamRoomServiceImpl.class); @Autowired private ApplyTaskCacheService applyTaskCacheService; @Autowired private OrgCacheService orgCacheService; @Autowired private TimePeriodService timePeriodService; @Autowired private ExamSiteCacheService examSiteCacheService; @Autowired private ExamRoomService examRoomService; @Autowired private ConcurrentService concurrentService; @Autowired private ExamSiteService examSiteService; @Autowired private TimePeriodExamRoomService timePeriodExamRoomService; @Autowired private StudentApplyService studentApplyService; @Override public List ListDetail(LoginUser loginUser, Long examRoomId) { //获取当前机构 OrgInfo org = orgCacheService.currentOrg(); if (org == null) { log.warn("[考场排班设置列表]未找到当前机构"); return Collections.emptyList(); } //判断考场是否存在 ExamRoomEntity examRoom = examRoomService.getById(examRoomId); if (examRoom == null) { log.warn("[考场排班设置列表]未找到考场:{}", examRoomId); return Collections.emptyList(); } //教学点管理员权限限制 ExamSiteCacheBean examSite = examSiteCacheService.getExamSiteById(examRoom.getExamSiteId()); if (examSite == null) { log.warn("[考场排班设置列表]考场:{}未找到所属考点", examRoomId); return Collections.emptyList(); } if (loginUser.getRole().equals(Role.TEACHING) && !loginUser.getCategoryId().equals(examSite.getCategoryId())) { return Collections.emptyList(); } //获取当前任务 CurrentApplyTaskVO curApplyTask = applyTaskCacheService.currentApplyTask(org.getOrgId()); if (curApplyTask == null) { log.warn("[考场排班设置列表]机构:{},当前未有启用的任务", org.getOrgId()); return Collections.emptyList(); } // 所有的预约日期 List dateList = timePeriodService.listTimePeriodDate(curApplyTask.getTaskId()); if (CollectionUtils.isEmpty(dateList)) { log.warn("[考场排班设置列表]当前任务:{}未设置预约日期", curApplyTask.getTaskId()); return Collections.emptyList(); } //教学点管理员设置的所有的考点时段 List timePeriodExamRoomList = timePeriodService.listTimePeriodByExamRoomId(curApplyTask.getTaskId(), examRoomId); //学校管理员设置的所有预约时段 List timePeriodList = timePeriodService.listTimePeriodByTask(curApplyTask.getTaskId()); List resultList; if (CollectionUtils.isEmpty(timePeriodExamRoomList)) { resultList = timePeriodList; } else { //取并集 resultList = UnionUtil.unionByAttribute(timePeriodExamRoomList, timePeriodList, TimePeriodExamSiteBean::getTimePeriodId); } //按日期封装 List list = new ArrayList<>(); for (String date : dateList) { TimePeriodExamSiteVo timePeriodVo = new TimePeriodExamSiteVo(); timePeriodVo.setDateStr(getDateStr(date)); timePeriodVo.setTimePeriodList(filterTimePeriod(date, resultList, curApplyTask.getAllowApplyCancelDays())); list.add(timePeriodVo); } return list; } private String getDateStr(String date) { if (StringUtils.isEmpty(date)) { return ""; } String[] dateArr = date.split("-"); if (dateArr.length != 3) { return ""; } return dateArr[1] + "月" + dateArr[2] + "日"; } private List filterTimePeriod(String date, List timePeriodList, Integer canCancelDay) { // 参数校验 if (timePeriodList == null || date == null || canCancelDay == null || canCancelDay < 0) { return new ArrayList<>(); } List resultList = new ArrayList<>(); // 过滤符合条件的时间段 List filteredTimePeriodList = timePeriodList.stream() .filter(item -> DateUtil.getShortDateByLongTime(item.getStartTime()).equals(date)) .sorted(Comparator.comparing(TimePeriodExamSiteBean::getStartTime)) .collect(Collectors.toList()); // 计算可取消时间范围 Long longToday = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(new Date()) + " 00:00:00"); Date today = new Date(longToday); Date otherDay = DateUtil.addValues(today, Calendar.DAY_OF_MONTH, canCancelDay); long longOtherDay = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(otherDay) + " 23:59:59"); // 当前时间 long now = System.currentTimeMillis(); for (TimePeriodExamSiteBean time : filteredTimePeriodList) { // 检查时间是否有效 if (time.getStartTime() == null || time.getEndTime() == null) { continue; // 跳过无效时间段 } TimePeriodExamSiteInfo bean = new TimePeriodExamSiteInfo(); bean.setId(time.getId()); bean.setTimePeriodId(time.getTimePeriodId()); bean.setEnable(time.getEnable()); bean.setTimePeriodStr(DateUtil.getStartToEndTime(time.getStartTime(), time.getEndTime())); // 判断是否可编辑 boolean isEditable = isTimeEditable(now, time.getEndTime(), longOtherDay, time.getStartTime()); bean.setEditable(isEditable); resultList.add(bean); } return resultList; } private boolean isTimeEditable(long now, long endTime, long longOtherDay, long startTime) { return !(endTime < now || startTime < longOtherDay); } @Transactional(rollbackFor = Exception.class) @Override public void save(Long userId, Long examRoomId, List timePeriodExamRoomList) { if (concurrentService.isLocked(CacheConstants.LOCK_AUTO_APPLY)) { log.warn("[考场排班保存]系统自动预约中,不允许操作考场排班!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY); throw new StatusException("系统正在自动预约中,不允许修改"); } if (CollectionUtils.isEmpty(timePeriodExamRoomList)) { log.warn("[考场排班设置]时间段列表为空"); throw new StatusException("保存失败,时间段列表为空"); } // 获取当前机构 OrgInfo org = orgCacheService.currentOrg(); if (org == null) { log.warn("[考场排班设置]未找到当前机构"); throw new StatusException("保存失败,未找到当前机构"); } // 获取当前任务 CurrentApplyTaskVO curApplyTask = applyTaskCacheService.currentApplyTask(org.getOrgId()); if (curApplyTask == null) { log.warn("[考场排班设置]机构:{},当前未有启用的任务", org.getOrgId()); throw new StatusException("保存失败,未有启用的任务"); } // 校验考场信息 ExamRoomEntity examRoom = examRoomService.getById(examRoomId); if (examRoom == null || !examRoom.getEnable()) { log.warn("[考场排班设置]未找到或已被禁用的考场:{}", examRoomId); throw new StatusException("保存失败,考场不存在或已被禁用"); } // 校验考点信息 ExamSiteCacheBean examSiteCacheBean = examSiteCacheService.getExamSiteById(examRoom.getExamSiteId()); if (examSiteCacheBean == null) { log.warn("[考场排班设置]未找到考点:{}", examRoom.getExamSiteId()); throw new StatusException("保存失败,未找到考点"); } String lockKey = String.format(CacheConstants.LOCK_EXAM_SITE_CAPACITY, examRoom.getExamSiteId()); if (concurrentService.isLocked(lockKey)) { log.warn("[考场排班设置]考点剩余可约数量更新中,不允许操作修改!lockKey:{}", lockKey); throw new StatusException("系统正在更新可预约数量,不允许保存"); } //考点容量变更锁 String examSiteLockKey = String.format(CacheConstants.LOCK_EXAM_SITE_CHANGE_CAPACITY, examRoom.getExamSiteId()); RLock examSiteLock = (RLock) concurrentService.getLock(examSiteLockKey); try { if (!examSiteLock.tryLock()) { log.warn("[考场排班设置] 获取锁失败,同一个教学点不允许同时操作一个考点的容量修改, lockKey:{}", examSiteLockKey); throw new StatusException("其他老师正在修改该考点的容量,请稍后重试!"); } else { // 获取已有时段数据 List existingTimePeriods = timePeriodService.listTimePeriodByExamRoomId(curApplyTask.getTaskId(), examRoomId); Map existingMap = existingTimePeriods.stream() .collect(Collectors.toMap(TimePeriodExamSiteBean::getId, t -> t)); // 过滤出可编辑的数据 List editableList = timePeriodExamRoomList.stream() .filter(TimePeriodExamSiteReq::getEditable) .collect(Collectors.toList()); if (editableList.isEmpty()) { log.warn("[考场排班设置]无可编辑数据"); return; } // 构建实体列表 List entityList = editableList.stream() .map(req -> createTimePeriodExamRoomEntity(req, examRoomId, userId)) .collect(Collectors.toList()); // 分离新增和更新 List toBeSaved = new ArrayList<>(); List toBeUpdated = new ArrayList<>(); entityList.forEach(e -> { if (e.getId() == null) { toBeSaved.add(e); } else { toBeUpdated.add(e); } }); // 检查是否可编辑 List timePeriodIdsToEdit = new ArrayList<>(); toBeSaved.forEach(e -> timePeriodIdsToEdit.add(e.getTimePeriodId())); toBeUpdated.forEach(e -> { TimePeriodExamSiteBean old = existingMap.get(e.getId()); if (old != null && !old.getEnable().equals(e.getEnable())) { timePeriodIdsToEdit.add(e.getTimePeriodId()); } }); canEdit(timePeriodIdsToEdit, curApplyTask); // 待更新、保存时段ID Set updatedTimePeriodIds = new HashSet<>(); toBeSaved.forEach(e -> updatedTimePeriodIds.add(e.getTimePeriodId())); toBeUpdated.forEach(e -> updatedTimePeriodIds.add(e.getTimePeriodId())); // 保存前先记录旧容量 Map oldCapacityMap = new HashMap<>(); for (Long timePeriodId : updatedTimePeriodIds) { int capacity = examSiteService.getExamSiteTimePeriodCapacity(examSiteCacheBean.getExamSiteId(), timePeriodId); oldCapacityMap.put(timePeriodId, capacity); } List verifyList = new ArrayList<>(); //只处理禁用的时段 List newTimePeriodList = toBeSaved.stream().filter(item -> !item.getEnable()).collect(Collectors.toList()); if (!newTimePeriodList.isEmpty()) { verifyList.addAll(newTimePeriodList); } if (!toBeUpdated.isEmpty()) { verifyList.addAll(toBeUpdated); } //容量验证 int availableCount, haveApplyCount, oldCount; for (TimePeriodExamRoomEntity toUpdate : verifyList) { TimePeriodExamSiteBean bean; if (toUpdate.getId() == null) { bean = new TimePeriodExamSiteBean(); bean.setEnable(toUpdate.getEnable()); bean.setTimePeriodId(toUpdate.getTimePeriodId()); } else { bean = existingMap.get(toUpdate.getId()); } //只处理从开启到关闭的时段 if (bean != null && (bean.getId() == null || (bean.getEnable() && !toUpdate.getEnable()))) { // 剩余的容量,从缓存中获取 availableCount = applyTaskCacheService.getApplyAvailableCount(examSiteCacheBean.getExamSiteId(), toUpdate.getTimePeriodId()); // 修改之前的容量 oldCount = oldCapacityMap.getOrDefault(toUpdate.getTimePeriodId(), 0); // 已预约的容量 haveApplyCount = oldCount - availableCount; // 关闭之后的剩余容量 int remainCount = oldCount - examRoom.getCapacity(); log.warn("haveApplyCount:{}, remainCount:{}", haveApplyCount, remainCount); if (haveApplyCount > remainCount) { TimePeriodEntity timePeriod = timePeriodService.getById(toUpdate.getTimePeriodId()); String dateStr = DateUtil.getShortDateByLongTime(timePeriod.getStartTime()); String timeStr = DateUtil.getStartToEndTime(timePeriod.getStartTime(), timePeriod.getEndTime()); String errorMessage = String.format("时段:%s %s,已预约%d人,当前已预约人数超出剩余容量%d人,无法关闭。", dateStr, timeStr, haveApplyCount, remainCount); log.error(errorMessage); throw new StatusException(errorMessage); } } } // 批量保存或更新 if (!toBeSaved.isEmpty()) { //防止重复保存 checkExistTimePeriodExamRoom(toBeSaved, Collections.emptyList()); saveBatch(toBeSaved); } if (!toBeUpdated.isEmpty()) { List ids = toBeUpdated.stream() .map(TimePeriodExamRoomEntity::getId) .collect(Collectors.toList()); checkExistTimePeriodExamRoom(toBeUpdated, ids); updateBatchById(toBeUpdated); } // 提交事务后刷新缓存 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { for (Long timePeriodId : updatedTimePeriodIds) { int oldCapacity = oldCapacityMap.getOrDefault(timePeriodId, 0);; int newCapacity = examSiteService.getExamSiteTimePeriodCapacity(examSiteCacheBean.getExamSiteId(), timePeriodId); applyTaskCacheService.refreshApplyAvailableCountCache( examRoom.getExamSiteId(), timePeriodId, oldCapacity, newCapacity ); } } }); } } catch (Exception e) { log.error("[考场排班设置]保存失败,原因:{}", e.getMessage(), e); throw new StatusException(e.getMessage()); } finally { try { if (examSiteLock.isLocked() && examSiteLock.isHeldByCurrentThread()) { examSiteLock.unlock(); log.info("[考场排班设置] 解锁成功,lockKey:{}", examSiteLockKey); } } catch (Exception e) { log.warn(e.getMessage()); } } } private void checkExistTimePeriodExamRoom(List toBeSaved, List ids) { // 判断考场+时段是否在库中已经存在 List examRoomIds = toBeSaved.stream() .map(TimePeriodExamRoomEntity::getExamRoomId) .distinct() .collect(Collectors.toList()); List timePeriodIds = toBeSaved.stream() .map(TimePeriodExamRoomEntity::getTimePeriodId) .distinct() .collect(Collectors.toList()); // 批量查询已存在的记录 List existingRecords = getBaseMapper().listByExamRoomIdsAndTimePeriodIds(examRoomIds, timePeriodIds, ids); // 构建已存在的记录集合,用于快速查找 Set existingKeySet = existingRecords.stream() .map(e -> e.getExamRoomId() + "-" + e.getTimePeriodId()) .collect(Collectors.toSet()); // 检查是否有重复的记录 for (TimePeriodExamRoomEntity item : toBeSaved) { String key = item.getExamRoomId() + "-" + item.getTimePeriodId(); if (existingKeySet.contains(key)) { log.error("[考场排班设置]保存失败,该时间段已存在: examRoomId={}, timePeriodId={}", item.getExamRoomId(), item.getTimePeriodId()); throw new StatusException("保存失败,时段重复"); } } } @Override public List listExamRoom(Long examSiteId, Long timePeriodId, Boolean enable) { return getBaseMapper().listExamRoom(examSiteId, timePeriodId, enable); } @Override public List listExamRoom(Long examRoomId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(TimePeriodExamRoomEntity::getExamRoomId, examRoomId); return list(wrapper); } @Override public List listByExamRoomIdsAndTimePeriodId(List examRoomIds, Long timePeriodId) { return getBaseMapper().listByExamRoomIdsAndTimePeriodId(examRoomIds, timePeriodId); } @Override public TimePeriodExamRoomEntity getTimePeriodExamRoom(Long roomId, Long timePeriodId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(TimePeriodExamRoomEntity::getExamRoomId, roomId); wrapper.eq(TimePeriodExamRoomEntity::getTimePeriodId, timePeriodId); return getOne(wrapper); } @Override public List listByExamRoomAndPeriodIds(Long examRoomId, Set periodIds) { return getBaseMapper().listByExamRoomAndPeriodIds(examRoomId, periodIds); } // 校验是否可编辑 private void canEdit(List timePeriodExamRoomList, CurrentApplyTaskVO curApplyTask) { if (CollectionUtils.isNotEmpty(timePeriodExamRoomList)) { Long longToday = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(new Date()) + " 00:00:00"); Date today = new Date(longToday); Date otherDay = DateUtil.addValues(today, Calendar.DAY_OF_MONTH, curApplyTask.getAllowApplyCancelDays()); long longOtherDay = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(otherDay) + " 23:59:59"); // 当前时间 long now = System.currentTimeMillis(); List timePeriodEntities = timePeriodService.listByIds(timePeriodExamRoomList); Map timePeriodEntityMap = timePeriodEntities.stream() .collect(Collectors.toMap(TimePeriodEntity::getId, tpe -> tpe)); for (Long timePeriodId : timePeriodExamRoomList) { TimePeriodEntity timePeriod = timePeriodEntityMap.get(timePeriodId); if (!isTimeEditable(now, timePeriod.getEndTime(), longOtherDay, timePeriod.getStartTime())) { throw new StatusException("保存失败," + DateUtil.getShortDateByLongTime(timePeriod.getStartTime()) + "可编辑时间范围已过"); } } } } private TimePeriodExamRoomEntity createTimePeriodExamRoomEntity(TimePeriodExamSiteReq item, Long examRoomId, Long userId) { TimePeriodExamRoomEntity entity = new TimePeriodExamRoomEntity(); BeanUtils.copyProperties(item, entity); entity.setExamRoomId(examRoomId); entity.setOperateId(userId); return entity; } }