123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- 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<StudentApplyDao, StudentApplyEntity> 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<StudentEntity> studentList = studentService.listNoFinishStudent(taskId, Boolean.FALSE);
- //过滤掉已完成预约的考生
- studentList = studentList.stream()
- .filter(item -> item.getApplyNumber() - cacheService.getStudentApplyFinishCount(item.getId()) > 0)
- .collect(Collectors.toList());
- //按照教学点分组
- Map<Long, List<StudentEntity>> noFinishApplyMap = studentList.stream().collect(Collectors.groupingBy(StudentEntity::getCategoryId));
- // 所有预约时段
- List<TimePeriodExamSiteBean> 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<ExamSiteEntity> siteList = listExamSite(teachingId);
- if (siteList.isEmpty()) {
- log.warn("[autoAssign] 教学点{}下没有考点数据,不参与自动分配", teachingId);
- continue;
- }
- List<StudentEntity> teachingStudentList = noFinishApplyMap.get(teachingId);
- for (ExamSiteEntity site : siteList) {
- //考点对应的可用时段
- List<TimePeriodExamSiteBean> 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<Long, List<StudentEntity>> noFinishApplyMap, List<TimePeriodExamSiteBean> timeList, Long taskId) {
- for (Long teachingId : noFinishApplyMap.keySet()) {
- List<ExamSiteEntity> siteList = listExamSite(teachingId);
- if (siteList.isEmpty()) {
- continue;
- }
- List<StudentEntity> 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<TimePeriodExamSiteBean> 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<TimePeriodExamSiteBean> listAvailableTimePeriod(Long taskId, ExamSiteEntity site, List<TimePeriodExamSiteBean> timeList) {
- List<TimePeriodExamSiteBean> 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<TimePeriodExamSiteBean> listNoCancelExamTimePeriod(List<TimePeriodExamSiteBean> 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<ExamSiteEntity> listExamSite(Long categoryId) {
- LambdaQueryWrapper<ExamSiteEntity> 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<StudentEntity> teachingStudentList, int remainNum) {
- int num = 0;
- String studentApplyLockKey;
- RLock studentApplyLock;
- for (Iterator<StudentEntity> 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<StudentApplyEntity> 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;
- }
- }
|