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.apply.ApplyRecordCacheBean; import com.qmth.exam.reserve.bean.applytask.CurrentApplyTaskVO; import com.qmth.exam.reserve.bean.category.CategoryCacheBean; import com.qmth.exam.reserve.bean.org.OrgInfo; import com.qmth.exam.reserve.bean.timeperiod.TimePeriodExamSiteBean; import com.qmth.exam.reserve.cache.CacheConstants; import com.qmth.exam.reserve.cache.impl.ApplyTaskCacheService; import com.qmth.exam.reserve.cache.impl.CategoryCacheService; import com.qmth.exam.reserve.cache.impl.OrgCacheService; import com.qmth.exam.reserve.dao.StudentApplyDao; import com.qmth.exam.reserve.entity.*; 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.time.DateFormatUtils; import org.redisson.api.RLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.text.MessageFormat; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @Service public class StudentAutoAssignServiceImpl extends ServiceImpl implements StudentAutoAssignService { private final Logger log = LoggerFactory.getLogger(StudentAutoAssignServiceImpl.class); @Autowired private ConcurrentService concurrentService; @Autowired private StudentService studentService; @Autowired private ApplyTaskCacheService cacheService; @Autowired private CategoryService categoryService; @Autowired private ExamSiteService examSiteService; @Autowired private TimePeriodService timePeriodService; @Autowired private CategoryCacheService categoryCacheService; @Autowired private StudentApplyService studentApplyService; @Autowired private OrgCacheService orgCacheService; @Override public String autoAssign(Long taskId, Long operateId) { StringJoiner stringJoiner = new StringJoiner("\n"); log.warn("[autoAssign] 开始自动预约考试"); RLock lock = (RLock) concurrentService.getLock(CacheConstants.LOCK_AUTO_APPLY); try { if (!lock.tryLock()) { log.warn("[autoAssign] 获取锁失败,不允许同时执行自动分配!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY); throw new StatusException("其他老师正在执行自动分配,请不要重复执行!"); } log.warn("[autoAssign] 获取锁成功!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY); // 未完成预约的考生 List studentList = studentService.listNoFinishStudent(taskId, Boolean.FALSE); //过滤掉已完成预约的考生 studentList = studentList.stream() .filter(item -> item.getApplyNumber() - cacheService.getStudentApplyFinishCount(item.getId()) > 0) .collect(Collectors.toList()); //按照教学点分组 Map> noFinishApplyMap = studentList.stream().collect(Collectors.groupingBy(StudentEntity::getCategoryId)); // 所有预约时段 List timeList = timePeriodService.listTimePeriodByTask(taskId); stringJoiner.add( MessageFormat.format("{0}-未完成预约的考生数:{1} 个", DateFormatUtils.format(new Date(), DateUtil.LongDateString), studentList.size())); // 考位是否充足 checkTeachingCapacity(noFinishApplyMap, timeList, taskId); int successNum = 0; // 按照教学点安排考位。规则:不能和已预约的时间上有冲突 for (Long teachingId : noFinishApplyMap.keySet()) { List siteList = listExamSite(teachingId); if (siteList.isEmpty()) { log.warn("[autoAssign] 教学点{}下没有考点数据,不参与自动分配", teachingId); continue; } List teachingStudentList = noFinishApplyMap.get(teachingId); for (ExamSiteEntity site : siteList) { //考点对应的可用时段 List timePeriodExamSiteList = listAvailableTimePeriod(taskId, site, timeList); for (TimePeriodExamSiteBean time : timePeriodExamSiteList) { // 剩余的考位 int remainNum = cacheService.getApplyAvailableCount(site.getId(), time.getTimePeriodId()); if (remainNum > 0) { int applyNum = assignStudentApply(operateId, site.getId(), time.getTimePeriodId(), teachingStudentList, remainNum); successNum += applyNum; } } } // 判断是否还有剩余考生未完成预约,提醒考位不足 if (!teachingStudentList.isEmpty()) { CategoryCacheBean categoryBean = categoryCacheService.getCategoryById(teachingId); throw new StatusException("【" + categoryBean.getName() + "】教学点考位不足,还需要【" + teachingStudentList.size() + "】个考位"); } } stringJoiner.add(MessageFormat.format("{0}-自动预约成功的次数为:{1} 次", DateFormatUtils.format(new Date(), DateUtil.LongDateString), successNum)); } catch (Exception e) { log.error(e.getMessage()); throw new StatusException(e.getMessage()); } finally { try { // 解锁前检查当前线程是否持有该锁 if (lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); log.info("解锁成功!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY); } } catch (Exception e) { log.warn(e.getMessage()); } } return stringJoiner.toString(); } private void checkTeachingCapacity(Map> noFinishApplyMap, List timeList, Long taskId) { for (Long teachingId : noFinishApplyMap.keySet()) { List siteList = listExamSite(teachingId); if (siteList.isEmpty()) { continue; } List teachingStudentList = noFinishApplyMap.get(teachingId); // 未预约的考位 AtomicInteger toBeApplySum = new AtomicInteger(); teachingStudentList.forEach(item -> { toBeApplySum.addAndGet(item.getApplyNumber() - cacheService.getStudentApplyFinishCount(item.getId())); }); AtomicInteger remainSum = new AtomicInteger(); for (ExamSiteEntity site : siteList) { //考点对应的可用时段 List timePeriodExamSiteList = listAvailableTimePeriod(taskId, site, timeList); //剩余可用考位数 timePeriodExamSiteList.forEach(item -> { remainSum.addAndGet(cacheService.getApplyAvailableCount(site.getId(), item.getTimePeriodId())); }); } int difference = remainSum.get() - toBeApplySum.get(); if (difference < 0) { CategoryCacheBean teachingBean = categoryCacheService.getCategoryById(teachingId); throw new StatusException("【" + teachingBean.getName() + "】教学点考位不足,还需要【" + (-difference) + "】个考位"); } } } private List listAvailableTimePeriod(Long taskId, ExamSiteEntity site, List timeList) { List timePeriodExamSiteList = timePeriodService.listTimePeriodByExamSiteId(taskId, site.getId()); // 教学点未设置,则为所有的时段 if (CollectionUtils.isEmpty(timePeriodExamSiteList)) { timePeriodExamSiteList = timeList; } else { // 解决教学点管理员在设置了预约日期后 学校管理员有新增预约时间段的场景 timePeriodExamSiteList = UnionUtil.unionByAttribute(timePeriodExamSiteList, timeList, TimePeriodExamSiteBean::getTimePeriodId); } // 只取可以预约的时段 timePeriodExamSiteList = listNoCancelExamTimePeriod(timePeriodExamSiteList); timePeriodExamSiteList = timePeriodExamSiteList.stream() .filter(TimePeriodExamSiteBean::getEnable) .collect(Collectors.toList()); return timePeriodExamSiteList; } private List listNoCancelExamTimePeriod(List timePeriodList) { OrgInfo org = orgCacheService.currentOrg(); CurrentApplyTaskVO curApplyTask = cacheService.currentApplyTask(org.getOrgId()); 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"); return timePeriodList.stream().filter(time -> time.getStartTime() > longOtherDay).collect(Collectors.toList()); } private List listExamSite(Long categoryId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ExamSiteEntity::getCategoryId, categoryId); wrapper.eq(ExamSiteEntity::getEnable, Boolean.TRUE); return examSiteService.list(wrapper); } private int assignStudentApply(Long operateId, Long siteId, Long timeId, List teachingStudentList, int remainNum) { int num = 0; String studentApplyLockKey; RLock studentApplyLock; for (Iterator iterator = teachingStudentList.iterator(); iterator.hasNext(); ) { StudentEntity student = iterator.next(); if (num >= remainNum) { break; } // 考生锁 studentApplyLockKey = String.format(CacheConstants.LOCK_STUDENT_APPLY, student.getId()); studentApplyLock = (RLock) concurrentService.getLock(studentApplyLockKey); try { if (!studentApplyLock.tryLock()) { log.warn("[autoAssign] 获取锁失败,考生在同时操作预约!lockKey:{}", studentApplyLockKey); iterator.remove(); } else { log.warn("[autoAssign] 获取锁成功!lockKey:{}", studentApplyLockKey); // 考生已完成预约的数量 int haveApplyCount = cacheService.getStudentApplyFinishCount(student.getId()); // 还需要预约的数量 int toApplyNum = student.getApplyNumber() - haveApplyCount; if (toApplyNum <= 0) { log.warn("[autoAssign] 数据问题, 考生需要预约最大次数:{},已完成预约的数量:{} ", student.getApplyNumber(), haveApplyCount); } if (toApplyNum > 0 && !haveApplySameTimePeriod(siteId, timeId, student.getId())) { //写入数据库 StudentApplyEntity studentApply = new StudentApplyEntity(); studentApply.setStudentId(student.getId()); studentApply.setExamSiteId(siteId); studentApply.setCancel(Boolean.FALSE); studentApply.setTimePeriodId(timeId); studentApply.setOperateId(operateId); // 保存预约 studentApplyService.saveOrUpdateStudentApply(studentApply); ApplyRecordCacheBean bean = new ApplyRecordCacheBean(); bean.setStudentId(student.getId()); bean.setExamSiteId(siteId); bean.setTimePeriodId(timeId); bean.setCancel(Boolean.FALSE); bean.setOperateId(operateId); bean.setOperateTime(System.currentTimeMillis()); bean.setBizId(cacheService.increaseBizId()); // 某考点某时段的“剩余可约数量”(抢占1个数量) boolean takeSuccess = cacheService.decreaseApplyAvailableCount(bean.getExamSiteId(), bean.getTimePeriodId()); if (!takeSuccess) { log.warn("[autoAssign] 预约失败,当前预约时段已约满!examSiteId:{} timePeriodId:{} studentId:{}", bean.getExamSiteId(), bean.getTimePeriodId(), bean.getStudentId()); continue; } /* // 推送至预约队列 boolean pushSuccess = cacheService.pushStudentApplyRecordQueue(bean); if (!pushSuccess) { // 推送失败时,归还1个被占数量 cacheService.increaseApplyAvailableCount(bean.getExamSiteId(), bean.getTimePeriodId()); log.warn("预约消息推送失败!examSiteId:{} timePeriodId:{} studentId:{}", bean.getExamSiteId(), bean.getTimePeriodId(), bean.getStudentId()); continue; }*/ // 保存至预约缓存 cacheService.saveStudentApplyRecord(bean); num++; //完成所有的预约,移除考生 if (student.getApplyNumber() - (haveApplyCount + 1) == 0) { iterator.remove(); } } } } catch (Exception e) { throw new StatusException("自动安排预约失败,失败原因:" + e.getMessage()); } finally { try { // 解锁前检查当前线程是否持有该锁 if (studentApplyLock.isLocked() && studentApplyLock.isHeldByCurrentThread()) { studentApplyLock.unlock(); log.info("解锁成功!lockKey:{}", studentApplyLockKey); } } catch (Exception e) { log.warn(e.getMessage()); } } } return num; } private boolean haveApplySameTimePeriod(Long siteId, Long timeId, Long studentId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StudentApplyEntity::getExamSiteId, siteId); wrapper.eq(StudentApplyEntity::getTimePeriodId, timeId); wrapper.eq(StudentApplyEntity::getStudentId, studentId); wrapper.eq(StudentApplyEntity::getCancel, Boolean.FALSE); StudentApplyEntity studentApply = baseMapper.selectOne(wrapper); return studentApply != null; } }