StudentApplyServiceImpl.java 60 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249
  1. package com.qmth.exam.reserve.service.impl;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.baomidou.mybatisplus.core.metadata.IPage;
  4. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  5. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  6. import com.qmth.boot.core.collection.PageResult;
  7. import com.qmth.boot.core.concurrent.service.ConcurrentService;
  8. import com.qmth.boot.core.exception.StatusException;
  9. import com.qmth.boot.tools.excel.ExcelReader;
  10. import com.qmth.boot.tools.excel.enums.ExcelType;
  11. import com.qmth.boot.tools.excel.model.DataMap;
  12. import com.qmth.boot.tools.io.ZipWriter;
  13. import com.qmth.exam.reserve.bean.Constants;
  14. import com.qmth.exam.reserve.bean.apply.ApplyRecordCacheBean;
  15. import com.qmth.exam.reserve.bean.applytask.CurrentApplyTaskVO;
  16. import com.qmth.exam.reserve.bean.category.CategoryCacheBean;
  17. import com.qmth.exam.reserve.bean.examsite.ExamSiteCacheBean;
  18. import com.qmth.exam.reserve.bean.login.LoginUser;
  19. import com.qmth.exam.reserve.bean.org.OrgInfo;
  20. import com.qmth.exam.reserve.bean.stdapply.*;
  21. import com.qmth.exam.reserve.bean.timeperiod.TimePeriodExamSiteBean;
  22. import com.qmth.exam.reserve.cache.CacheConstants;
  23. import com.qmth.exam.reserve.cache.impl.ApplyTaskCacheService;
  24. import com.qmth.exam.reserve.cache.impl.CategoryCacheService;
  25. import com.qmth.exam.reserve.cache.impl.ExamSiteCacheService;
  26. import com.qmth.exam.reserve.cache.impl.OrgCacheService;
  27. import com.qmth.exam.reserve.dao.StudentApplyDao;
  28. import com.qmth.exam.reserve.entity.*;
  29. import com.qmth.exam.reserve.entity.base.BaseEntity;
  30. import com.qmth.exam.reserve.enums.AsyncTaskType;
  31. import com.qmth.exam.reserve.enums.CategoryLevel;
  32. import com.qmth.exam.reserve.enums.EventType;
  33. import com.qmth.exam.reserve.enums.Role;
  34. import com.qmth.exam.reserve.service.*;
  35. import com.qmth.exam.reserve.template.execute.AutoAssignStudentService;
  36. import com.qmth.exam.reserve.template.execute.StudentApplyDetailExportService;
  37. import com.qmth.exam.reserve.template.execute.StudentApplyNoFinishExportService;
  38. import com.qmth.exam.reserve.util.DateUtil;
  39. import com.qmth.exam.reserve.util.JsonHelper;
  40. import com.qmth.exam.reserve.util.PageUtil;
  41. import com.qmth.exam.reserve.util.UnionUtil;
  42. import org.apache.commons.collections4.CollectionUtils;
  43. import org.apache.commons.lang3.StringUtils;
  44. import org.apache.commons.lang3.time.DateUtils;
  45. import org.redisson.api.RLock;
  46. import org.slf4j.Logger;
  47. import org.slf4j.LoggerFactory;
  48. import org.springframework.beans.factory.annotation.Autowired;
  49. import org.springframework.stereotype.Service;
  50. import org.springframework.transaction.annotation.Transactional;
  51. import java.io.File;
  52. import java.io.IOException;
  53. import java.io.InputStream;
  54. import java.text.MessageFormat;
  55. import java.time.LocalDate;
  56. import java.time.ZoneId;
  57. import java.util.*;
  58. import java.util.stream.Collectors;
  59. import static org.apache.commons.lang3.time.DateUtils.isSameDay;
  60. /**
  61. * @Description 管理端-预约
  62. */
  63. @Service
  64. public class StudentApplyServiceImpl extends ServiceImpl<StudentApplyDao, StudentApplyEntity> implements StudentApplyService {
  65. private static final Logger log = LoggerFactory.getLogger(StudentApplyServiceImpl.class);
  66. private static final String[] EXCEL_HEADER = new String[]{"学号", "姓名", "证件号", "所属教学点", "预约考点1", "预约时段1", "预约考点2", "预约时段2", "预约考点3",
  67. "预约时段3", "预约考点4", "预约时段4"};
  68. private static final int maxApplyNum = 3;
  69. @Autowired
  70. private ApplyTaskService applyTaskService;
  71. @Autowired
  72. private ExamSiteService examSiteService;
  73. @Autowired
  74. private TimePeriodService timePeriodService;
  75. @Autowired
  76. private ConcurrentService concurrentService;
  77. @Autowired
  78. private ApplyTaskCacheService cacheService;
  79. @Autowired
  80. private OperateLogService operateLogService;
  81. @Autowired
  82. private CategoryService categoryService;
  83. @Autowired
  84. private StudentService studentService;
  85. @Autowired
  86. private ExamSiteCacheService examSiteCacheService;
  87. @Autowired
  88. private CategoryCacheService categoryCacheService;
  89. @Autowired
  90. private MaterialGenerateService materialGenerateService;
  91. @Autowired
  92. private ExamRoomService examRoomService;
  93. @Autowired
  94. private AsyncTaskService asyncTaskService;
  95. @Autowired
  96. private StudentApplyNoFinishExportService studentApplyNoFinishExportService;
  97. @Autowired
  98. private StudentApplyDetailExportService studentApplyDetailExportService;
  99. @Autowired
  100. private AutoAssignStudentService autoAssignStudentService;
  101. @Autowired
  102. private OrgCacheService orgCacheService;
  103. @Override
  104. public PageResult<StudentApplyVO> page(StudentApplyReq req) {
  105. ApplyTaskEntity task = getApplyTask();
  106. req.setTaskId(task.getId());
  107. if (req.getTeachingId() != null) {
  108. List<CategoryVO> examSiteList = examSiteService.listExamSite(req.getTeachingId(), Boolean.FALSE);
  109. if (examSiteList == null || examSiteList.isEmpty()) {
  110. return new PageResult<>();
  111. }
  112. List<Long> examSiteIds = examSiteList.stream().map(CategoryVO::getId).collect(Collectors.toList());
  113. req.setExamSiteIds(examSiteIds.isEmpty() ? null : examSiteIds);
  114. }
  115. IPage<StudentApplyVO> iPage = baseMapper.page(new Page<>(req.getPageNumber(), req.getPageSize()), req);
  116. List<StudentApplyVO> records = iPage.getRecords();
  117. for (StudentApplyVO record : records) {
  118. AgentAndRoomVO agentRoom = baseMapper.getAgentAndRoom(record.getId());
  119. record.setAgentName(agentRoom.getAgentName());
  120. record.setTeachingName(agentRoom.getTeachingName());
  121. record.setApplyTeachingName(agentRoom.getApplyTeachingName());
  122. record.setRoomName(agentRoom.getRoomName());
  123. record.setUserName(agentRoom.getUserName());
  124. // 是否可取消
  125. TimePeriodEntity timePeriod = timePeriodService.getById(record.getTimePeriodId());
  126. Date applyDate = DateUtils.truncate(new Date(timePeriod.getStartTime()), Calendar.DATE);
  127. Date canCancelDay = DateUtil.addValues(applyDate, Calendar.DAY_OF_MONTH, -task.getAllowApplyCancelDays());
  128. if (new Date().after(canCancelDay)) {
  129. record.setCanCancelFlag(Boolean.FALSE);
  130. } else {
  131. record.setCanCancelFlag(Boolean.TRUE);
  132. }
  133. }
  134. return PageUtil.of(iPage);
  135. }
  136. private ApplyTaskEntity getApplyTask() {
  137. LambdaQueryWrapper<ApplyTaskEntity> wrapper = new LambdaQueryWrapper<>();
  138. wrapper.eq(ApplyTaskEntity::getEnable, Boolean.TRUE);
  139. ApplyTaskEntity task = applyTaskService.getOne(wrapper);
  140. if (task == null) {
  141. throw new StatusException("未开启预约任务");
  142. }
  143. return task;
  144. }
  145. @Transactional
  146. @Override
  147. public void cancel(LoginUser user, Long id) {
  148. StudentApplyEntity studentApplyEntity = getById(id);
  149. if (studentApplyEntity == null) {
  150. log.warn("[cancel] 考生预约信息不存在, id:{} userAccount:{}", id, user.getAccount());
  151. throw new StatusException("没有预约信息");
  152. }
  153. if (studentApplyEntity.getTimePeriodId() == null) {
  154. log.warn("[cancel] 考生预约的时段为空, id:{} userAccount:{}", id, user.getAccount());
  155. throw new StatusException("没有预约信息");
  156. }
  157. TimePeriodEntity timePeriod = timePeriodService.getById(studentApplyEntity.getTimePeriodId());
  158. if (timePeriod == null) {
  159. log.warn("[cancel] 考生预约的时段不存在, timePeriodId:{} userAccount:{}", studentApplyEntity.getTimePeriodId(), user.getAccount());
  160. throw new StatusException("考试时段不存在");
  161. }
  162. ApplyTaskEntity task = getApplyTask();
  163. Date applyDate = DateUtils.truncate(new Date(timePeriod.getStartTime()), Calendar.DATE);
  164. Date canCancelDay = DateUtil.addValues(applyDate, Calendar.DAY_OF_MONTH, -task.getAllowApplyCancelDays());
  165. if (new Date().after(canCancelDay)) {
  166. throw new StatusException("[cancel] 可取消时间已过,无法在取消");
  167. }
  168. StudentEntity student = studentService.getById(studentApplyEntity.getStudentId());
  169. if(student == null) {
  170. log.warn("[cancel] 考生不存在, studentId:{} ", studentApplyEntity.getStudentId());
  171. throw new StatusException("考生不存在");
  172. }
  173. if (concurrentService.isLocked(CacheConstants.LOCK_AUTO_APPLY)) {
  174. // 系统自动预约“任务执行期间”不允许取消预约
  175. log.warn("系统自动预约中,不允许考生操作预约!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);
  176. throw new StatusException(Constants.SYSTEM_BUSY);
  177. }
  178. // 教学点管理员,只能取消本教学点的考生
  179. if (user.getRole().equals(Role.TEACHING) && !user.getCategoryId().equals(student.getCategoryId())) {
  180. throw new StatusException("[cancel] 只能取消本教学点的考生");
  181. }
  182. // 考生是否已经取消判断
  183. ApplyRecordCacheBean studentApplyRecord = cacheService.getStudentApplyRecord(studentApplyEntity.getStudentId(), studentApplyEntity.getExamSiteId(),
  184. studentApplyEntity.getTimePeriodId());
  185. if (studentApplyRecord != null && studentApplyRecord.getCancel()) {
  186. throw new StatusException("[cancel] 考生已经取消,请稍后查看");
  187. }
  188. String studentApplyLockKey = String.format(CacheConstants.LOCK_STUDENT_APPLY, studentApplyEntity.getStudentId());
  189. RLock studentApplyLock = (RLock) concurrentService.getLock(studentApplyLockKey);
  190. try {
  191. if (!studentApplyLock.tryLock()) {
  192. log.warn("[cancel] 获取锁失败,同一个考生不允许同时操作取消预约, lockKey:{}", studentApplyLockKey);
  193. throw new StatusException(Constants.SYSTEM_BUSY);
  194. } else {
  195. //更新预约取消标志
  196. studentApplyEntity.setCancel(Boolean.TRUE);
  197. updateById(studentApplyEntity);
  198. ApplyRecordCacheBean bean = new ApplyRecordCacheBean();
  199. bean.setStudentId(studentApplyEntity.getStudentId());
  200. bean.setExamSiteId(studentApplyEntity.getExamSiteId());
  201. bean.setTimePeriodId(studentApplyEntity.getTimePeriodId());
  202. bean.setCancel(Boolean.TRUE);
  203. bean.setOperateId(user.getId());
  204. bean.setOperateTime(System.currentTimeMillis());
  205. bean.setBizId(cacheService.increaseBizId());
  206. // 推送至预约队列
  207. /*boolean pushSuccess = cacheService.pushStudentApplyRecordQueue(bean);
  208. if (!pushSuccess) {
  209. log.error("[cancel] 预约消息推送失败,id:{}", id);
  210. throw new RuntimeException("取消失败,请稍后再试!");
  211. }*/
  212. // 保存至预约缓存
  213. cacheService.saveStudentApplyRecord(bean);
  214. // 某考点某时段的“剩余可约数量”(归还1个被占数量)
  215. cacheService.increaseApplyAvailableCount(studentApplyEntity.getExamSiteId(), studentApplyEntity.getTimePeriodId());
  216. }
  217. } catch (Exception e) {
  218. log.error("[cancel] 取消预约失败, msg:{}", e.getMessage());
  219. throw new StatusException("取消预约失败,请稍后再试");
  220. } finally {
  221. try {
  222. if (studentApplyLock.isLocked() && studentApplyLock.isHeldByCurrentThread()) {
  223. studentApplyLock.unlock();
  224. log.info("[cancel] 解锁成功,lockKey:{}", studentApplyLock);
  225. }
  226. } catch (Exception e) {
  227. log.warn(e.getMessage());
  228. }
  229. }
  230. //写入日志
  231. operateLogService.insertOperateLog(user.getId(), EventType.CANCEL_APPLY, JsonHelper.toJson(studentApplyEntity));
  232. }
  233. @Transactional
  234. @Override
  235. public List<Map<String, Object>> importPreExam(LoginUser user, Long teachingId, Integer level, InputStream inputStream) {
  236. checkOpenTime(user.getOrgId());
  237. List<DataMap> lineList;
  238. ExcelReader reader = ExcelReader.create(ExcelType.XLSX, inputStream, 0);
  239. try {
  240. lineList = reader.getDataMapList();
  241. } catch (Exception e) {
  242. log.warn("[importPreExam] 读取预考数据失败,msg:{}", e.getMessage());
  243. throw new StatusException("Excel解析失败,请检查文件格式");
  244. }
  245. if (!Arrays.equals(EXCEL_HEADER, reader.getColumnNames())) {
  246. throw new StatusException("Excel表头错误,请不要修改模版中的表头");
  247. }
  248. if (CollectionUtils.isEmpty(lineList)) {
  249. throw new StatusException("Excel中没有任何考生预约数据");
  250. }
  251. List<Map<String, Object>> failRecords = new ArrayList<>();
  252. RLock lock = (RLock) concurrentService.getLock(CacheConstants.LOCK_AUTO_APPLY);
  253. try {
  254. if (!lock.tryLock()) {
  255. log.warn("[importPreExam] 其他教学点正在执行预约,获取锁失败!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);
  256. throw new StatusException("系统繁忙,请稍后重试!");
  257. }
  258. log.warn("[importPreExam] 获取锁成功!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);
  259. CurrentApplyTaskVO task = cacheService.currentApplyTask(user.getOrgId());
  260. // 开放式预约开始时间
  261. Long openSelfStartTime = task.getOpenApplyStartTime();
  262. // 开放式预约结束时间
  263. Long openSelfEndTime = task.getOpenApplyEndTime();
  264. long now = System.currentTimeMillis();
  265. List<StudentImportVO> applyList = new ArrayList<>();
  266. //教学点缓存
  267. Map<String, Long> teachingCache = getTeachingCache(level);
  268. //考点缓存
  269. Map<String, Long> examSiteCache = getExamSiteCache();
  270. //考试时段缓存
  271. Map<String, TimePeriodEntity> timePeriodCache = getTimePeriodCache();
  272. //考点时段缓存
  273. Map<Long, Map<String, TimePeriodEntity>> timePeriodExamSiteCache = getTimePeriodExamSiteCache(task.getTaskId());
  274. //考点所属教学点缓存
  275. Map<Long, Long> examSiteCategoryCache = getExamSiteCategoryCache();
  276. for (int i = 0; i < lineList.size(); i++) {
  277. DataMap line = lineList.get(i);
  278. List<AgentAndTimeVO> agentTimeList = new ArrayList<>();
  279. AgentAndTimeVO agentTime = new AgentAndTimeVO();
  280. StudentImportVO apply = new StudentImportVO();
  281. StringBuilder msg = new StringBuilder();
  282. String studentCode = trimAndNullIfBlank(line.get(EXCEL_HEADER[0]));
  283. if (StringUtils.isBlank(studentCode)) {
  284. msg.append(" 学号不能为空");
  285. }
  286. String name = trimAndNullIfBlank(line.get(EXCEL_HEADER[1]));
  287. if (StringUtils.isBlank(name)) {
  288. msg.append(" 姓名不能为空");
  289. }
  290. String identityNumber = trimAndNullIfBlank(line.get(EXCEL_HEADER[2]));
  291. if (StringUtils.isBlank(identityNumber)) {
  292. msg.append(" 证件号码不能为空");
  293. }
  294. //判断考生是否存在
  295. StudentEntity studentEntity = checkStudent(studentCode, name, identityNumber, task.getTaskId());
  296. if (studentEntity == null) {
  297. msg.append(" 考生信息填写错误");
  298. failRecords.add(newError(i + 1, msg.toString()));
  299. continue;
  300. } else {
  301. apply.setStudentId(studentEntity.getId());
  302. apply.setApplyNumber(studentEntity.getApplyNumber());
  303. }
  304. String teachingName = trimAndNullIfBlank(line.get(EXCEL_HEADER[3]));
  305. if (StringUtils.isBlank(teachingName)) {
  306. msg.append(" 考生所属教学点不能为空");
  307. }
  308. Long categoryId = teachingCache.get(teachingName);
  309. if (categoryId == null) {
  310. msg.append(" 考生所属教学点不存在");
  311. failRecords.add(newError(i + 1, msg.toString()));
  312. continue;
  313. }
  314. if (!studentEntity.getCategoryId().equals(categoryId)) {
  315. msg.append(" 导入考生的所属教学点和系统中的考生教学点不匹配");
  316. }
  317. if (user.getRole().equals(Role.TEACHING) && !categoryId.equals(teachingId)) {
  318. msg.append(" 非本教学点考生");
  319. }
  320. String examSiteName = trimAndNullIfBlank(line.get(EXCEL_HEADER[4]));
  321. if (StringUtils.isBlank(examSiteName)) {
  322. msg.append(" 预约考点1不能为空");
  323. }
  324. //考点所属教学点
  325. Long applyTeachingId;
  326. Long examSiteId = examSiteCache.get(examSiteName);
  327. if (examSiteId == null) {
  328. msg.append(" 预约考点1不存在");
  329. } else {
  330. applyTeachingId = examSiteCategoryCache.get(examSiteId);
  331. if ((now < openSelfStartTime || now > openSelfEndTime) && !categoryId.equals(applyTeachingId)) {
  332. msg.append(" 未到自由预约时间,不能预约其他教学点下的考点");
  333. }
  334. //教学点管理员导入-不能预约其他教学点下的考点
  335. if (user.getRole().equals(Role.TEACHING) && !categoryId.equals(applyTeachingId)) {
  336. msg.append(" 不能预约其他教学点下的考点");
  337. }
  338. }
  339. //预约的时段
  340. String timePeriodStr = trimAndNullIfBlank(line.get(EXCEL_HEADER[5]));
  341. if (StringUtils.isBlank(timePeriodStr)) {
  342. msg.append(" 预约时段1不能为空");
  343. }
  344. TimePeriodEntity timePeriodEntity;
  345. Map<String, TimePeriodEntity> examSiteTimePeriodMap;
  346. try {
  347. //考点下的预约时段
  348. examSiteTimePeriodMap = timePeriodExamSiteCache.get(examSiteId);
  349. if (examSiteTimePeriodMap != null) {
  350. timePeriodCache = examSiteTimePeriodMap;
  351. }
  352. timePeriodEntity = checkTimePeriod(timePeriodStr, timePeriodCache);
  353. agentTime.setAgentId(examSiteId);
  354. agentTime.setTimePeriodId(timePeriodEntity.getId());
  355. agentTime.setStartTime(timePeriodEntity.getStartTime());
  356. agentTimeList.add(agentTime);
  357. } catch (StatusException e) {
  358. msg.append(" ").append(e.getMessage());
  359. }
  360. int index = 0;
  361. for (int num = 1; num <= maxApplyNum; num++) {
  362. String agentName = trimAndNullIfBlank(line.get(EXCEL_HEADER[num + 5 + index]));
  363. String timePeriodName = trimAndNullIfBlank(line.get(EXCEL_HEADER[num + 6 + index]));
  364. if (StringUtils.isBlank(agentName) && StringUtils.isBlank(timePeriodName)) {
  365. apply.setAgentTimeList(agentTimeList);
  366. applyList.add(apply);
  367. if (msg.length() > 0) {
  368. failRecords.add(newError(i + 1, msg.toString()));
  369. }
  370. break;
  371. } else {
  372. examSiteId = examSiteCache.get(agentName);
  373. if (examSiteId == null) {
  374. msg.append(" 预约考点").append(num + 1).append("不存在");
  375. } else {
  376. applyTeachingId = examSiteCategoryCache.get(examSiteId);
  377. if ((now < openSelfStartTime || now > openSelfEndTime) && !applyTeachingId.equals(categoryId)) {
  378. msg.append(" 未到自由预约时间,不允许预约其他教学点的考点");
  379. }
  380. //教学点管理员导入-不能预约其他教学点下的考点
  381. if (user.getRole().equals(Role.TEACHING) && !categoryId.equals(applyTeachingId)) {
  382. msg.append(" 不能预约其他教学点下的考点");
  383. }
  384. }
  385. try {
  386. //考点下的预约时段
  387. examSiteTimePeriodMap = timePeriodExamSiteCache.get(examSiteId);
  388. if (examSiteTimePeriodMap != null) {
  389. timePeriodCache = examSiteTimePeriodMap;
  390. }
  391. timePeriodEntity = checkTimePeriod(timePeriodName, timePeriodCache);
  392. agentTime = new AgentAndTimeVO();
  393. agentTime.setAgentId(examSiteId);
  394. agentTime.setTimePeriodId(timePeriodEntity.getId());
  395. agentTime.setStartTime(timePeriodEntity.getStartTime());
  396. agentTimeList.add(agentTime);
  397. } catch (StatusException e) {
  398. msg.append(" ").append(e.getMessage());
  399. }
  400. }
  401. ++index;
  402. }
  403. // 处理导入预考模版中填满的情况
  404. if(apply.getAgentTimeList() == null || apply.getAgentTimeList().isEmpty()) {
  405. apply.setAgentTimeList(agentTimeList);
  406. applyList.add(apply);
  407. //导入预考模版中填满的情况 如果有错误,加入到错误记录中(bug解决)
  408. if(msg.length() > 0) {
  409. failRecords.add(newError(i + 1, msg.toString()));
  410. }
  411. }
  412. //检测考生是否已经完成预约和超出预约的最大次数
  413. checkStudentTimePeriod(apply, i, task.getAllowApplyCancelDays(), failRecords);
  414. }
  415. //考点容量判断
  416. checkExamSiteTimeCapacity(applyList, failRecords);
  417. if (CollectionUtils.isNotEmpty(failRecords)) {
  418. return failRecords;
  419. }
  420. //保存数据
  421. for (int i = 0; i < applyList.size(); i++) {
  422. StudentImportVO vo = applyList.get(i);
  423. try {
  424. List<AgentAndTimeVO> agentTimeList = vo.getAgentTimeList();
  425. String studentApplyLockKey = String.format(CacheConstants.LOCK_STUDENT_APPLY, vo.getStudentId());
  426. RLock studentApplyLock = (RLock) concurrentService.getLock(studentApplyLockKey);
  427. try {
  428. if (!studentApplyLock.tryLock()) {
  429. log.warn("[importPreExam] 获取锁失败,考生在同时操作预约,lockKey:{}", studentApplyLockKey);
  430. } else {
  431. log.warn("[importPreExam] 获取锁成功,lockKey:{}", studentApplyLockKey);
  432. for (AgentAndTimeVO agentTimeVO : agentTimeList) {
  433. StudentApplyEntity entity = new StudentApplyEntity();
  434. entity.setStudentId(vo.getStudentId());
  435. entity.setExamSiteId(agentTimeVO.getAgentId());
  436. entity.setTimePeriodId(agentTimeVO.getTimePeriodId());
  437. entity.setCancel(Boolean.FALSE);
  438. entity.setOperateId(user.getId());
  439. //保存考生预约
  440. boolean successFlag = saveOrUpdateStudentApply(entity);
  441. if (successFlag) {
  442. // 队列bean
  443. ApplyRecordCacheBean bean = new ApplyRecordCacheBean();
  444. bean.setStudentId(vo.getStudentId());
  445. bean.setExamSiteId(agentTimeVO.getAgentId());
  446. bean.setTimePeriodId(agentTimeVO.getTimePeriodId());
  447. bean.setCancel(Boolean.FALSE);
  448. bean.setOperateId(user.getId());
  449. bean.setOperateTime(System.currentTimeMillis());
  450. bean.setBizId(cacheService.increaseBizId());
  451. // 某考点某时段的“剩余可约数量”(抢占1个数量)
  452. boolean takeSuccess = cacheService.decreaseApplyAvailableCount(bean.getExamSiteId(), bean.getTimePeriodId());
  453. if (!takeSuccess) {
  454. log.warn("[importPreExam] 预约失败,当前预约时段已约满!examSiteId:{} timePeriodId:{} studentId:{}",
  455. bean.getExamSiteId(), bean.getTimePeriodId(), bean.getStudentId());
  456. //考点
  457. ExamSiteCacheBean examSiteBean = examSiteCacheService.getExamSiteById(bean.getExamSiteId());
  458. //时段
  459. TimePeriodEntity timePeriod = timePeriodService.getById(bean.getTimePeriodId());
  460. String message = MessageFormat.format("第{0}行,预约考点:{1},预约时段:{2}", i + 1, examSiteBean.getExamSiteName(),
  461. DateUtil.getStartAndEndTime(timePeriod.getStartTime(), timePeriod.getEndTime()));
  462. throw new StatusException(message);
  463. }
  464. // 保存至预约缓存
  465. cacheService.saveStudentApplyRecord(bean);
  466. }
  467. }
  468. }
  469. } catch (Exception e) {
  470. log.error("[importPreExam] 导入预约失败,msg:{}", e.getMessage());
  471. throw new StatusException("导入预考失败,错误原因:" + e.getMessage());
  472. } finally {
  473. try {
  474. if (studentApplyLock.isLocked() && studentApplyLock.isHeldByCurrentThread()) {
  475. studentApplyLock.unlock();
  476. log.info("[importPreExam] 解锁成功!lockKey:{}", studentApplyLockKey);
  477. }
  478. } catch (Exception e) {
  479. log.warn(e.getMessage());
  480. }
  481. }
  482. } catch (StatusException e) {
  483. failRecords.add(newError(i + 1, "系统异常"));
  484. log.error("[importPreExam] 导入异常", e);
  485. }
  486. }
  487. } catch (StatusException e) {
  488. log.error("[importPreExam] 导入预考失败,msg:{}", e.getMessage());
  489. throw new StatusException(e.getMessage());
  490. } finally {
  491. try {
  492. // 解锁前检查当前线程是否持有该锁
  493. if (lock.isLocked() && lock.isHeldByCurrentThread()) {
  494. lock.unlock();
  495. log.info("解锁成功!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY);
  496. }
  497. } catch (Exception e) {
  498. log.warn(e.getMessage());
  499. }
  500. }
  501. return failRecords;
  502. }
  503. private Map<Long, Map<String, TimePeriodEntity>> getTimePeriodExamSiteCache(Long taskId) {
  504. //学校管理员设置的所有预约时段
  505. List<TimePeriodExamSiteBean> timePeriodList = timePeriodService.listTimePeriodByTask(taskId);
  506. Map<Long, Map<String, TimePeriodEntity>> timePeriodExamSiteCache = new HashMap<>();
  507. List<TimePeriodExamSiteBean> timePeriodExamSiteBeans = timePeriodService.listTimePeriodByExamSiteId(taskId, null);
  508. /*// 过滤开启的预约时段
  509. timePeriodExamSiteBeans = timePeriodExamSiteBeans.stream()
  510. .filter(TimePeriodExamSiteBean::getEnable)
  511. .collect(Collectors.toList());*/
  512. // 按照考点分组
  513. Map<Long, List<TimePeriodExamSiteBean>> timePeriodMap = timePeriodExamSiteBeans.stream()
  514. .collect(Collectors.groupingBy(TimePeriodExamSiteBean::getExamSiteId));
  515. Set<Map.Entry<Long, List<TimePeriodExamSiteBean>>> entries = timePeriodMap.entrySet();
  516. for (Map.Entry<Long, List<TimePeriodExamSiteBean>> entry : entries) {
  517. Long examSiteId = entry.getKey();
  518. List<TimePeriodExamSiteBean> timePeriodExamSiteBeansList = entry.getValue();
  519. //取并集
  520. timePeriodExamSiteBeansList = UnionUtil.unionByAttribute(timePeriodExamSiteBeansList, timePeriodList, TimePeriodExamSiteBean::getTimePeriodId);
  521. // 过滤开启的预约时段
  522. timePeriodExamSiteBeansList = timePeriodExamSiteBeansList.stream()
  523. .filter(TimePeriodExamSiteBean::getEnable)
  524. .collect(Collectors.toList());
  525. // 时段转换为Map key: start_time + end_time
  526. Map<String, TimePeriodEntity> map = getStringTimePeriodEntityMap(timePeriodExamSiteBeansList);
  527. timePeriodExamSiteCache.put(examSiteId, map);
  528. }
  529. return timePeriodExamSiteCache;
  530. }
  531. private Map<String, TimePeriodEntity> getStringTimePeriodEntityMap(List<TimePeriodExamSiteBean> timePeriodExamSiteBeansList) {
  532. Map<String, TimePeriodEntity> map = new HashMap<>(timePeriodExamSiteBeansList.size());
  533. for (TimePeriodExamSiteBean bean : timePeriodExamSiteBeansList) {
  534. TimePeriodEntity entity = new TimePeriodEntity();
  535. entity.setId(bean.getTimePeriodId());
  536. entity.setStartTime(bean.getStartTime());
  537. map.put(bean.getStartTime() + "-" + bean.getEndTime(), entity);
  538. }
  539. return map;
  540. }
  541. private StudentApplyEntity findStudentApply(StudentApplyEntity studentApply) {
  542. LambdaQueryWrapper<StudentApplyEntity> lm = new LambdaQueryWrapper<>();
  543. lm.eq(StudentApplyEntity::getExamSiteId, studentApply.getExamSiteId());
  544. lm.eq(StudentApplyEntity::getTimePeriodId, studentApply.getTimePeriodId());
  545. lm.eq(StudentApplyEntity::getStudentId, studentApply.getStudentId());
  546. return baseMapper.selectOne(lm);
  547. }
  548. private void checkStudentTimePeriod(StudentImportVO studentImportVO, int row, int allowCancelDays, List<Map<String, Object>> failRecords) {
  549. List<AgentAndTimeVO> agentTimeList = studentImportVO.getAgentTimeList();
  550. //是否重复预约时段
  551. boolean isDuplicate = agentTimeList.stream()
  552. .collect(Collectors.groupingBy(AgentAndTimeVO::getTimePeriodId, Collectors.counting()))
  553. .entrySet()
  554. .stream()
  555. .anyMatch(entry -> entry.getValue() > 1);
  556. if (isDuplicate) {
  557. failRecords.add(newError(row + 1, "同一个考生预约的时间不能相同"));
  558. }
  559. //预约的日期,是否在可取消日期之后
  560. LocalDate day = LocalDate.now().plusDays(allowCancelDays);
  561. long epochMilli = day.plusDays(1).atStartOfDay(ZoneId.systemDefault()).minusSeconds(1).toInstant().toEpochMilli();
  562. for (AgentAndTimeVO time : agentTimeList) {
  563. if (time.getStartTime() <= epochMilli) {
  564. failRecords.add(newError(row + 1, "预约的考试时段,只能为" + allowCancelDays + "天之后的时段"));
  565. }
  566. }
  567. /* 数据库的方式
  568. List<StudentApplyEntity> haveApplyList = listStudentHaveApply(studentImportVO.getStudentId());
  569. int studentApplyFinishCount = haveApplyList.size();*/
  570. // 考生已经预约的次数
  571. int studentApplyFinishCount = cacheService.getStudentApplyFinishCount(studentImportVO.getStudentId());
  572. List<AgentAndTimeVO> toBeApplyTimeList = new ArrayList<>();
  573. //考生已完成预约
  574. if (studentApplyFinishCount >= studentImportVO.getApplyNumber()) {
  575. studentImportVO.setAgentTimeList(toBeApplyTimeList);
  576. failRecords.add(newError(row + 1, "考生已完成预约,请先取消考生已预约的信息"));
  577. }
  578. //判断是否超过可预约次数
  579. if (studentImportVO.getApplyNumber() < studentApplyFinishCount + agentTimeList.size()) {
  580. failRecords.add(newError(row + 1, "超过考生可预约的最大数量:" + studentImportVO.getApplyNumber()));
  581. }
  582. }
  583. private void checkExamSiteTimeCapacity(List<StudentImportVO> applyList, List<Map<String, Object>> failRecords) {
  584. try {
  585. //导入的所有时段
  586. List<AgentAndTimeVO> availableTimeList = new ArrayList<>();
  587. applyList.forEach(item -> availableTimeList.addAll(item.getAgentTimeList()));
  588. //按照考点+时段 分组
  589. Map<Long, Map<Long, List<AgentAndTimeVO>>> agentTimeMap = availableTimeList.stream()
  590. .collect(Collectors.groupingBy(AgentAndTimeVO::getAgentId, Collectors.groupingBy(AgentAndTimeVO::getTimePeriodId)));
  591. for (Map.Entry<Long, Map<Long, List<AgentAndTimeVO>>> examSiteEntry : agentTimeMap.entrySet()) {
  592. Long examSiteId = examSiteEntry.getKey();
  593. Map<Long, List<AgentAndTimeVO>> timePeriodMap = examSiteEntry.getValue();
  594. for (Map.Entry<Long, List<AgentAndTimeVO>> timePeriodEntry : timePeriodMap.entrySet()) {
  595. Long timePeriodKey = timePeriodEntry.getKey();
  596. //考点下某个时段可预约的容量
  597. int availableCount = cacheService.getApplyAvailableCount(examSiteId, timePeriodKey);
  598. // 导入的某个考点,某个时段的数量
  599. int impTimePeriodNum = timePeriodEntry.getValue().size();
  600. if (impTimePeriodNum > availableCount) {
  601. ExamSiteCacheBean examSiteBean = examSiteCacheService.getExamSiteById(examSiteId);
  602. if (examSiteBean != null) {
  603. String examSiteName = examSiteBean.getExamSiteName();
  604. TimePeriodEntity timePeriod = timePeriodService.getById(timePeriodKey);
  605. String startAndEndTime = DateUtil.getStartAndEndTime(timePeriod.getStartTime(), timePeriod.getEndTime());
  606. log.warn("[importPreExam],{} {} 剩余容量为:{},而导入的数量为:{}", examSiteName, startAndEndTime, availableCount, impTimePeriodNum);
  607. failRecords.add(newError(0,
  608. examSiteName + " " + startAndEndTime + " 剩余容量为:" + (availableCount) + ",而导入的数量为:" + impTimePeriodNum));
  609. }
  610. }
  611. }
  612. }
  613. } catch (Exception e) {
  614. log.error("[importPreExam],导入出错,msg:{}", e.getMessage());
  615. failRecords.add(newError(0, "导入失败"));
  616. }
  617. }
  618. private TimePeriodEntity checkTimePeriod(String timePeriodStr, Map<String, TimePeriodEntity> timeCache) {
  619. if (timePeriodStr == null || timePeriodStr.split(" ").length != 2) {
  620. throw new StatusException(" 预约时段格式不正确");
  621. }
  622. String[] arr = timePeriodStr.split("-");
  623. String startTime = arr[0] + ":00";
  624. String endTime = startTime.substring(0, startTime.indexOf("日") + 1) + " " + arr[1] + ":00";
  625. Long startTimeLong = DateUtil.getLongTimeByZHDate(startTime);
  626. Long endTimeLong = DateUtil.getLongTimeByZHDate(endTime);
  627. if (timeCache.get(startTimeLong + "-" + endTimeLong) == null) {
  628. throw new StatusException(" 预约时段不存在");
  629. }
  630. return timeCache.get(startTimeLong + "-" + endTimeLong);
  631. }
  632. private Map<String, Object> newError(int lineNum, String msg) {
  633. Map<String, Object> map = new HashMap<>();
  634. map.put("lineNum", lineNum);
  635. map.put("msg", msg);
  636. return map;
  637. }
  638. private StudentEntity checkStudent(String studentCode, String studentName, String identityNumber, Long taskId) {
  639. LambdaQueryWrapper<StudentEntity> wrapper = new LambdaQueryWrapper<>();
  640. wrapper.eq(StudentEntity::getStudentCode, studentCode);
  641. wrapper.eq(StudentEntity::getName, studentName);
  642. wrapper.eq(StudentEntity::getIdentityNumber, identityNumber);
  643. wrapper.eq(StudentEntity::getApplyTaskId, taskId);
  644. return studentService.getOne(wrapper);
  645. }
  646. private String trimAndNullIfBlank(String s) {
  647. if (StringUtils.isBlank(s)) {
  648. return null;
  649. }
  650. return s.trim();
  651. }
  652. private Map<Long, Long> getExamSiteCategoryCache() {
  653. Map<Long, Long> cache = new HashMap<>();
  654. LambdaQueryWrapper<ExamSiteEntity> wrapper = new LambdaQueryWrapper<>();
  655. wrapper.eq(ExamSiteEntity::getEnable, Boolean.TRUE);
  656. List<ExamSiteEntity> examSiteList = examSiteService.list(wrapper);
  657. examSiteList.forEach(site -> cache.put(site.getId(), site.getCategoryId()));
  658. return cache;
  659. }
  660. private Map<String, TimePeriodEntity> getTimePeriodCache() {
  661. Map<String, TimePeriodEntity> map = new HashMap<>();
  662. LambdaQueryWrapper<TimePeriodEntity> wrapper = new LambdaQueryWrapper<>();
  663. wrapper.eq(TimePeriodEntity::getApplyTaskId, getApplyTask().getId());
  664. List<TimePeriodEntity> timeList = timePeriodService.list(wrapper);
  665. for (TimePeriodEntity time : timeList) {
  666. map.put(time.getStartTime() + "-" + time.getEndTime(), time);
  667. }
  668. return map;
  669. }
  670. private Map<String, Long> getExamSiteCache() {
  671. LambdaQueryWrapper<ExamSiteEntity> wrapper = new LambdaQueryWrapper<>();
  672. wrapper.eq(ExamSiteEntity::getEnable, Boolean.TRUE);
  673. List<ExamSiteEntity> categoryList = examSiteService.list(wrapper);
  674. return categoryList.stream().collect(Collectors.toMap(ExamSiteEntity::getName, ExamSiteEntity::getId));
  675. }
  676. private Map<String, Long> getTeachingCache(Integer level) {
  677. LambdaQueryWrapper<CategoryEntity> wrapper = new LambdaQueryWrapper<>();
  678. wrapper.eq(CategoryEntity::getEnable, Boolean.TRUE);
  679. wrapper.eq(CategoryEntity::getLevel, level == null ? CategoryLevel.TEACHING.getValue() : level);
  680. List<CategoryEntity> categoryList = categoryService.list(wrapper);
  681. return categoryList.stream().collect(Collectors.toMap(CategoryEntity::getName, CategoryEntity::getId));
  682. }
  683. private void checkOpenTime(Long orgId) {
  684. CurrentApplyTaskVO task = cacheService.currentApplyTask(orgId);
  685. if(task == null) {
  686. throw new StatusException("未有开启的任务");
  687. }
  688. Date startTime = new Date(task.getSelfApplyStartTime());
  689. Date endTime = new Date(task.getOpenApplyEndTime());
  690. Date now = new Date();
  691. if (now.before(startTime) || now.after(endTime)) {
  692. throw new StatusException("未到开放时间!");
  693. }
  694. }
  695. @Override
  696. public void autoAssign(Long taskId, Long operateId) {
  697. RLock lock = (RLock) concurrentService.getLock(CacheConstants.LOCK_AUTO_APPLY);
  698. if (lock.isLocked()) {
  699. log.warn("[autoAssign] 获取锁失败, taskId:{}, lockKey:{}", taskId, CacheConstants.LOCK_AUTO_APPLY);
  700. throw new StatusException("自动分配执行未结束,请不要重复执行");
  701. }
  702. // 自动分配须在第一阶段之后和第三阶段开始之前
  703. CurrentApplyTaskVO curApplyTask = applyTaskService.currentApplyTask(null);
  704. Date selfEndTime = new Date(curApplyTask.getSelfApplyEndTime());
  705. Date openStartTime = new Date(curApplyTask.getOpenApplyStartTime());
  706. if (!DateUtil.isBetwwen(selfEndTime, openStartTime)) {
  707. throw new StatusException("自动分配,时间必须要在第一阶段结束之后,第三阶段开始之前");
  708. }
  709. //是否有可预约的时段
  710. List<TimePeriodEntity> timePeriodList = listTimePeriod(taskId);
  711. timePeriodList = listNoCancelTimePeriod(timePeriodList, taskId);
  712. if (CollectionUtils.isEmpty(timePeriodList)) {
  713. log.warn("[autoAssign] 当前时间没有可预约的时段, taskId:{}", taskId);
  714. throw new StatusException("当前时间没有可预约的时段");
  715. }
  716. //写入异步任务
  717. Map<String, Object> taskMap = asyncTaskService.saveAsyncTask(AsyncTaskType.AUTO_ASSIGN, operateId);
  718. taskMap.computeIfAbsent("taskId", v -> taskId);
  719. taskMap.computeIfAbsent("operateId", v -> operateId);
  720. try {
  721. autoAssignStudentService.assignTask(taskMap);
  722. } catch (Exception e) {
  723. throw new StatusException(e.getMessage());
  724. }
  725. }
  726. private List<ExamSiteEntity> listExamSite(Long teachingId, Long examSiteId) {
  727. LambdaQueryWrapper<ExamSiteEntity> wrapper = new LambdaQueryWrapper<>();
  728. wrapper.eq(ExamSiteEntity::getCategoryId, teachingId);
  729. wrapper.eq(examSiteId != null, ExamSiteEntity::getId, examSiteId);
  730. wrapper.eq(ExamSiteEntity::getEnable, Boolean.TRUE);
  731. return examSiteService.list(wrapper);
  732. }
  733. private List<TimePeriodEntity> listNoCancelTimePeriod(List<TimePeriodEntity> timePeriodList, Long taskId) {
  734. ApplyTaskEntity task = applyTaskService.getById(taskId);
  735. Long longToday = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(new Date()) + " 00:00:00");
  736. Date today = new Date(longToday);
  737. Date otherDay = DateUtil.addValues(today, Calendar.DAY_OF_MONTH, task.getAllowApplyCancelDays());
  738. Long longOtherDay = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(otherDay) + " 23:59:59");
  739. return timePeriodList.stream().filter(time -> time.getStartTime() > longOtherDay).collect(Collectors.toList());
  740. }
  741. private List<TimePeriodEntity> listTimePeriod(Long taskId) {
  742. LambdaQueryWrapper<TimePeriodEntity> wrapper = new LambdaQueryWrapper<>();
  743. wrapper.eq(TimePeriodEntity::getApplyTaskId, taskId);
  744. wrapper.orderByAsc(TimePeriodEntity::getStartTime);
  745. List<TimePeriodEntity> timeList = timePeriodService.list(wrapper);
  746. if (timeList.isEmpty()) {
  747. log.warn("当前任务未配置可用的考试时段,taskId:{}", taskId);
  748. throw new StatusException("考试时段未设置");
  749. }
  750. return timeList;
  751. }
  752. @Transactional
  753. @Override
  754. public void autoLayout(Long teachingId) {
  755. OrgInfo org = orgCacheService.currentOrg();
  756. CurrentApplyTaskVO applyTask = cacheService.currentApplyTask(org.getOrgId());
  757. if(applyTask == null) {
  758. log.warn("[autoLayout] 未有开启的任务");
  759. return;
  760. }
  761. String autoLayoutLockKey = String.format(CacheConstants.LOCK_ARRANGE_EXAM, DateUtil.formatShortDateString(new Date()));
  762. RLock autoLayoutLock = (RLock) concurrentService.getLock(autoLayoutLockKey);
  763. try {
  764. if (!autoLayoutLock.tryLock()) {
  765. log.warn("获取锁失败,已有线程在执行排考!lockKey:{}", autoLayoutLock);
  766. return;
  767. }
  768. log.warn("获取锁成功!lockKey:{}", autoLayoutLockKey);
  769. // 1.根据当前日期,查询不能取消的时段
  770. List<TimePeriodEntity> timePeriodList = listTimePeriod(applyTask.getTaskId());
  771. List<TimePeriodEntity> noCancelTimePeroidList = listNoCancelApplyTimePeriod(timePeriodList, applyTask.getAllowApplyCancelDays());
  772. if (noCancelTimePeroidList.isEmpty()) {
  773. log.warn("[autoLayout] 没有待排考的时段");
  774. return;
  775. }
  776. // 2.查询待排考的考生
  777. List<Long> timePeriodIds = noCancelTimePeroidList.stream()
  778. .map(BaseEntity::getId)
  779. .collect(Collectors.toList());
  780. List<StudentApplyEntity> toBeLayoutStudentList = baseMapper.listTimePeriod(timePeriodIds, Boolean.FALSE);
  781. if (CollectionUtils.isEmpty(toBeLayoutStudentList)) {
  782. log.warn("[autoLayout] 没有待排考的考生");
  783. return;
  784. }
  785. // 3.开始排考
  786. Map<Long, List<StudentApplyEntity>> toBeLayoutStudentMap = toBeLayoutStudentList.stream()
  787. .collect(Collectors.groupingBy(StudentApplyEntity::getExamSiteId));
  788. for (Long examSiteId : toBeLayoutStudentMap.keySet()) {
  789. List<StudentApplyEntity> studentApplyList= toBeLayoutStudentMap.get(examSiteId);
  790. Map<Long, List<StudentApplyEntity>> timeLayoutStudentMap = studentApplyList.stream()
  791. .collect(Collectors.groupingBy(StudentApplyEntity::getTimePeriodId));
  792. List<ExamRoomEntity> roomList = listExamRoom(examSiteId);
  793. if (roomList.isEmpty()) {
  794. log.warn("{}:未设置考场", examSiteId);
  795. return;
  796. }
  797. ExamSiteEntity examSite = examSiteService.getById(examSiteId);
  798. layoutStudentByTimePeriod(applyTask.getTaskId(), examSite, roomList, timeLayoutStudentMap);
  799. }
  800. } catch (StatusException e) {
  801. log.error(e.getMessage());
  802. } finally {
  803. try {
  804. // 解锁前检查当前线程是否持有该锁
  805. if (autoLayoutLock.isLocked() && autoLayoutLock.isHeldByCurrentThread()) {
  806. autoLayoutLock.unlock();
  807. log.info("解锁成功!lockKey:{}", autoLayoutLockKey);
  808. }
  809. } catch (Exception e) {
  810. log.warn(e.getMessage());
  811. }
  812. }
  813. }
  814. private void layoutStudentByTimePeriod(Long taskId, ExamSiteEntity examSite, List<ExamRoomEntity> roomList,
  815. Map<Long, List<StudentApplyEntity>> timeLayoutStudentMap) {
  816. for (Long timePeriodId : timeLayoutStudentMap.keySet()) {
  817. List<StudentApplyEntity> studentApplyList = timeLayoutStudentMap.get(timePeriodId);
  818. TimePeriodEntity timePeriod = timePeriodService.getById(timePeriodId);
  819. layoutStudentToRoom(taskId, examSite, roomList, studentApplyList, timePeriod);
  820. }
  821. }
  822. private void layoutStudentToRoom(Long taskId, ExamSiteEntity examSite, List<ExamRoomEntity> roomList,
  823. List<StudentApplyEntity> studentApplyList, TimePeriodEntity timePeriod) {
  824. Integer timePeriodOrder = getTimePeriodOrder(taskId, timePeriod);
  825. for (ExamRoomEntity room : roomList) {
  826. int num = 0;
  827. for (Iterator<StudentApplyEntity> iterator = studentApplyList.iterator(); iterator.hasNext(); ) {
  828. StudentApplyEntity student = iterator.next();
  829. if (num >= room.getCapacity()) {
  830. break;
  831. }
  832. //座位号
  833. String seatNumber = StringUtils.leftPad(String.valueOf(++num), 3, '0');
  834. student.setExamRoomId(room.getId());
  835. student.setSeatNumber(seatNumber);
  836. // 准考证号
  837. String ticketNum = DateUtil.formatShortDateString(new Date()) + timePeriodOrder + examSite.getCode() + room.getCode() + seatNumber;
  838. student.setTicketNumber(ticketNum);
  839. baseMapper.updateById(student);
  840. iterator.remove();
  841. }
  842. }
  843. }
  844. private Integer getTimePeriodOrder(Long taskId, TimePeriodEntity timePeriod) {
  845. List<TimePeriodEntity> timeList = listTimePeriod(taskId);
  846. List<TimePeriodEntity> sameDayTimeList = listSameDayTimePeriod(timeList, timePeriod.getStartTime());
  847. for (int i = 0; i < sameDayTimeList.size(); i++) {
  848. TimePeriodEntity time = sameDayTimeList.get(i);
  849. if (time.getStartTime().equals(timePeriod.getStartTime()))
  850. return i + 1;
  851. }
  852. return 0;
  853. }
  854. private List<TimePeriodEntity> listNoCancelApplyTimePeriod(List<TimePeriodEntity> list, Integer allowApplyCancelDays) {
  855. // 不能修改预约的日期
  856. Date noCancelDate = DateUtil.addValues(Calendar.DAY_OF_MONTH, allowApplyCancelDays);
  857. return list.stream()
  858. .filter( item -> isSameDay(noCancelDate, new Date(item.getStartTime())))
  859. .collect(Collectors.toList());
  860. /*
  861. List<TimePeriodEntity> noCancelTimePeroidList;
  862. for (TimePeriodEntity time : list) {
  863. // 预约的时段 和 不能修改预约的日期
  864. if (isSameDay(noCancelDate, new Date(time.getStartTime())))
  865. noCancelTimePeroidList.add(time);
  866. }*/
  867. }
  868. @Override
  869. public File downloadSignIn(Long teachingId, Long agentId, Long examDate) {
  870. ApplyTaskEntity task = getApplyTask();
  871. List<TimePeriodEntity> timePeriodList = listTimePeriod(task.getId());
  872. List<TimePeriodEntity> sameDayTimePeriodList = listSameDayTimePeriod(timePeriodList, examDate);
  873. if (sameDayTimePeriodList.isEmpty()) {
  874. log.warn("[downloadSignIn] 没有查询到当天的时段,examDate:{}", examDate);
  875. throw new StatusException("没有可下载的签到表");
  876. }
  877. List<ExamSiteEntity> examSiteList = listExamSite(teachingId, agentId);
  878. if (examSiteList == null || examSiteList.isEmpty()) {
  879. log.warn("[downloadSignIn] teachingId:{},agentId:{} 没有可用的考点", teachingId, agentId);
  880. throw new StatusException("没有可用的考点");
  881. }
  882. File tempDir = new File("temp");
  883. if (!tempDir.exists()) {
  884. try {
  885. boolean success = tempDir.mkdirs();
  886. if (!success) {
  887. log.warn("[downloadSignIn] 临时目录创建失败!");
  888. }
  889. } catch (Exception e) {
  890. log.error("[downloadSignIn] 临时目录创建失败,msg:{}", e.getMessage());
  891. }
  892. }
  893. CategoryCacheBean category = categoryCacheService.getCategoryById(teachingId);
  894. File zipFile = new File(tempDir, category.getName() + "签到表.zip");
  895. ZipWriter writer;
  896. try {
  897. writer = ZipWriter.create(zipFile);
  898. List<File> fileList = new ArrayList<>();
  899. for (ExamSiteEntity examSite : examSiteList) {
  900. fileList.addAll(downloadByExamSite(writer, task.getName(), examSite, sameDayTimePeriodList));
  901. }
  902. if (fileList.isEmpty()) {
  903. throw new StatusException("暂未排考,请等待排考后下载");
  904. }
  905. } catch (IOException e) {
  906. log.error("[downloadSignIn] 创建文件异常,msg:{}", e.getMessage());
  907. throw new StatusException("文件写入异常");
  908. }
  909. writer.close();
  910. return zipFile;
  911. }
  912. public List<File> downloadByExamSite(ZipWriter writer, String taskName, ExamSiteEntity site, List<TimePeriodEntity> timeList) throws IOException {
  913. List<File> fileList = new ArrayList<>();
  914. List<ExamRoomEntity> roomList = listExamRoom(site.getId());
  915. MaterialTitleInfo title = new MaterialTitleInfo();
  916. title.setTaskName(taskName);
  917. title.setSiteName(site.getName());
  918. for (TimePeriodEntity time : timeList) {
  919. title.setTimePeriod(DateUtil.getStartAndEndTime(time.getStartTime(), time.getEndTime()));
  920. for (ExamRoomEntity room : roomList) {
  921. title.setAddress(room.getAddress());
  922. title.setRoomCode(room.getCode());
  923. title.setRoomName(room.getName());
  924. List<StudentApplyVO> studentList = baseMapper.listStudentApply(time.getId(), room.getId());
  925. if (!studentList.isEmpty()) {
  926. File file = materialGenerateService.generateSignInForm(title, studentList);
  927. fileList.add(file);
  928. writer.write(file, title.getSiteName(), title.getTimePeriod() + " 第【" + title.getRoomCode() + "】考场" + ".pdf");
  929. }
  930. }
  931. }
  932. return fileList;
  933. }
  934. public List<ExamRoomEntity> listExamRoom(Long examSiteId) {
  935. LambdaQueryWrapper<ExamRoomEntity> wrapper = new LambdaQueryWrapper<>();
  936. wrapper.eq(ExamRoomEntity::getExamSiteId, examSiteId);
  937. wrapper.eq(ExamRoomEntity::getEnable, Boolean.TRUE);
  938. wrapper.orderByAsc(ExamRoomEntity::getCode);
  939. return examRoomService.list(wrapper);
  940. }
  941. private List<TimePeriodEntity> listSameDayTimePeriod(List<TimePeriodEntity> timePeriodList, Long startTime) {
  942. Date day = new Date(startTime);
  943. List<TimePeriodEntity> resultList = new ArrayList<>();
  944. for (TimePeriodEntity time : timePeriodList) {
  945. if (isSameDay(day, new Date(time.getStartTime())))
  946. resultList.add(time);
  947. }
  948. return resultList.stream().sorted(Comparator.comparing(TimePeriodEntity::getStartTime)).collect(Collectors.toList());
  949. }
  950. @Override
  951. public int countApplyFinishForExamSiteAndTimePeriod(Long examSiteId, Long timePeriodId) {
  952. Integer value = baseMapper.countApplyFinishForExamSiteAndTimePeriod(examSiteId, timePeriodId);
  953. return value != null ? value : 0;
  954. }
  955. @Override
  956. public List<SignInVO> listSignInDate(Long taskId) {
  957. ApplyTaskEntity task;
  958. if (taskId != null) {
  959. task = applyTaskService.getById(taskId);
  960. } else {
  961. LambdaQueryWrapper<ApplyTaskEntity> wrapper = new LambdaQueryWrapper<>();
  962. wrapper.eq(ApplyTaskEntity::getEnable, Boolean.TRUE);
  963. task = applyTaskService.getOne(wrapper);
  964. }
  965. if (task == null) {
  966. log.warn("[listSignDate] 未开启预约任务");
  967. return Collections.emptyList();
  968. }
  969. List<TimePeriodEntity> timePeriodList = listTimePeriod(task.getId());
  970. if (timePeriodList.isEmpty()) {
  971. log.warn("[listSignDate] 未配置考试时段");
  972. return Collections.emptyList();
  973. }
  974. List<SignInVO> signInVoList = new ArrayList<>();
  975. Date today = DateUtils.truncate(new Date(), Calendar.DATE);
  976. if (isInTimePeriod(today, timePeriodList)) {
  977. SignInVO signInVO = new SignInVO();
  978. signInVO.setExamDate(today.getTime());
  979. signInVoList.add(signInVO);
  980. }
  981. for (int i = 1; i <= task.getAllowApplyCancelDays(); i++) {
  982. Date otherDay = DateUtils.addDays(today, i);
  983. if (isInTimePeriod(otherDay, timePeriodList)) {
  984. SignInVO signInVO = new SignInVO();
  985. signInVO.setExamDate(otherDay.getTime());
  986. signInVoList.add(signInVO);
  987. }
  988. }
  989. signInVoList.sort(Comparator.comparing(SignInVO::getExamDate));
  990. return signInVoList;
  991. }
  992. private boolean isInTimePeriod(Date date, List<TimePeriodEntity> timePeriodList) {
  993. for (TimePeriodEntity timePeriod : timePeriodList) {
  994. Date day = new Date(timePeriod.getStartTime());
  995. if (DateUtils.isSameDay(day, date)) {
  996. return true;
  997. }
  998. }
  999. return false;
  1000. }
  1001. @Override
  1002. public List<String[]> listExamSiteAvailable(List<CategoryVO> examSiteList) {
  1003. List<String[]> availableList = new ArrayList<>();
  1004. ApplyTaskEntity task = getApplyTask();
  1005. List<TimePeriodEntity> timePeriodList = listTimePeriod(task.getId());
  1006. for (TimePeriodEntity timePeriod : timePeriodList) {
  1007. String[] availableArr = getAvailableArr(examSiteList, timePeriod);
  1008. availableList.add(availableArr);
  1009. }
  1010. return availableList;
  1011. }
  1012. private String[] getAvailableArr(List<CategoryVO> examSiteList, TimePeriodEntity timePeriod) {
  1013. String[] availableArr = new String[examSiteList.size() + 1];
  1014. availableArr[0] = DateUtil.getStartAndEndTime(timePeriod.getStartTime(), timePeriod.getEndTime());
  1015. for (int i = 0; i < examSiteList.size(); i++) {
  1016. CategoryVO categoryVO = examSiteList.get(i);
  1017. int applyFinishCount = countApplyFinishForExamSiteAndTimePeriod(categoryVO.getId(), timePeriod.getId());
  1018. //剩余的容量
  1019. availableArr[i + 1] = String.valueOf(categoryVO.getCapacity() - applyFinishCount);
  1020. }
  1021. return availableArr;
  1022. }
  1023. @Override
  1024. public List<StudentApplyExport> listStudentApplyDetail(StudentApplyReq req) {
  1025. OrgInfo org = orgCacheService.currentOrg();
  1026. CurrentApplyTaskVO curApplyTask = cacheService.currentApplyTask(org.getOrgId());
  1027. if (curApplyTask == null) {
  1028. return Collections.emptyList();
  1029. }
  1030. return baseMapper.listStudentApplyDetail(req, curApplyTask.getTaskId());
  1031. }
  1032. @Override
  1033. public List<StudentExport> listStudentNoApply(Long teachingId) {
  1034. return Collections.emptyList();
  1035. }
  1036. @Override
  1037. public void exportNoApplyStudent(Long teachingId, Long operateId) {
  1038. //写入异步任务
  1039. Map<String, Object> taskMap = asyncTaskService.saveAsyncTask(AsyncTaskType.NO_FINISH_APPLY_STUDENT,
  1040. operateId);
  1041. taskMap.computeIfAbsent("teachingId", v -> teachingId);
  1042. taskMap.computeIfAbsent("operateId", v -> operateId);
  1043. try {
  1044. studentApplyNoFinishExportService.exportTask(taskMap);
  1045. } catch (Exception e) {
  1046. throw new StatusException(e.getMessage());
  1047. }
  1048. }
  1049. @Override
  1050. public void exportStudentApplyDetail(StudentApplyReq req, Long operateId) {
  1051. //写入异步任务
  1052. Map<String, Object> taskMap = asyncTaskService.saveAsyncTask(AsyncTaskType.STUDENT_APPLY_EXPORT,
  1053. operateId);
  1054. taskMap.computeIfAbsent("operateId", v -> operateId);
  1055. taskMap.computeIfAbsent("condition", v -> req);
  1056. try {
  1057. studentApplyDetailExportService.exportTask(taskMap);
  1058. } catch (Exception e) {
  1059. throw new StatusException(e.getMessage());
  1060. }
  1061. }
  1062. @Transactional
  1063. @Override
  1064. public boolean saveOrUpdateStudentApply(StudentApplyEntity studentApply) {
  1065. // 缓存中考生是否已经预约
  1066. ApplyRecordCacheBean studentApplyRecord = cacheService.getStudentApplyRecord(studentApply.getStudentId(), studentApply.getExamSiteId(),
  1067. studentApply.getTimePeriodId());
  1068. //数据库中考生是否已经预约
  1069. StudentApplyEntity existStudentApply = findStudentApply(studentApply);
  1070. // 缓存中考生未预约
  1071. if (studentApplyRecord == null) {
  1072. // 数据库中不存在
  1073. if (existStudentApply == null) {
  1074. baseMapper.insert(studentApply);
  1075. } else if (existStudentApply.getCancel()) { //数据库中存在,但已取消
  1076. existStudentApply.setCancel(Boolean.FALSE);
  1077. existStudentApply.setOperateId(studentApply.getOperateId());
  1078. baseMapper.updateById(existStudentApply);
  1079. } else { // 数据库中已存在预约信息
  1080. return false;
  1081. }
  1082. return true;
  1083. }
  1084. // 考生先预约,然后做了取消操作
  1085. if (studentApplyRecord.getCancel()) {
  1086. if (existStudentApply != null) {
  1087. existStudentApply.setCancel(Boolean.FALSE);
  1088. existStudentApply.setOperateId(studentApply.getOperateId());
  1089. baseMapper.updateById(existStudentApply);
  1090. } else {
  1091. baseMapper.insert(studentApply);
  1092. }
  1093. return true;
  1094. } else { //考生已经预约
  1095. return false;
  1096. }
  1097. /* // 预约是否已经存在
  1098. StudentApplyEntity existStudentApply = findStudentApply(studentApply);
  1099. if (studentApplyRecord != null) {
  1100. existStudentApply.setCancel(Boolean.FALSE);
  1101. baseMapper.updateById(existStudentApply);
  1102. } else {
  1103. baseMapper.insert(studentApply);
  1104. }*/
  1105. }
  1106. }