123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494 |
- 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<TimePeriodExamRoomDao, TimePeriodExamRoomEntity> 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<TimePeriodExamSiteVo> 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<String> dateList = timePeriodService.listTimePeriodDate(curApplyTask.getTaskId());
- if (CollectionUtils.isEmpty(dateList)) {
- log.warn("[考场排班设置列表]当前任务:{}未设置预约日期", curApplyTask.getTaskId());
- return Collections.emptyList();
- }
- //教学点管理员设置的所有的考点时段
- List<TimePeriodExamSiteBean> timePeriodExamRoomList = timePeriodService.listTimePeriodByExamRoomId(curApplyTask.getTaskId(), examRoomId);
- //学校管理员设置的所有预约时段
- List<TimePeriodExamSiteBean> timePeriodList = timePeriodService.listTimePeriodByTask(curApplyTask.getTaskId());
- List<TimePeriodExamSiteBean> resultList;
- if (CollectionUtils.isEmpty(timePeriodExamRoomList)) {
- resultList = timePeriodList;
- } else {
- //取并集
- resultList = UnionUtil.unionByAttribute(timePeriodExamRoomList, timePeriodList, TimePeriodExamSiteBean::getTimePeriodId);
- }
- //按日期封装
- List<TimePeriodExamSiteVo> 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<TimePeriodExamSiteInfo> filterTimePeriod(String date, List<TimePeriodExamSiteBean> timePeriodList, Integer canCancelDay) {
- // 参数校验
- if (timePeriodList == null || date == null || canCancelDay == null || canCancelDay < 0) {
- return new ArrayList<>();
- }
- List<TimePeriodExamSiteInfo> resultList = new ArrayList<>();
- // 过滤符合条件的时间段
- List<TimePeriodExamSiteBean> 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<TimePeriodExamSiteReq> 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<TimePeriodExamSiteBean> existingTimePeriods = timePeriodService.listTimePeriodByExamRoomId(curApplyTask.getTaskId(), examRoomId);
- Map<Long, TimePeriodExamSiteBean> existingMap = existingTimePeriods.stream()
- .collect(Collectors.toMap(TimePeriodExamSiteBean::getId, t -> t));
- // 过滤出可编辑的数据
- List<TimePeriodExamSiteReq> editableList = timePeriodExamRoomList.stream()
- .filter(TimePeriodExamSiteReq::getEditable)
- .collect(Collectors.toList());
- if (editableList.isEmpty()) {
- log.warn("[考场排班设置]无可编辑数据");
- return;
- }
- // 构建实体列表
- List<TimePeriodExamRoomEntity> entityList = editableList.stream()
- .map(req -> createTimePeriodExamRoomEntity(req, examRoomId, userId))
- .collect(Collectors.toList());
- // 分离新增和更新
- List<TimePeriodExamRoomEntity> toBeSaved = new ArrayList<>();
- List<TimePeriodExamRoomEntity> toBeUpdated = new ArrayList<>();
- entityList.forEach(e -> {
- if (e.getId() == null) {
- toBeSaved.add(e);
- } else {
- toBeUpdated.add(e);
- }
- });
- // 检查是否可编辑
- List<Long> 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<Long> updatedTimePeriodIds = new HashSet<>();
- toBeSaved.forEach(e -> updatedTimePeriodIds.add(e.getTimePeriodId()));
- toBeUpdated.forEach(e -> updatedTimePeriodIds.add(e.getTimePeriodId()));
- // 保存前先记录旧容量
- Map<Long, Integer> oldCapacityMap = new HashMap<>();
- for (Long timePeriodId : updatedTimePeriodIds) {
- int capacity = examSiteService.getExamSiteTimePeriodCapacity(examSiteCacheBean.getExamSiteId(), timePeriodId);
- oldCapacityMap.put(timePeriodId, capacity);
- }
- List<TimePeriodExamRoomEntity> verifyList = new ArrayList<>();
- //只处理禁用的时段
- List<TimePeriodExamRoomEntity> 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<Long> 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<TimePeriodExamRoomEntity> toBeSaved, List<Long> ids) {
- // 判断考场+时段是否在库中已经存在
- List<Long> examRoomIds = toBeSaved.stream()
- .map(TimePeriodExamRoomEntity::getExamRoomId)
- .distinct()
- .collect(Collectors.toList());
- List<Long> timePeriodIds = toBeSaved.stream()
- .map(TimePeriodExamRoomEntity::getTimePeriodId)
- .distinct()
- .collect(Collectors.toList());
- // 批量查询已存在的记录
- List<TimePeriodExamRoomEntity> existingRecords = getBaseMapper().listByExamRoomIdsAndTimePeriodIds(examRoomIds, timePeriodIds, ids);
- // 构建已存在的记录集合,用于快速查找
- Set<String> 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<ExamRoomEntity> listExamRoom(Long examSiteId, Long timePeriodId, Boolean enable) {
- return getBaseMapper().listExamRoom(examSiteId, timePeriodId, enable);
- }
- @Override
- public List<TimePeriodExamRoomEntity> listExamRoom(Long examRoomId) {
- LambdaQueryWrapper<TimePeriodExamRoomEntity> wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(TimePeriodExamRoomEntity::getExamRoomId, examRoomId);
- return list(wrapper);
- }
- @Override
- public List<TimePeriodExamRoomEntity> listByExamRoomIdsAndTimePeriodId(List<Long> examRoomIds, Long timePeriodId) {
- return getBaseMapper().listByExamRoomIdsAndTimePeriodId(examRoomIds, timePeriodId);
- }
- @Override
- public TimePeriodExamRoomEntity getTimePeriodExamRoom(Long roomId, Long timePeriodId) {
- LambdaQueryWrapper<TimePeriodExamRoomEntity> wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(TimePeriodExamRoomEntity::getExamRoomId, roomId);
- wrapper.eq(TimePeriodExamRoomEntity::getTimePeriodId, timePeriodId);
- return getOne(wrapper);
- }
- @Override
- public List<TimePeriodExamRoomEntity> listByExamRoomAndPeriodIds(Long examRoomId, Set<Long> periodIds) {
- return getBaseMapper().listByExamRoomAndPeriodIds(examRoomId, periodIds);
- }
- // 校验是否可编辑
- private void canEdit(List<Long> 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<TimePeriodEntity> timePeriodEntities = timePeriodService.listByIds(timePeriodExamRoomList);
- Map<Long, TimePeriodEntity> 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;
- }
- }
|