StudentAutoAssignServiceImpl.java 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. package com.qmth.exam.reserve.service.impl;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  4. import com.qmth.boot.core.concurrent.service.ConcurrentService;
  5. import com.qmth.boot.core.exception.StatusException;
  6. import com.qmth.exam.reserve.bean.apply.ApplyRecordCacheBean;
  7. import com.qmth.exam.reserve.bean.applytask.CurrentApplyTaskVO;
  8. import com.qmth.exam.reserve.bean.category.CategoryCacheBean;
  9. import com.qmth.exam.reserve.bean.org.OrgInfo;
  10. import com.qmth.exam.reserve.bean.timeperiod.TimePeriodExamSiteBean;
  11. import com.qmth.exam.reserve.cache.CacheConstants;
  12. import com.qmth.exam.reserve.cache.impl.ApplyTaskCacheService;
  13. import com.qmth.exam.reserve.cache.impl.CategoryCacheService;
  14. import com.qmth.exam.reserve.cache.impl.OrgCacheService;
  15. import com.qmth.exam.reserve.dao.StudentApplyDao;
  16. import com.qmth.exam.reserve.entity.*;
  17. import com.qmth.exam.reserve.service.*;
  18. import com.qmth.exam.reserve.util.DateUtil;
  19. import com.qmth.exam.reserve.util.UnionUtil;
  20. import org.apache.commons.collections4.CollectionUtils;
  21. import org.apache.commons.lang3.time.DateFormatUtils;
  22. import org.redisson.api.RLock;
  23. import org.slf4j.Logger;
  24. import org.slf4j.LoggerFactory;
  25. import org.springframework.beans.factory.annotation.Autowired;
  26. import org.springframework.stereotype.Service;
  27. import java.text.MessageFormat;
  28. import java.util.*;
  29. import java.util.concurrent.atomic.AtomicInteger;
  30. import java.util.stream.Collectors;
  31. @Service
  32. public class StudentAutoAssignServiceImpl extends ServiceImpl<StudentApplyDao, StudentApplyEntity> implements StudentAutoAssignService {
  33. private final Logger log = LoggerFactory.getLogger(StudentAutoAssignServiceImpl.class);
  34. @Autowired
  35. private ConcurrentService concurrentService;
  36. @Autowired
  37. private StudentService studentService;
  38. @Autowired
  39. private ApplyTaskCacheService cacheService;
  40. @Autowired
  41. private CategoryService categoryService;
  42. @Autowired
  43. private ExamSiteService examSiteService;
  44. @Autowired
  45. private TimePeriodService timePeriodService;
  46. @Autowired
  47. private CategoryCacheService categoryCacheService;
  48. @Autowired
  49. private StudentApplyService studentApplyService;
  50. @Autowired
  51. private OrgCacheService orgCacheService;
  52. @Override
  53. public String autoAssign(Long taskId, Long operateId) {
  54. StringJoiner stringJoiner = new StringJoiner("\n");
  55. log.warn("[autoAssign] 开始自动预约考试");
  56. RLock lock = (RLock) concurrentService.getLock(CacheConstants.LOCK_AUTO_APPLY);
  57. try {
  58. if (!lock.tryLock()) {
  59. log.warn("[autoAssign] 获取锁失败,不允许同时执行自动分配!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);
  60. throw new StatusException("其他老师正在执行自动分配,请不要重复执行!");
  61. }
  62. log.warn("[autoAssign] 获取锁成功!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);
  63. // 未完成预约的考生
  64. List<StudentEntity> studentList = studentService.listNoFinishStudent(taskId, Boolean.FALSE);
  65. //过滤掉已完成预约的考生
  66. studentList = studentList.stream()
  67. .filter(item -> item.getApplyNumber() - cacheService.getStudentApplyFinishCount(item.getId()) > 0)
  68. .collect(Collectors.toList());
  69. //按照教学点分组
  70. Map<Long, List<StudentEntity>> noFinishApplyMap = studentList.stream().collect(Collectors.groupingBy(StudentEntity::getCategoryId));
  71. // 所有预约时段
  72. List<TimePeriodExamSiteBean> timeList = timePeriodService.listTimePeriodByTask(taskId);
  73. stringJoiner.add(
  74. MessageFormat.format("{0}-未完成预约的考生数:{1} 个", DateFormatUtils.format(new Date(), DateUtil.LongDateString), studentList.size()));
  75. // 考位是否充足
  76. checkTeachingCapacity(noFinishApplyMap, timeList, taskId);
  77. int successNum = 0;
  78. // 按照教学点安排考位。规则:不能和已预约的时间上有冲突
  79. for (Long teachingId : noFinishApplyMap.keySet()) {
  80. List<ExamSiteEntity> siteList = listExamSite(teachingId);
  81. if (siteList.isEmpty()) {
  82. log.warn("[autoAssign] 教学点{}下没有考点数据,不参与自动分配", teachingId);
  83. continue;
  84. }
  85. List<StudentEntity> teachingStudentList = noFinishApplyMap.get(teachingId);
  86. for (ExamSiteEntity site : siteList) {
  87. //考点对应的可用时段
  88. List<TimePeriodExamSiteBean> timePeriodExamSiteList = listAvailableTimePeriod(taskId, site, timeList);
  89. for (TimePeriodExamSiteBean time : timePeriodExamSiteList) {
  90. // 剩余的考位
  91. int remainNum = cacheService.getApplyAvailableCount(site.getId(), time.getTimePeriodId());
  92. if (remainNum > 0) {
  93. int applyNum = assignStudentApply(operateId, site.getId(), time.getTimePeriodId(), teachingStudentList, remainNum);
  94. successNum += applyNum;
  95. }
  96. }
  97. }
  98. // 判断是否还有剩余考生未完成预约,提醒考位不足
  99. if (!teachingStudentList.isEmpty()) {
  100. CategoryCacheBean categoryBean = categoryCacheService.getCategoryById(teachingId);
  101. throw new StatusException("【" + categoryBean.getName() + "】教学点考位不足,还需要【" + teachingStudentList.size() + "】个考位");
  102. }
  103. }
  104. stringJoiner.add(MessageFormat.format("{0}-自动预约成功的次数为:{1} 次", DateFormatUtils.format(new Date(), DateUtil.LongDateString), successNum));
  105. } catch (Exception e) {
  106. log.error(e.getMessage());
  107. throw new StatusException(e.getMessage());
  108. } finally {
  109. try {
  110. // 解锁前检查当前线程是否持有该锁
  111. if (lock.isLocked() && lock.isHeldByCurrentThread()) {
  112. lock.unlock();
  113. log.info("解锁成功!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);
  114. }
  115. } catch (Exception e) {
  116. log.warn(e.getMessage());
  117. }
  118. }
  119. return stringJoiner.toString();
  120. }
  121. private void checkTeachingCapacity(Map<Long, List<StudentEntity>> noFinishApplyMap, List<TimePeriodExamSiteBean> timeList, Long taskId) {
  122. for (Long teachingId : noFinishApplyMap.keySet()) {
  123. List<ExamSiteEntity> siteList = listExamSite(teachingId);
  124. if (siteList.isEmpty()) {
  125. continue;
  126. }
  127. List<StudentEntity> teachingStudentList = noFinishApplyMap.get(teachingId);
  128. // 未预约的考位
  129. AtomicInteger toBeApplySum = new AtomicInteger();
  130. teachingStudentList.forEach(item -> {
  131. toBeApplySum.addAndGet(item.getApplyNumber() - cacheService.getStudentApplyFinishCount(item.getId()));
  132. });
  133. AtomicInteger remainSum = new AtomicInteger();
  134. for (ExamSiteEntity site : siteList) {
  135. //考点对应的可用时段
  136. List<TimePeriodExamSiteBean> timePeriodExamSiteList = listAvailableTimePeriod(taskId, site, timeList);
  137. //剩余可用考位数
  138. timePeriodExamSiteList.forEach(item -> {
  139. remainSum.addAndGet(cacheService.getApplyAvailableCount(site.getId(), item.getTimePeriodId()));
  140. });
  141. }
  142. int difference = remainSum.get() - toBeApplySum.get();
  143. if (difference < 0) {
  144. CategoryCacheBean teachingBean = categoryCacheService.getCategoryById(teachingId);
  145. throw new StatusException("【" + teachingBean.getName() + "】教学点考位不足,还需要【" + (-difference) + "】个考位");
  146. }
  147. }
  148. }
  149. private List<TimePeriodExamSiteBean> listAvailableTimePeriod(Long taskId, ExamSiteEntity site, List<TimePeriodExamSiteBean> timeList) {
  150. List<TimePeriodExamSiteBean> timePeriodExamSiteList = timePeriodService.listTimePeriodByExamSiteId(taskId, site.getId());
  151. // 教学点未设置,则为所有的时段
  152. if (CollectionUtils.isEmpty(timePeriodExamSiteList)) {
  153. timePeriodExamSiteList = timeList;
  154. } else {
  155. // 解决教学点管理员在设置了预约日期后 学校管理员有新增预约时间段的场景
  156. timePeriodExamSiteList = UnionUtil.unionByAttribute(timePeriodExamSiteList, timeList, TimePeriodExamSiteBean::getTimePeriodId);
  157. }
  158. // 只取可以预约的时段
  159. timePeriodExamSiteList = listNoCancelExamTimePeriod(timePeriodExamSiteList);
  160. timePeriodExamSiteList = timePeriodExamSiteList.stream()
  161. .filter(TimePeriodExamSiteBean::getEnable)
  162. .collect(Collectors.toList());
  163. return timePeriodExamSiteList;
  164. }
  165. private List<TimePeriodExamSiteBean> listNoCancelExamTimePeriod(List<TimePeriodExamSiteBean> timePeriodList) {
  166. OrgInfo org = orgCacheService.currentOrg();
  167. CurrentApplyTaskVO curApplyTask = cacheService.currentApplyTask(org.getOrgId());
  168. Long longToday = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(new Date()) + " 00:00:00");
  169. Date today = new Date(longToday);
  170. Date otherDay = DateUtil.addValues(today, Calendar.DAY_OF_MONTH, curApplyTask.getAllowApplyCancelDays());
  171. Long longOtherDay = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(otherDay) + " 23:59:59");
  172. return timePeriodList.stream().filter(time -> time.getStartTime() > longOtherDay).collect(Collectors.toList());
  173. }
  174. private List<ExamSiteEntity> listExamSite(Long categoryId) {
  175. LambdaQueryWrapper<ExamSiteEntity> wrapper = new LambdaQueryWrapper<>();
  176. wrapper.eq(ExamSiteEntity::getCategoryId, categoryId);
  177. wrapper.eq(ExamSiteEntity::getEnable, Boolean.TRUE);
  178. return examSiteService.list(wrapper);
  179. }
  180. private int assignStudentApply(Long operateId, Long siteId, Long timeId, List<StudentEntity> teachingStudentList, int remainNum) {
  181. int num = 0;
  182. String studentApplyLockKey;
  183. RLock studentApplyLock;
  184. for (Iterator<StudentEntity> iterator = teachingStudentList.iterator(); iterator.hasNext(); ) {
  185. StudentEntity student = iterator.next();
  186. if (num >= remainNum) {
  187. break;
  188. }
  189. // 考生锁
  190. studentApplyLockKey = String.format(CacheConstants.LOCK_STUDENT_APPLY, student.getId());
  191. studentApplyLock = (RLock) concurrentService.getLock(studentApplyLockKey);
  192. try {
  193. if (!studentApplyLock.tryLock()) {
  194. log.warn("[autoAssign] 获取锁失败,考生在同时操作预约!lockKey:{}", studentApplyLockKey);
  195. iterator.remove();
  196. } else {
  197. log.warn("[autoAssign] 获取锁成功!lockKey:{}", studentApplyLockKey);
  198. // 考生已完成预约的数量
  199. int haveApplyCount = cacheService.getStudentApplyFinishCount(student.getId());
  200. // 还需要预约的数量
  201. int toApplyNum = student.getApplyNumber() - haveApplyCount;
  202. if (toApplyNum <= 0) {
  203. log.warn("[autoAssign] 数据问题, 考生需要预约最大次数:{},已完成预约的数量:{} ", student.getApplyNumber(), haveApplyCount);
  204. }
  205. if (toApplyNum > 0 && !haveApplySameTimePeriod(siteId, timeId, student.getId())) {
  206. //写入数据库
  207. StudentApplyEntity studentApply = new StudentApplyEntity();
  208. studentApply.setStudentId(student.getId());
  209. studentApply.setExamSiteId(siteId);
  210. studentApply.setCancel(Boolean.FALSE);
  211. studentApply.setTimePeriodId(timeId);
  212. studentApply.setOperateId(operateId);
  213. // 保存预约
  214. studentApplyService.saveOrUpdateStudentApply(studentApply);
  215. ApplyRecordCacheBean bean = new ApplyRecordCacheBean();
  216. bean.setStudentId(student.getId());
  217. bean.setExamSiteId(siteId);
  218. bean.setTimePeriodId(timeId);
  219. bean.setCancel(Boolean.FALSE);
  220. bean.setOperateId(operateId);
  221. bean.setOperateTime(System.currentTimeMillis());
  222. bean.setBizId(cacheService.increaseBizId());
  223. // 某考点某时段的“剩余可约数量”(抢占1个数量)
  224. boolean takeSuccess = cacheService.decreaseApplyAvailableCount(bean.getExamSiteId(), bean.getTimePeriodId());
  225. if (!takeSuccess) {
  226. log.warn("[autoAssign] 预约失败,当前预约时段已约满!examSiteId:{} timePeriodId:{} studentId:{}",
  227. bean.getExamSiteId(), bean.getTimePeriodId(), bean.getStudentId());
  228. continue;
  229. }
  230. /*
  231. // 推送至预约队列
  232. boolean pushSuccess = cacheService.pushStudentApplyRecordQueue(bean);
  233. if (!pushSuccess) {
  234. // 推送失败时,归还1个被占数量
  235. cacheService.increaseApplyAvailableCount(bean.getExamSiteId(), bean.getTimePeriodId());
  236. log.warn("预约消息推送失败!examSiteId:{} timePeriodId:{} studentId:{}", bean.getExamSiteId(),
  237. bean.getTimePeriodId(), bean.getStudentId());
  238. continue;
  239. }*/
  240. // 保存至预约缓存
  241. cacheService.saveStudentApplyRecord(bean);
  242. num++;
  243. //完成所有的预约,移除考生
  244. if (student.getApplyNumber() - (haveApplyCount + 1) == 0) {
  245. iterator.remove();
  246. }
  247. }
  248. }
  249. } catch (Exception e) {
  250. throw new StatusException("自动安排预约失败,失败原因:" + e.getMessage());
  251. } finally {
  252. try {
  253. // 解锁前检查当前线程是否持有该锁
  254. if (studentApplyLock.isLocked() && studentApplyLock.isHeldByCurrentThread()) {
  255. studentApplyLock.unlock();
  256. log.info("解锁成功!lockKey:{}", studentApplyLockKey);
  257. }
  258. } catch (Exception e) {
  259. log.warn(e.getMessage());
  260. }
  261. }
  262. }
  263. return num;
  264. }
  265. private boolean haveApplySameTimePeriod(Long siteId, Long timeId, Long studentId) {
  266. LambdaQueryWrapper<StudentApplyEntity> wrapper = new LambdaQueryWrapper<>();
  267. wrapper.eq(StudentApplyEntity::getExamSiteId, siteId);
  268. wrapper.eq(StudentApplyEntity::getTimePeriodId, timeId);
  269. wrapper.eq(StudentApplyEntity::getStudentId, studentId);
  270. wrapper.eq(StudentApplyEntity::getCancel, Boolean.FALSE);
  271. StudentApplyEntity studentApply = baseMapper.selectOne(wrapper);
  272. return studentApply != null;
  273. }
  274. }