StudentApplyServiceImpl.java 62 KB

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