package com.qmth.exam.reserve.service.impl; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.time.LocalDate; import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateUtils; import org.redisson.api.RLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.qmth.boot.core.collection.PageResult; import com.qmth.boot.core.concurrent.service.ConcurrentService; import com.qmth.boot.core.exception.StatusException; import com.qmth.boot.tools.excel.ExcelReader; import com.qmth.boot.tools.excel.enums.ExcelType; import com.qmth.boot.tools.excel.model.DataMap; import com.qmth.boot.tools.io.ZipWriter; import com.qmth.exam.reserve.bean.Constants; import com.qmth.exam.reserve.bean.apply.ApplyRecordCacheBean; import com.qmth.exam.reserve.bean.login.LoginUser; import com.qmth.exam.reserve.bean.stdapply.AgentAndTimeVO; import com.qmth.exam.reserve.bean.stdapply.CategoryVO; import com.qmth.exam.reserve.bean.stdapply.MaterialTitleInfo; import com.qmth.exam.reserve.bean.stdapply.SignInVO; import com.qmth.exam.reserve.bean.stdapply.StudentApplyReq; import com.qmth.exam.reserve.bean.stdapply.StudentApplyVO; import com.qmth.exam.reserve.bean.stdapply.StudentImportVO; import com.qmth.exam.reserve.cache.CacheConstants; import com.qmth.exam.reserve.cache.impl.ApplyTaskCacheService; import com.qmth.exam.reserve.dao.StudentApplyDao; import com.qmth.exam.reserve.entity.ApplyTaskEntity; import com.qmth.exam.reserve.entity.CategoryEntity; import com.qmth.exam.reserve.entity.ExamRoomEntity; import com.qmth.exam.reserve.entity.ExamSiteEntity; import com.qmth.exam.reserve.entity.StudentApplyEntity; import com.qmth.exam.reserve.entity.StudentEntity; import com.qmth.exam.reserve.entity.TimePeriodEntity; import com.qmth.exam.reserve.enums.CategoryLevel; import com.qmth.exam.reserve.enums.EventType; import com.qmth.exam.reserve.service.ApplyTaskService; import com.qmth.exam.reserve.service.CategoryService; import com.qmth.exam.reserve.service.ExamRoomService; import com.qmth.exam.reserve.service.ExamSiteService; import com.qmth.exam.reserve.service.MaterialGenerateService; import com.qmth.exam.reserve.service.OperateLogService; import com.qmth.exam.reserve.service.StudentApplyService; import com.qmth.exam.reserve.service.StudentService; import com.qmth.exam.reserve.service.TimePeriodService; import com.qmth.exam.reserve.util.DateUtil; import com.qmth.exam.reserve.util.JsonHelper; import com.qmth.exam.reserve.util.PageUtil; @Service public class StudentApplyServiceImpl extends ServiceImpl implements StudentApplyService { private final static Logger log = LoggerFactory.getLogger(StudentApplyServiceImpl.class); private static final String[] EXCEL_HEADER = new String[] { "学号", "姓名", "证件号", "所属教学点", "预约考点1", "预约时段1", "预约考点2", "预约时段2", "预约考点3", "预约时段3", "预约考点4", "预约时段4" }; @Autowired private TimePeriodService timePeriodService; @Autowired private ApplyTaskService applyTaskService; @Autowired private CategoryService categoryService; @Autowired private StudentService studentService; @Autowired private ExamSiteService examSiteService; @Autowired private OperateLogService operateLogService; @Autowired private ExamRoomService examRoomService; @Autowired private MaterialGenerateService materialService; @Autowired private ConcurrentService concurrentService; @Autowired private ApplyTaskCacheService cacheService; @Override public PageResult page(StudentApplyReq req) { if (req.getTaskId() == null) { ApplyTaskEntity task = getApplyTask(); req.setTaskId(task.getId()); } if (req.getTeachingId() != null) { List listExamSite = examSiteService.listExamSite(req.getTeachingId()); List examSiteIds = listExamSite.stream().map(CategoryVO::getId).collect(Collectors.toList()); if (!examSiteIds.isEmpty()) { req.setExamSiteIds(examSiteIds); } } IPage iPage = this.baseMapper .page(new Page(req.getPageNumber(), req.getPageSize()), req); return PageUtil.of(iPage); } @Transactional @Override public void cancel(LoginUser user, Long id) { // 时间判断 StudentApplyEntity studentApply = this.baseMapper.selectById(id); if (studentApply == null || studentApply.getTimePeriodId() == null) { throw new StatusException("考生没有预约,无法取消!"); } TimePeriodEntity timePeriod = timePeriodService.getById(studentApply.getTimePeriodId()); if (timePeriod == null) { throw new StatusException("考试时段不存在,请检查考试时段数据!"); } ApplyTaskEntity task = getApplyTask(); Date applyDate = DateUtils.truncate(new Date(timePeriod.getStartTime()), Calendar.DATE); Date canCancelDay = DateUtil.addValues(applyDate, Calendar.DAY_OF_MONTH, -task.getAllowApplyCancelDays()); if (new Date().after(canCancelDay)) { throw new StatusException("可取消时间已过,无法取消!"); } String studentApplyLockKey = String.format(CacheConstants.LOCK_STUDENT_APPLY, studentApply.getStudentId()); RLock studentApplyLock = (RLock) concurrentService.getLock(studentApplyLockKey); try { if (!studentApplyLock.tryLock()) { log.warn("获取锁失败,同一个考生不允许同时操作预约!lockKey:{}", studentApplyLockKey); throw new StatusException(Constants.SYSTEM_BUSY); } else { log.warn("获取锁成功!lockKey:{}", studentApplyLockKey); studentApply.setCancel(Boolean.TRUE); this.baseMapper.updateById(studentApply); ApplyRecordCacheBean bean = new ApplyRecordCacheBean(); bean.setStudentId(studentApply.getStudentId()); bean.setExamSiteId(studentApply.getExamSiteId()); bean.setTimePeriodId(studentApply.getTimePeriodId()); bean.setCancel(Boolean.TRUE); bean.setOperateId(user.getId()); bean.setOperateTime(System.currentTimeMillis()); // 先推送至预约队列 cacheService.pushStudentApplyRecordQueue(bean); cacheService.saveStudentApplyRecord(bean); cacheService.decreaseApplyFinishCount(studentApply.getExamSiteId(), studentApply.getTimePeriodId()); } } catch (Exception e) { throw new StatusException("取消预约失败,请稍后再试!", e); } finally { try { // 解锁前检查当前线程是否持有该锁 if (studentApplyLock.isLocked() && studentApplyLock.isHeldByCurrentThread()) { studentApplyLock.unlock(); log.info("解锁成功!lockKey:{}", studentApplyLockKey); } } catch (Exception e) { log.warn(e.getMessage()); } } operateLogService.insertOperateLog(user.getId(), EventType.CANCEL_APPLY, JsonHelper.toJson(studentApply)); } @Transactional @Override public List> importPreExam(Long userId, Long teachingId, Integer level, InputStream inputStream) { List lineList; ExcelReader reader = ExcelReader.create(ExcelType.XLSX, inputStream, 0); try { lineList = reader.getDataMapList(); } catch (Exception e) { throw new StatusException("Excel 解析失败"); } if (!Arrays.equals(EXCEL_HEADER, reader.getColumnNames())) { throw new StatusException("Excel表头错误"); } if (CollectionUtils.isEmpty(lineList)) { throw new StatusException("Excel无内容"); } ApplyTaskEntity task = getApplyTask(); Date openStartTime = new Date(task.getOpenApplyStartTime()); Date now = new Date(); List> failRecords = new ArrayList>(); Map teachingCache = getTeachingCache(level); Map agentCache = getAgentCache(); Map timeCache = getTimePeriodCache(); Map examSiteCategoryCache = getExamSiteCategoryCache(); List applyList = new ArrayList<>(); AgentAndTimeVO agentTime; for (int i = 0; i < lineList.size(); i++) { List agentTimeList = new ArrayList<>(); DataMap line = lineList.get(i); StudentImportVO apply = new StudentImportVO(); StringBuilder msg = new StringBuilder(); String studentCode = trimAndNullIfBlank(line.get(EXCEL_HEADER[0])); if (StringUtils.isBlank(studentCode)) { msg.append(" 学号不能为空"); } String name = trimAndNullIfBlank(line.get(EXCEL_HEADER[1])); if (StringUtils.isBlank(name)) { msg.append(" 姓名不能为空"); } String identityNumber = trimAndNullIfBlank(line.get(EXCEL_HEADER[2])); if (StringUtils.isBlank(identityNumber)) { msg.append(" 证件号不能为空"); } StudentEntity student = null; try { student = checkStd(studentCode, name, identityNumber, task); apply.setStudentId(student.getId()); apply.setApplyNumber(student.getApplyNumber()); } catch (StatusException e) { msg.append(" " + e.getMessage()); failRecords.add(newError(i + 1, msg.toString())); continue; } String teachingName = trimAndNullIfBlank(line.get(EXCEL_HEADER[3])); if (StringUtils.isBlank(teachingName)) { msg.append(" 所属教学点不能为空"); } Long categoryId = teachingCache.get(teachingName); if (categoryId == null) { msg.append(" 所属教学点不存在"); } if (categoryId != null && !student.getCategoryId().equals(categoryId)) { msg.append(" 导入的考生所属教学点和系统中的考生教学点不匹配"); } if (categoryId != null && !categoryId.equals(teachingId)) { msg.append(" 不是本教学点的考生"); } String agentName1 = trimAndNullIfBlank(line.get(EXCEL_HEADER[4])); if (StringUtils.isBlank(agentName1)) { msg.append(" 预约考点1不能为空"); } agentTime = new AgentAndTimeVO(); Long agentId = agentCache.get(agentName1); Long applyCategoryId = null; if (agentId == null) { msg.append(" 预约考点1不存在"); } else { applyCategoryId = examSiteCategoryCache.get(agentId); if (now.before(openStartTime) && categoryId != null && !applyCategoryId.equals(categoryId)) { msg.append(" 未到自由预约时间,不允许预约其他教学点的考点"); } } String timePeriod1 = trimAndNullIfBlank(line.get(EXCEL_HEADER[5])); if (StringUtils.isBlank(timePeriod1)) { msg.append(" 预约时段1不能为空"); } TimePeriodEntity timePeriod = null; try { timePeriod = checkTimePeriod(timePeriod1, timeCache); agentTime.setAgentId(agentId); agentTime.setTimePeriodId(timePeriod.getId()); agentTime.setStartTime(timePeriod.getStartTime()); agentTimeList.add(agentTime); } catch (StatusException e) { msg.append(" " + e.getMessage()); } String agentName2 = trimAndNullIfBlank(line.get(EXCEL_HEADER[6])); String timePeriod2 = trimAndNullIfBlank(line.get(EXCEL_HEADER[7])); if (StringUtils.isBlank(agentName2) && StringUtils.isBlank(timePeriod2)) { apply.setAgentTimeList(agentTimeList); applyList.add(apply); if (msg.length() > 0) failRecords.add(newError(i + 1, msg.toString())); continue; } else { agentId = agentCache.get(agentName2); if (agentId == null) { msg.append(" 预约考点2不存在"); } else { applyCategoryId = examSiteCategoryCache.get(agentId); if (now.before(openStartTime) && categoryId != null && !applyCategoryId.equals(categoryId)) { msg.append(" 未到自由预约时间,不允许预约其他教学点的考点"); } } try { timePeriod = checkTimePeriod(timePeriod2, timeCache); agentTime = new AgentAndTimeVO(); agentTime.setAgentId(agentId); agentTime.setTimePeriodId(timePeriod.getId()); agentTime.setStartTime(timePeriod.getStartTime()); agentTimeList.add(agentTime); } catch (StatusException e) { msg.append(" " + e.getMessage()); } } String agentName3 = trimAndNullIfBlank(line.get(EXCEL_HEADER[8])); String timePeriod3 = trimAndNullIfBlank(line.get(EXCEL_HEADER[9])); if (StringUtils.isBlank(agentName3) && StringUtils.isBlank(timePeriod3)) { apply.setAgentTimeList(agentTimeList); applyList.add(apply); if (msg.length() > 0) failRecords.add(newError(i + 1, msg.toString())); continue; } else { agentId = agentCache.get(agentName3); if (agentId == null) { msg.append(" 预约考点3不存在"); } else { applyCategoryId = examSiteCategoryCache.get(agentId); if (now.before(openStartTime) && categoryId != null && !applyCategoryId.equals(categoryId)) { msg.append(" 未到自由预约时间,不允许预约其他教学点的考点"); } } try { timePeriod = checkTimePeriod(timePeriod3, timeCache); agentTime = new AgentAndTimeVO(); agentTime.setAgentId(agentId); agentTime.setTimePeriodId(timePeriod.getId()); agentTime.setStartTime(timePeriod.getStartTime()); agentTimeList.add(agentTime); } catch (StatusException e) { msg.append(" " + e.getMessage()); } } String agentName4 = trimAndNullIfBlank(line.get(EXCEL_HEADER[10])); String timePeriod4 = trimAndNullIfBlank(line.get(EXCEL_HEADER[11])); if (StringUtils.isBlank(agentName4) && StringUtils.isBlank(timePeriod4)) { apply.setAgentTimeList(agentTimeList); applyList.add(apply); if (msg.length() > 0) failRecords.add(newError(i + 1, msg.toString())); continue; } else { agentId = agentCache.get(agentName4); if (agentId == null) { msg.append(" 预约考点4不存在"); } else { applyCategoryId = examSiteCategoryCache.get(agentId); if (now.before(openStartTime) && categoryId != null && !applyCategoryId.equals(categoryId)) { msg.append(" 未到自由预约时间,不允许预约其他教学点的考点"); } } try { timePeriod = checkTimePeriod(timePeriod4, timeCache); agentTime = new AgentAndTimeVO(); agentTime.setAgentId(agentId); agentTime.setTimePeriodId(timePeriod.getId()); agentTime.setStartTime(timePeriod.getStartTime()); agentTimeList.add(agentTime); apply.setAgentTimeList(agentTimeList); applyList.add(apply); } catch (StatusException e) { msg.append(" " + e.getMessage()); } } } checkStudentApplyTime(applyList, getApplyTask().getAllowApplyCancelDays(), failRecords); if (CollectionUtils.isNotEmpty(failRecords)) { // TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return failRecords; } checkAvailableTimePeriod(applyList); for (int i = 0; i < applyList.size(); i++) { StudentImportVO vo = applyList.get(i); try { saveStdApply(i, vo, userId, failRecords); } catch (StatusException e) { failRecords.add(newError(i + 1, " 系统异常")); log.error("导入异常", e); } } if (CollectionUtils.isNotEmpty(failRecords)) { // TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return failRecords; } return failRecords; } private Map getExamSiteCategoryCache() { Map cache = new HashMap<>(); LambdaQueryWrapper lm = new LambdaQueryWrapper<>(); lm.eq(ExamSiteEntity::getEnable, Boolean.TRUE); List examSiteList = examSiteService.list(lm); examSiteList.forEach(site -> { cache.put(site.getId(), site.getCategoryId()); }); return cache; } private void checkAvailableTimePeriod(List applyList) { for (StudentImportVO vo : applyList) { List agentTimeList = vo.getAgentTimeList(); List haveApplyList = listStudentApply(vo.getStudentId(), Boolean.FALSE); int studentApplyFinishCount = cacheService.getStudentApplyFinishCount(vo.getStudentId()); if (studentApplyFinishCount == 0) { studentApplyFinishCount = haveApplyList.size(); } agentTimeList.sort(Comparator.comparing(AgentAndTimeVO::getStartTime)); List tobeInsertTimeList = new ArrayList<>(); // 考生已经完成预约-不做处理 if (studentApplyFinishCount >= vo.getApplyNumber()) { vo.setAgentTimeList(tobeInsertTimeList); continue; } // 只预约了部分 if (studentApplyFinishCount > 0 && studentApplyFinishCount < vo.getApplyNumber()) { List availableList = listAvailableTime(haveApplyList, agentTimeList); // 需要填充的次数 tobeInsertTimeList.addAll(availableList); } // 未预约 if (studentApplyFinishCount == 0) { tobeInsertTimeList = agentTimeList; } vo.setAgentTimeList(tobeInsertTimeList); } } private List listAvailableTime(List haveApplyList, List agentTimeList) { List availableList = new ArrayList<>(); for (AgentAndTimeVO time : agentTimeList) { boolean flag = false; for (StudentApplyEntity apply : haveApplyList) { if (time.getTimePeriodId().equals(apply.getTimePeriodId())) { flag = true; break; } } if (!flag) { availableList.add(time); } } return availableList; } private void checkStudentApplyTime(List applyList, Integer days, List> failRecords) { for (int i = 0; i < applyList.size(); i++) { StudentImportVO vo = applyList.get(i); List agentTimeList = vo.getAgentTimeList(); if (agentTimeList.size() > vo.getApplyNumber()) { failRecords.add(newError(i + 1, " 导入考生预约数量大于考生可预约的数量")); } boolean isDuplicate = agentTimeList.stream() .collect(Collectors.groupingBy(AgentAndTimeVO::getTimePeriodId, Collectors.counting())).entrySet() .stream().anyMatch(entry -> entry.getValue() > 1); if (isDuplicate) { failRecords.add(newError(i + 1, " 预约的时间不能相同")); } LocalDate day = LocalDate.now().plusDays(days); long epochMilli = day.plusDays(1).atStartOfDay(ZoneId.systemDefault()).minusSeconds(1).toInstant() .toEpochMilli(); for (AgentAndTimeVO time : agentTimeList) { if (time.getStartTime() <= epochMilli) { failRecords.add(newError(i + 1, " 预约的考试时间,必须在可取消的时间之后")); } } } } private ApplyTaskEntity getApplyTask() { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(ApplyTaskEntity::getEnable, Boolean.TRUE); ApplyTaskEntity task = applyTaskService.getOne(wrapper); if (task == null) throw new StatusException("未开启预约任务"); return task; } private List saveStdApply(int row, StudentImportVO vo, Long userId, List> failRecords) { List ApplyList = new ArrayList<>(); List agentTimeList = vo.getAgentTimeList(); String studentApplyLockKey = String.format(CacheConstants.LOCK_STUDENT_APPLY, vo.getStudentId()); RLock studentApplyLock = (RLock) concurrentService.getLock(studentApplyLockKey); try { if (!studentApplyLock.tryLock()) { log.warn("获取锁失败,考生在同时操作预约!lockKey:{}", studentApplyLockKey); } else { log.warn("获取锁成功!lockKey:{}", studentApplyLockKey); for (AgentAndTimeVO agentTime : agentTimeList) { // 已经预约的容量和考点的容量从redis中获取 int haveApplyCount = cacheService.getApplyFinishCount(agentTime.getAgentId(), agentTime.getTimePeriodId()); int examSiteCount = cacheService.getApplyTotalCount(agentTime.getAgentId()); if (haveApplyCount >= examSiteCount) { ExamSiteEntity examSite = examSiteService.getById(agentTime.getAgentId()); failRecords.add(newError(row + 1, " 考点【" + examSite.getName() + "】的容量已满")); } else { StudentApplyEntity entity = new StudentApplyEntity(); entity.setStudentId(vo.getStudentId()); entity.setExamSiteId(agentTime.getAgentId()); entity.setTimePeriodId(agentTime.getTimePeriodId()); entity.setCancel(Boolean.FALSE); entity.setOperateId(userId); StudentApplyEntity existStudentApply = findStudentApply(entity); if (existStudentApply != null) { existStudentApply.setCancel(Boolean.FALSE); baseMapper.updateById(existStudentApply); } else { baseMapper.insert(entity); } ApplyRecordCacheBean bean = new ApplyRecordCacheBean(); bean.setStudentId(vo.getStudentId()); bean.setExamSiteId(agentTime.getAgentId()); bean.setTimePeriodId(agentTime.getTimePeriodId()); bean.setCancel(Boolean.FALSE); bean.setOperateId(userId); bean.setOperateTime(System.currentTimeMillis()); // 先推送至预约队列 cacheService.pushStudentApplyRecordQueue(bean); cacheService.saveStudentApplyRecord(bean); cacheService.increaseApplyFinishCount(agentTime.getAgentId(), agentTime.getTimePeriodId()); } } } } catch (Exception e) { log.error("导入预考失败,错误原因:", e.getMessage()); throw new StatusException("导入预考失败,请稍后再试!", e); } finally { try { if (studentApplyLock.isLocked() && studentApplyLock.isHeldByCurrentThread()) { studentApplyLock.unlock(); log.info("解锁成功!lockKey:{}", studentApplyLockKey); } } catch (Exception e) { log.warn(e.getMessage()); } } return ApplyList; } private TimePeriodEntity checkTimePeriod(String timePeriod, Map timeCache) { if (timePeriod == null || timePeriod.split(" ").length != 2) { throw new StatusException(" 预约时段格式不正确"); } String[] arr = timePeriod.split("-"); String startTime = arr[0] + ":00"; String endTime = startTime.substring(0, startTime.indexOf("日") + 1) + " " + arr[1] + ":00"; Long startTimeLong = DateUtil.getLongTimeByZHDate(startTime); Long endTimeLong = DateUtil.getLongTimeByZHDate(endTime); if (timeCache.get(startTimeLong + "-" + endTimeLong) == null) { throw new StatusException(" 预约时段不存在"); } return timeCache.get(startTimeLong + "-" + endTimeLong); } private Map getTimePeriodCache() { Map map = new HashMap<>(); LambdaQueryWrapper lm = new LambdaQueryWrapper<>(); lm.eq(TimePeriodEntity::getApplyTaskId, getApplyTask().getId()); List timeList = timePeriodService.list(lm); for (TimePeriodEntity time : timeList) { map.put(time.getStartTime() + "-" + time.getEndTime(), time); } return map; } private Map getTeachingCache(Integer level) { LambdaQueryWrapper lm = new LambdaQueryWrapper<>(); lm.eq(CategoryEntity::getEnable, Boolean.TRUE); lm.eq(CategoryEntity::getLevel, level == null ? CategoryLevel.TEACHING.getValue() : level); List categoryList = categoryService.list(lm); return categoryList.stream().collect(Collectors.toMap(CategoryEntity::getName, CategoryEntity::getId)); } private Map getAgentCache() { LambdaQueryWrapper lm = new LambdaQueryWrapper<>(); lm.eq(ExamSiteEntity::getEnable, Boolean.TRUE); List categoryList = examSiteService.list(lm); return categoryList.stream().collect(Collectors.toMap(ExamSiteEntity::getName, ExamSiteEntity::getId)); } private StudentEntity checkStd(String studentCode, String name, String identityNumber, ApplyTaskEntity task) { LambdaQueryWrapper lm = new LambdaQueryWrapper<>(); lm.eq(StudentEntity::getStudentCode, studentCode); lm.eq(StudentEntity::getName, name); lm.eq(StudentEntity::getIdentityNumber, identityNumber); lm.eq(StudentEntity::getApplyTaskId, task.getId()); StudentEntity student = studentService.getOne(lm); if (student == null) { throw new StatusException(" 考生信息填写错误"); } return student; } private Map newError(int lineNum, String msg) { Map map = new HashMap<>(); map.put("lineNum", lineNum); map.put("msg", msg); return map; } private String trimAndNullIfBlank(String s) { if (StringUtils.isBlank(s)) { return null; } return s.trim(); } @Override public void autoAssign(Long taskId, Long userId) { checkAfterOpenTime(); RLock lock = (RLock) concurrentService.getLock(CacheConstants.LOCK_AUTO_APPLY); try { if (!lock.tryLock()) { log.warn("获取锁失败,不允许同时执行自动分配!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY); throw new StatusException("其他老师正在执行自动分配,请不要重复执行!"); } log.warn("获取锁成功!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY); // 1、未完成预约的考生 List studentList = studentService.listNoFinishStudent(taskId, Boolean.FALSE); Map> noFinishApplyMap = studentList.stream() .collect(Collectors.groupingBy(StudentEntity::getCategoryId)); // 2、考位是否充足 List timeList = listTimePeriod(taskId); timeList = listNoCancelExamTimePeriod(timeList, taskId); checkTeachingCapacity(noFinishApplyMap, timeList, taskId); // 3、按照教学点安排考位。规则:不能和已预约的时间上有冲突 for (Long key : noFinishApplyMap.keySet()) { List siteList = listExamSite(key, null); List teachingStudentList = noFinishApplyMap.get(key); for (ExamSiteEntity site : siteList) { for (TimePeriodEntity time : timeList) { // 该时段已预约的考生 Integer haveApplyNum = cacheService.getApplyFinishCount(site.getId(), time.getId()); if (haveApplyNum == 0) { haveApplyNum = getHaveApplyNum(site.getId(), time.getId()); } // 剩余的考位 Integer remainNum = site.getCapacity() - haveApplyNum; assignStudentApply(userId, site.getId(), time.getId(), teachingStudentList, remainNum); } } // 4、判断是否还有剩余考生未完成预约,提醒考位不够 if (!teachingStudentList.isEmpty()) throw new StatusException("【" + categoryService.getById(key).getName() + "】教学点考位不足"); } } catch (Exception e) { log.error(e.getMessage()); throw new StatusException(e.getMessage()); } finally { try { // 解锁前检查当前线程是否持有该锁 if (lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); log.info("解锁成功!lockKey:{}", CacheConstants.LOCK_AUTO_APPLY); } } catch (Exception e) { log.warn(e.getMessage()); } } } private List listNoCancelExamTimePeriod(List timeList, Long taskId) { ApplyTaskEntity task = applyTaskService.getById(taskId); Long longToday = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(new Date()) + " 00:00:00"); Date today = new Date(longToday); Date otherDay = DateUtil.addValues(today, Calendar.DAY_OF_MONTH, task.getAllowApplyCancelDays()); Long longOtherDay = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(otherDay) + " 23:59:59"); return timeList.stream().filter(time -> time.getStartTime() > longOtherDay).collect(Collectors.toList()); } private List assignStudentApply(Long userId, Long siteId, Long timeId, List teachingStudentList, Integer remainNum) { List insertApplyList = new ArrayList<>(); int num = 0; String studentApplyLockKey = null; RLock studentApplyLock = null; for (Iterator iterator = teachingStudentList.iterator(); iterator.hasNext();) { StudentEntity student = iterator.next(); if (num >= remainNum) break; studentApplyLockKey = String.format(CacheConstants.LOCK_STUDENT_APPLY, student.getId()); studentApplyLock = (RLock) concurrentService.getLock(studentApplyLockKey); try { if (!studentApplyLock.tryLock()) { log.warn("获取锁失败,考生在同时操作预约!lockKey:{}", studentApplyLockKey); iterator.remove(); } else { log.warn("获取锁成功!lockKey:{}", studentApplyLockKey); List studentApplyList = listStudentApply(student.getId(), Boolean.FALSE); int toApplyNum = student.getApplyNumber() - studentApplyList.size(); if (toApplyNum > 0 && !haveApplySameTimePeriod(siteId, timeId, student.getId())) { StudentApplyEntity studentApply = new StudentApplyEntity(); studentApply.setStudentId(student.getId()); studentApply.setExamSiteId(siteId); studentApply.setCancel(Boolean.FALSE); studentApply.setTimePeriodId(timeId); studentApply.setOperateId(userId); StudentApplyEntity existStudentApply = findStudentApply(studentApply); if (existStudentApply != null) { existStudentApply.setCancel(Boolean.FALSE); baseMapper.updateById(existStudentApply); } else { baseMapper.insert(studentApply); } insertApplyList.add(studentApply); num++; if (student.getApplyNumber() - (studentApplyList.size() + 1) == 0) { iterator.remove(); } ApplyRecordCacheBean bean = new ApplyRecordCacheBean(); bean.setStudentId(studentApply.getStudentId()); bean.setExamSiteId(studentApply.getExamSiteId()); bean.setTimePeriodId(studentApply.getTimePeriodId()); bean.setCancel(Boolean.FALSE); bean.setOperateId(userId); bean.setOperateTime(System.currentTimeMillis()); // 先推送至预约队列 cacheService.pushStudentApplyRecordQueue(bean); cacheService.saveStudentApplyRecord(bean); cacheService.increaseApplyFinishCount(studentApply.getExamSiteId(), studentApply.getTimePeriodId()); } } } catch (Exception e) { log.error("自动安排预约失败,错误信息:{}", e.getMessage()); throw new StatusException("自动安排预约失败,请稍后再试!"); } finally { try { // 解锁前检查当前线程是否持有该锁 if (studentApplyLock.isLocked() && studentApplyLock.isHeldByCurrentThread()) { studentApplyLock.unlock(); log.info("解锁成功!lockKey:{}", studentApplyLockKey); } } catch (Exception e) { log.warn(e.getMessage()); } } } return insertApplyList; } private StudentApplyEntity findStudentApply(StudentApplyEntity studentApply) { LambdaQueryWrapper lm = new LambdaQueryWrapper<>(); lm.eq(StudentApplyEntity::getExamSiteId, studentApply.getExamSiteId()); lm.eq(StudentApplyEntity::getTimePeriodId, studentApply.getTimePeriodId()); lm.eq(StudentApplyEntity::getStudentId, studentApply.getStudentId()); return baseMapper.selectOne(lm); } private boolean haveApplySameTimePeriod(Long siteId, Long timeId, Long studentId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StudentApplyEntity::getExamSiteId, siteId); wrapper.eq(StudentApplyEntity::getTimePeriodId, timeId); wrapper.eq(StudentApplyEntity::getStudentId, studentId); wrapper.eq(StudentApplyEntity::getCancel, Boolean.FALSE); StudentApplyEntity studentApply = baseMapper.selectOne(wrapper); return studentApply != null; } private Integer getHaveApplyNum(Long siteId, Long timeId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StudentApplyEntity::getExamSiteId, siteId); wrapper.eq(StudentApplyEntity::getTimePeriodId, timeId); wrapper.eq(StudentApplyEntity::getCancel, Boolean.FALSE); return baseMapper.selectCount(wrapper); } private void checkAfterOpenTime() { ApplyTaskEntity task = getApplyTask(); Date selfEndTime = new Date(task.getSelfApplyEndTime()); Date openStartTime = new Date(task.getOpenApplyStartTime()); if (!DateUtil.isBetwwen(selfEndTime, openStartTime)) { throw new StatusException("自动分配,时间必须要在第一阶段结束之后,第三阶段开始之前"); } } private void checkTeachingCapacity(Map> map, List timeList, Long taskId) { for (Long key : map.keySet()) { List siteList = listExamSite(key, null); // 总考位数量 Integer total = siteList.stream().collect(Collectors.summingInt(ExamSiteEntity::getCapacity)) * timeList.size(); // 已经预约的数量 Integer haveApplyNum = 0; for (ExamSiteEntity site : siteList) { haveApplyNum += cacheService.getApplyTotalCount(site.getId()); } if (haveApplyNum == 0) { haveApplyNum = getBaseMapper().getHaveApplyCount( siteList.stream().map(site -> site.getId()).collect(Collectors.toList()), Boolean.FALSE); } // 未预约的数量 Integer noApplyNum = getNoApplyNum(map.get(key)); if (noApplyNum > total - haveApplyNum) { CategoryEntity category = categoryService.getById(key); throw new StatusException("【" + category.getName() + "】教学点考位不足!剩余的考位数量:【" + (total - haveApplyNum) + "】,实际需要的考位数量:【" + noApplyNum + "】"); } } } private Integer getNoApplyNum(List list) { int noApplyNum = 0; for (StudentEntity student : list) { if (student.getApplyNumber() == 1) { noApplyNum++; } else if (student.getApplyNumber() > 1) { // listStudentApply(student.getId(), Boolean.FALSE).size() int haveApplyNum = cacheService.getStudentApplyFinishCount(student.getId()); noApplyNum = noApplyNum + (student.getApplyNumber() - haveApplyNum); } } return noApplyNum; } private List listStudentApply(Long stdId, Boolean cancel) { LambdaQueryWrapper lm = new LambdaQueryWrapper<>(); lm.eq(StudentApplyEntity::getStudentId, stdId); lm.eq(StudentApplyEntity::getCancel, cancel); return this.baseMapper.selectList(lm); } private List listTimePeriod(Long taskId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(TimePeriodEntity::getApplyTaskId, taskId); wrapper.orderByAsc(TimePeriodEntity::getStartTime); List timeList = timePeriodService.list(wrapper); if (timeList.isEmpty()) { throw new StatusException("考试时段未设置"); } return timeList; } private List listExamSite(Long categoryId, Long examSiteId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ExamSiteEntity::getCategoryId, categoryId); wrapper.eq(examSiteId != null, ExamSiteEntity::getId, examSiteId); wrapper.eq(ExamSiteEntity::getEnable, Boolean.TRUE); return examSiteService.list(wrapper); } @Transactional @Override public void autoLayout(Long teachingId) { ApplyTaskEntity applyTask = getApplyTask(); String autoLayoutLockKey = String.format(CacheConstants.LOCK_ARRANGE_EXAM, DateUtil.formatShortDateString(new Date())); RLock autoLayoutLock = (RLock) concurrentService.getLock(autoLayoutLockKey); try { if (!autoLayoutLock.tryLock()) { log.warn("获取锁失败,已有线程在执行排考!lockKey:{}", autoLayoutLock); return; } log.warn("获取锁成功!lockKey:{}", autoLayoutLockKey); // 1.根据当前日期,查询不能取消的时段 List timePeriodList = listTimePeriod(applyTask.getId()); List noCancelTimePeroidList = listNoCancelApplyTimePeriod(timePeriodList, applyTask.getAllowApplyCancelDays()); if (noCancelTimePeroidList.isEmpty()) { log.warn("当前时间不在取消预约范围内"); return; } // 2.查询考试日期的待排考的考生 List toBeLayoutStudentList = this.baseMapper.listTimePeriod( noCancelTimePeroidList.stream().map(item -> item.getId()).collect(Collectors.toList()), Boolean.FALSE); if (toBeLayoutStudentList == null || toBeLayoutStudentList.isEmpty()) { log.warn("没有待排考的考生"); return; } // 3.开始排考 Map> toBeLayoutStudentMap = toBeLayoutStudentList.stream() .collect(Collectors.groupingBy(StudentApplyEntity::getExamSiteId)); for (Long examSiteId : toBeLayoutStudentMap.keySet()) { Map> timeLayoutStudentMap = toBeLayoutStudentMap.get(examSiteId).stream() .collect(Collectors.groupingBy(StudentApplyEntity::getTimePeriodId)); List roomList = listExamRoom(examSiteId); if (roomList.isEmpty()) { log.warn("{}:未设置考场", examSiteId); return; } ExamSiteEntity examSite = examSiteService.getById(examSiteId); layoutStudentByTimePeriod(applyTask.getId(), examSite, roomList, timeLayoutStudentMap); } } catch (StatusException e) { log.error(e.getMessage()); e.printStackTrace(); } finally { try { // 解锁前检查当前线程是否持有该锁 if (autoLayoutLock.isLocked() && autoLayoutLock.isHeldByCurrentThread()) { autoLayoutLock.unlock(); log.info("解锁成功!lockKey:{}", autoLayoutLockKey); } } catch (Exception e) { log.warn(e.getMessage()); } } } private void layoutStudentByTimePeriod(Long taskId, ExamSiteEntity examSite, List roomList, Map> timeLayoutStudentMap) { for (Long timePeriodId : timeLayoutStudentMap.keySet()) { List studentApplyList = timeLayoutStudentMap.get(timePeriodId); layoutStudentToRoom(taskId, examSite, roomList, studentApplyList, timePeriodService.getById(timePeriodId)); } } private void layoutStudentToRoom(Long taskId, ExamSiteEntity examSite, List roomList, List studentApplyList, TimePeriodEntity timePeriod) { Integer timePeriodOrder = getTimePeriodOrder(taskId, timePeriod); for (ExamRoomEntity room : roomList) { int num = 0; for (Iterator iterator = studentApplyList.iterator(); iterator.hasNext();) { StudentApplyEntity student = iterator.next(); if (num >= room.getCapacity()) break; String seatNumber = StringUtils.leftPad(String.valueOf(++num), 3, '0'); student.setExamRoomId(room.getId()); student.setSeatNumber(seatNumber); student.setTicketNumber( generateTicketNumber(timePeriodOrder, examSite.getCode(), room.getCode(), seatNumber)); this.baseMapper.updateById(student); iterator.remove(); } } } private Integer getTimePeriodOrder(Long taskId, TimePeriodEntity timePeriod) { List timeList = listTimePeriod(taskId); List sameDayTimeList = listSameDayTimePeriod(timeList, timePeriod.getStartTime()); for (int i = 0; i < sameDayTimeList.size(); i++) { TimePeriodEntity time = sameDayTimeList.get(i); if (time.getStartTime().equals(timePeriod.getStartTime())) return i + 1; } return 0; } private List listSameDayTimePeriod(List timeList, Long startTime) { Date day = new Date(startTime); List resultList = new ArrayList<>(); for (TimePeriodEntity time : timeList) { if (DateUtils.isSameDay(day, new Date(time.getStartTime()))) resultList.add(time); } return resultList.stream().sorted(Comparator.comparing(TimePeriodEntity::getStartTime)) .collect(Collectors.toList()); } private String generateTicketNumber(Integer timePeriodOrder, String examSiteCode, String roomCode, String seatNumber) { return DateUtil.formatShortDateString(new Date()) + timePeriodOrder + examSiteCode + roomCode + seatNumber; } public List listExamRoom(Long examSiteId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ExamRoomEntity::getExamSiteId, examSiteId); wrapper.orderByAsc(ExamRoomEntity::getCode); return examRoomService.list(wrapper); } private List listNoCancelApplyTimePeriod(List list, Integer allowApplyCancelDays) { Date noCancelDate = getNoCancelApplyDate(allowApplyCancelDays); List noCancelTimePeroidList = new ArrayList<>(); for (TimePeriodEntity time : list) { if (DateUtils.isSameDay(noCancelDate, new Date(time.getStartTime()))) noCancelTimePeroidList.add(time); } return noCancelTimePeroidList; } private Date getNoCancelApplyDate(Integer allowApplyCancelDays) { return DateUtil.addValues(Calendar.DAY_OF_MONTH, allowApplyCancelDays); } @Override public File downloadSignIn(Long teachingId, Long agentId, Long examDate) { ApplyTaskEntity applyTask = getApplyTask(); List timePeriodList = listTimePeriod(applyTask.getId()); List noCancelTimePeroidList = listSameDayTimePeriod(timePeriodList, examDate); if (noCancelTimePeroidList.isEmpty()) { throw new StatusException("当前时间没有可下载的签到表"); } List siteList = listExamSite(teachingId, agentId); if (siteList.isEmpty()) throw new StatusException("当前教学点下没有可用的考点"); CategoryEntity category = categoryService.getById(teachingId); File tempFolder = new File("temp"); if (!tempFolder.exists()) { tempFolder.mkdirs(); } ZipWriter writer = null; File zipFile = new File(tempFolder, category.getName() + "签到表.zip"); try { writer = ZipWriter.create(zipFile); List fileList = new ArrayList<>(); for (ExamSiteEntity site : siteList) { fileList.addAll(downloadByExamSite(writer, applyTask.getName(), site, noCancelTimePeroidList)); } if (fileList.isEmpty()) { throw new StatusException("暂未排考,请等待排考后下载"); } } catch (IOException e) { e.printStackTrace(); throw new StatusException("文件写入异常"); } writer.close(); return zipFile; } public List downloadByExamSite(ZipWriter writer, String taskName, ExamSiteEntity site, List timeList) throws IOException { List fileList = new ArrayList<>(); List roomList = listExamRoom(site.getId()); MaterialTitleInfo title = new MaterialTitleInfo(); title.setTaskName(taskName); title.setSiteName(site.getName()); for (TimePeriodEntity time : timeList) { title.setTimePeriod(DateUtil.getStartAndEndTime(time.getStartTime(), time.getEndTime())); for (ExamRoomEntity room : roomList) { title.setAddress(room.getAddress()); title.setRoomCode(room.getCode()); List studentList = baseMapper.listStudentApply(time.getId(), room.getId()); if (!studentList.isEmpty()) { File file = materialService.generateSignInForm(title, studentList); fileList.add(file); writer.write(file, title.getSiteName(), title.getTimePeriod() + " 第【" + title.getRoomCode() + "】考场" + ".pdf"); } } } return fileList; } /** * 获取某考点某时段的“已预约数量” */ @Override public int countApplyFinishForExamSiteAndTimePeriod(Long examSiteId, Long timePeriodId) { Integer value = baseMapper.countApplyFinishForExamSiteAndTimePeriod(examSiteId, timePeriodId); return value != null ? value : 0; } @Override public List listSignInDate(Long taskId) { List signInList = new ArrayList<>(); ApplyTaskEntity task = null; if (taskId != null) { task = applyTaskService.getById(taskId); } else { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(ApplyTaskEntity::getEnable, Boolean.TRUE); task = applyTaskService.getOne(wrapper); } if (task == null) { log.warn("当前没有开启的任务"); return signInList; } List timePeriodList = listTimePeriod(task.getId()); if (timePeriodList.isEmpty()) { log.warn("未配置考试时段"); return signInList; } Long startTime = timePeriodList.get(0).getStartTime(); Long endTime = timePeriodList.get(timePeriodList.size() - 1).getEndTime(); String todayStr = DateUtil.formatShortSplitDateString(new Date()); Long longToday = DateUtil.getLongTimeByDate(todayStr + " 00:00:00"); Date today = new Date(longToday); if (longToday >= startTime && longToday <= endTime) { SignInVO vo = new SignInVO(); vo.setExamDate(longToday); signInList.add(vo); } for (int i = 1; i <= task.getAllowApplyCancelDays(); i++) { Date otherDay = DateUtil.addValues(today, Calendar.DAY_OF_MONTH, i); Long longOtherDay = DateUtil.getLongTimeByDate(DateUtil.formatShortSplitDateString(otherDay) + " 00:00:00"); if (longOtherDay >= startTime && longOtherDay <= endTime) { SignInVO vo = new SignInVO(); vo.setExamDate(longOtherDay); signInList.add(vo); } } signInList.sort(Comparator.comparing(SignInVO::getExamDate)); return signInList; } }