package com.ruoyi.sim.service.impl; import java.util.ArrayList; import java.util.List; import java.util.Objects; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.utils.DateUtils; import com.ruoyi.sim.config.SimConfig; import com.ruoyi.sim.config.SimDebugConfig; import com.ruoyi.sim.domain.*; import com.ruoyi.sim.domain.vo.RealExamVo; import com.ruoyi.sim.domain.vo.StudentRealExamIngVo; import com.ruoyi.sim.domain.vo.StudentRealExamPostVo; import com.ruoyi.sim.domain.vo.StudentRealExamPreVo; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import com.ruoyi.sim.mapper.RealExamMapper; import org.springframework.transaction.annotation.Transactional; /** * 考试Service业务层处理 * * @author tom * @date 2024-12-15 */ @Service public class RealExamService { @Autowired private RealExamMapper realExamMapper; /** * 查询考试 * * @param examId 考试主键 * @return 考试 */ public RealExam selectRealExamByExamId(Long examId) { return realExamMapper.selectRealExamByExamId(examId); } /** * 查询考试列表 * * @param realExam 考试 * @return 考试 */ public List selectRealExamList(RealExam realExam) { return realExamMapper.selectRealExamList(realExam); } /** * 新增考试 * * @param realExam 考试 * @return 结果 */ public int insertRealExam(RealExam realExam) { realExam.setCreateTime(DateUtils.getNowDate()); return realExamMapper.insertRealExam(realExam); } /** * 修改考试 * * @param realExam 考试 * @return 结果 */ public int updateRealExam(RealExam realExam) { realExam.setUpdateTime(DateUtils.getNowDate()); return realExamMapper.updateRealExam(realExam); } /** * 批量删除考试 * * @param examIds 需要删除的考试主键 * @return 结果 */ public int deleteRealExamByExamIds(Long[] examIds) { return realExamMapper.deleteRealExamByExamIds(examIds); } /** * 删除考试信息 * * @param examId 考试主键 * @return 结果 */ public int deleteRealExamByExamId(Long examId) { return realExamMapper.deleteRealExamByExamId(examId); } // -------------------------------- tom add -------------------------------- private static final Logger l = LoggerFactory.getLogger(CommSendService.class); @Autowired private CommReceiveService commReceiveService; @Autowired private StudentService studentService; @Autowired private SimService simService; @Autowired private SeatService seatService; @Autowired private RealExamCollectionService realExamCollectionService; @Autowired private RealExamFaultService realExamFaultService; @Autowired @Lazy private CommSendService commSendService; @Autowired @Lazy private CommCheckService commCheckService; @Autowired private SimConfig simConfig; @Autowired private SocketService socketService; /** * examId 是否有效。 * * @param examId * @return */ public boolean exist(Long examId) { if (examId == null) { return false; } if (examId == 0) { return false; } RealExam re = selectRealExamByExamId(examId); if (re == null) { return false; } return true; } public List list(RealExam q) { List list = new ArrayList<>(); realExamMapper.selectRealExamList(q).forEach(re -> { RealExamVo v = new RealExamVo(); RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId()); v.setRealExam(re); v.setRealExamCollection(rec); list.add(v); }); return list; } public List listAllByStatus(String state) { RealExam q = new RealExam(); q.setExamStatus(state); return selectRealExamList(q); } /** * 交卷自动修改关联状态 * * @param examId * @param state * @return */ public int updateOneState(long examId, final String state) { RealExam q = selectRealExamByExamId(examId); // todo:屏蔽 if (false && RealExam.State.SUBMITTED.equals(state)) { // 关联故障list同步锁死。 realExamFaultService.listAllType2State2and3ByExamId(q.getExamId()) .forEach(ref -> { ref.setRefState(RealExamFault.State.FINISH); realExamFaultService.updateRealExamFault(ref); }); } q.setExamStatus(state); return updateRealExam(q); } /** * [学生]进入考试。 * * @return RealExam */ public AjaxResult studentEnterRealExam(Long examId) { RealExam re = selectRealExamByExamId(examId); if (re == null) { AjaxResult.error("realExamId error!"); } // todo:应该在登录位置实现 // todo: temp updateOneState(examId, RealExam.State.LOGGED_IN); // todo: temp realExamFaultService.resetAllType2(examId); return AjaxResult.success(re); } /** * [轮询][学生]准备考试界面。 * * @param realExamId * @return StudentRealExamPreVo */ public AjaxResult studentLoopPrepareRealExam(Long realExamId) { l.info("studentLoopPrepareRealExam"); // check if (realExamId == null || realExamId == 0) { // todo: } // RealExam re = selectRealExamByExamId(realExamId); if (re == null) { // todo: } // todo: 日期,不可进入考试。 // // todo: 验证学生登录身份` Objects.requireNonNull(re); RealExamCollection collection = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId()); // check collection Sim sim = simService.selectSimBySimId(re.getSimId()); // check sim Student student = studentService.selectStudentByUserId(re.getUserId()); // check student Seat seat = seatService.selectSeatBySeatId(re.getSeatId()); // check seat StudentRealExamPreVo vo = new StudentRealExamPreVo(); vo.setRealExam(re); vo.setRealExamCollection(collection); vo.setSim(sim); vo.setStudent(student); vo.setSeat(seat); // todo:多人请求同时进入的问题 boolean next = studentPrepareRealExamCheck(vo); vo.setNext(next); if (!next) { // 执行模拟器通信,让模拟器准备好。 // 异步执行 commSendService.clearListFaultByRealExamAsync(re); } l.info("vo = {}", vo); return AjaxResult.success(vo); } public boolean studentPrepareRealExamCheck(StudentRealExamPreVo v) { if (v == null || v.getRealExam() == null || v.getRealExamCollection() == null || v.getSim() == null || v.getStudent() == null || v.getSeat() == null) { return false; } // todo:在考试日期内。 // check 一个模拟器的所有选中故障点位都下发成功,准备是否可以 // // todo:?? // 学生答题中可以再次进入。 String examStatus = v.getRealExam().getExamStatus(); String simStatus = v.getSim().getSimState(); // 兜底 { if (realExamFaultService.isAllType2StateXiaFa(v.getRealExam().getExamId())) { // updateOneState(v.getRealExam().getExamId(), RealExam.State.SIM_PREPARE_OK); } } // 模拟器不通信。 if (!simConfig.isCommGlobal()) { return true; } if ((RealExam.State.SIM_PREPARE_OK.equals(examStatus) || RealExam.State.ANSWERING.equals(examStatus)) && Sim.State.ONLINE.equals(simStatus) ) { return true; } return false; } /** * [学生]开始考试、训练、练习 * * @param examId 考试Id * @param studentBindIp 考试学员IP * @param examCollectionType 考试集合类型 * @return */ public AjaxResult studentStartRealExam(final Long examId, final String studentBindIp, final String examCollectionType) { // Check:针对训练,进行特殊检查。 if (StringUtils.equals(RealExamCollection.Type.EXERCISE, examCollectionType)) { // 已经open的考试。 if (realExamCollectionService.existOpenedByType(RealExamCollection.Type.EXAM)) { return AjaxResult.error("存在打开的考试,无法开启训练!
请向教员说明情况。"); } } // Check:针对练习(自主练习),进行特殊检查。 if (StringUtils.equals(RealExamCollection.Type.SELF_EXERCISE, examCollectionType)) { // 已经open的考试。 if (realExamCollectionService.existOpenedByType(RealExamCollection.Type.EXAM)) { return AjaxResult.error("存在打开的考试,无法开启练习!
请向教员说明情况。"); } // 已经open的训练。 if (realExamCollectionService.existOpenedByType(RealExamCollection.Type.EXERCISE)) { return AjaxResult.error("存在打开的训练,无法开启练习!
请向教员说明情况。"); } } // Check:检查参数examId有效性 { AjaxResult ar = checkExamId(examId); if (ar.isError()) { return ar; } } RealExam re = selectRealExamByExamId(examId); // check:考试状态 if (StringUtils.equals(re.getExamStatus(), RealExam.State.SUBMITTED) || StringUtils.equals(re.getExamStatus(), RealExam.State.CALCULATING_SCORE) || StringUtils.equals(re.getExamStatus(), RealExam.State.GOT_REPORT)) { return AjaxResult.error("已经交卷,禁止重复开始考试!"); } RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId()); // Check:考试集合数据有效性。 if (rec == null) { return AjaxResult.error("考试集合数据异常!"); } if (!StringUtils.equals(rec.getExamCollectionState(), RealExamCollection.State.OPENED)) { return AjaxResult.error("教师端对应考试/训练尚未打开!
请向教员说明情况。"); } // Check:检查参数examCollectionType有效性 if (!StringUtils.equals(examCollectionType, rec.getExamCollectionType())) { return AjaxResult.error("考试集合类型不对应!"); } // Check:检查参数studentBindIp有效性 if (StringUtils.isBlank(studentBindIp)) { return AjaxResult.error("IP地址无效"); } Seat seat = seatService.uniqueByBindIp(studentBindIp); if (seat == null) { return AjaxResult.error("没有IP对应座次数据!"); } // Check:ping通 路由器。 { AjaxResult ar = commCheckService.checkRouterState(simConfig.getRouterIp()); if (ar.isError()) { return ar; } } // Check:ping通 学员端电脑。 if (false) { AjaxResult ar = commCheckService.checkPingStudentPcState(studentBindIp); if (ar.isError()) { return ar; } } // Check:ping通 RS485。 { AjaxResult ar = commCheckService.checkPingRs485State(seat.getSeatRs485Ip()); if (ar.isError()) { // todo:重复 // 更新SimId seatService.updateCurrentSimIdBySeatNum(seat.getSeatNum(), Seat.ID_0); // 更新SocketState seatService.updateSocketStateBySeatNum(seat.getSeatNum(), Seat.SocketState.OFFLINE); return ar; } else { // Ping通不代表在线,Socket连接建立表示在线。 } } // Check:如果有缓存Socket并且可用,使用缓存Socket,检查并建立Socket连接;否则返回对应错误。 { AjaxResult ar = socketService.openOne(seat.toSimSocketParamVo()); if (ar.isError()) { return ar; } } // Check:发送通用询问指令,询问是连接的哪种型号的哪一台模拟器;否则返回对应错误。 { AjaxResult ar = commCheckService.checkOneSeatState(seat, true); l.debug("ar = {}", ar); // 没有连接模拟器。 if (ar.get(AjaxResult.DATA_TAG) == null || !StringUtils.equals(((Sim) ar.get(AjaxResult.DATA_TAG)).getSimState(), Sim.State.ONLINE)) { return AjaxResult.error((String) ar.get(AjaxResult.MSG_TAG)); } // 其他的异常情况。 if (ar.isError()) { return ar; } } // Step:正式开始考试。锁定 座次 和 模拟器。 // Step:重新查询。已经确定simId和simState了。 { // 修改exam表对应examId的一条数据,填充并锁定seat_id和sim_id值。 // 设置上seatId和simId re = selectRealExamByExamId(examId); seat = seatService.uniqueByBindIp(studentBindIp); l.debug("seat = {}", seat); re.setSeatId(seat.getSeatId()); re.setSimId(seat.getCurrentSimId()); updateRealExam(re); } // Check: seat_id 和 current_sim_id { RealExam re0001 = selectRealExamByExamId(examId); if (re0001 == null || re0001.getSeatId() == null || re0001.getSeatId() == 0L || re0001.getSimId() == null || re0001.getSimId() == 0) { return AjaxResult.error("开始考试异常!
请刷新页面重试!"); } } // Step:查询模拟器在线状态,纯DB查询。 { AjaxResult ar = commCheckService.checkOneSimOnlineState(seat.getCurrentSimId()); if (ar.isError()) { return ar; } } Sim sim = simService.selectSimBySimId(re.getSimId()); // Check:检查模拟器类型 final String targetSimType = re.getSimType(); if (false) { AjaxResult ar = commCheckService.checkOneSimType(seat, true, targetSimType); if (ar.isError()) { return ar; } } // Step:可换件检查,读取对应一台模拟器 所有故障部位值。 // 检查模拟器所有的 真实的 故障部位 是否异常 或者 空值。特殊的故障部位要单独判断。 if (SimDebugConfig.CHECK_REPLACE_EMPTY) { AjaxResult ar = commSendService.readOneSimAllFaultCheck(seat, sim); if (ar.isError()) { return ar; } } // Step:清除对应一台模拟器 所有 真实的 故障部位故障。 { commSendService.clearOneSimAllFaultByExam(re); } // Step:下发对应一台模拟器 出题选中的 故障位置故障。 { commSendService.writeOneSimAllSelectFaultByExam(re); } // Step:读取对应一台模拟器 所有的 真实的 故障部位 电阻值代表值 作为出题值。 { commSendService.readOneSimAllFaultFirstTimeByExam(re); } // Step:修改当前exam_id的考试状态。 // 修改关联状态 if (realExamFaultService.isType2ExamPrepareStartOk(re.getExamId())) { updateOneState(re.getExamId(), RealExam.State.SIM_PREPARE_OK); updateOneState(re.getExamId(), RealExam.State.ANSWERING); // 修改真实考试开始时间。 re.setStartTime(DateUtils.getNowDate()); updateRealExam(re); return AjaxResult.success("开始考试成功!"); } else { return AjaxResult.error("开始考试失败,
请重新尝试开始考试!"); } } public AjaxResult studentRefreshSimState(final String studentBindIp) { Seat seat = seatService.uniqueByBindIp(studentBindIp); // Check:Seat有效性。 { if (seat == null) { return AjaxResult.error("没有IP对应座次数据!"); } } return commCheckService.checkOneSeatState(seat, true); } public AjaxResult checkExamId(final Long examId) { // Check:检查 examId 是否正确存在 if (!exist(examId)) { return AjaxResult.error("对应考试Id不存在!"); } else { return AjaxResult.success(); } } /** * [轮询][学生]正在考试界面。 * * @param realExamId * @return StudentRealExamIngVo */ public AjaxResult studentLoopAnsweringRealExam(Long realExamId) { RealExam re = selectRealExamByExamId(realExamId); RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId()); StudentRealExamIngVo vo = new StudentRealExamIngVo(); vo.setRealExam(re); long remaining = (re.getStartTime().getTime() + rec.getLimitDuration() * 60 * 1000) - DateUtils.getNowDate().getTime(); vo.setRemainingMilliseconds(remaining); vo.setCompulsiveSubmit(remaining >= RealExam.EXAM_TIMEOUT_LIMIT); l.info("studentLoopAnsweringRealExam vo = {}", vo); return AjaxResult.success(vo); } public static final Long DURATION_10_MIN = 1000L * 60 * 10; /** * [学生]交卷考试 * * @param examId * @return RealExam */ @Transactional public AjaxResult studentSubmitRealExam(final Long examId, final String studentBindIp, final String examCollectionType) { // Check:检查参数examId有效性 { AjaxResult ar = checkExamId(examId); if (ar.isError()) { return ar; } } RealExam re = selectRealExamByExamId(examId); if (re == null || re.getExamId() == 0L || re.getSimId() == null || re.getSimId() == 0L || re.getExamCollectionId() == null || re.getExamCollectionId() == 0L) { l.debug("RealExam = {}", re); return AjaxResult.error("交卷数据错误!"); } if (re.getStartTime() == null) { return AjaxResult.error("考试开始时间异常!"); } // check:考试状态 if (StringUtils.equals(re.getExamStatus(), RealExam.State.SUBMITTED)) { return AjaxResult.error("已经交卷,禁止重复交卷,
请刷新自动结束考试!"); } RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId()); // 允许考试时长,毫秒 Long millisecondsAllowed = rec.getLimitDuration() * 60 * 1000 + DURATION_10_MIN; // Check:已经超时的交卷。 if (DateUtils.getNowDate().getTime() > re.getStartTime().getTime() + millisecondsAllowed) { // 修改考试状态 re.setExamStatus(RealExam.State.SUBMITTED); // 修改真实考试结束时间。 re.setEndTime(DateUtils.getNowDate()); updateRealExam(re); return AjaxResult.success("考试时间已经超时,自动结束考试!"); } // Check:检查参数studentBindIp有效性 if (StringUtils.isBlank(studentBindIp)) { return AjaxResult.error("IP地址无效!"); } // 现在交卷的座次 Seat seatNow = seatService.uniqueByBindIp(studentBindIp); // 开始考试的座次 Seat seatStart = seatService.selectSeatBySeatId(re.getSeatId()); if (seatNow == null || seatStart == null) { return AjaxResult.error("没有IP对应座次数据!"); } // Check:防止换座位交卷。 if (!Objects.equals(seatStart.getSeatId(), seatNow.getSeatId())) { return AjaxResult.error("没有在原始座次上交卷,请回到原座次[" + seatStart.getSeatNum() + "]上进行交卷!"); } // Check:检查参数examCollectionType有效性 if (!StringUtils.equals(examCollectionType, rec.getExamCollectionType())) { return AjaxResult.error("考试集合类型不对应!"); } // Check:ping通 RS485。 { AjaxResult ar = commCheckService.checkPingRs485State(seatStart.getSeatRs485Ip()); if (ar.isError()) { // todo:重复 // 更新SimId seatService.updateCurrentSimIdBySeatNum(seatStart.getSeatNum(), Seat.ID_0); // 更新SocketState seatService.updateSocketStateBySeatNum(seatStart.getSeatNum(), Seat.SocketState.OFFLINE); return ar; } else { // Ping通不代表在线,Socket连接建立表示在线。 } } // Check:如果有缓存Socket并且可用,使用缓存Socket,检查并建立Socket连接;否则返回对应错误。 { AjaxResult ar = socketService.openOne(seatStart.toSimSocketParamVo()); if (ar.isError()) { return ar; } } // Check:发送通用询问指令,询问是连接的哪种型号的哪一台模拟器;否则返回对应错误。 { AjaxResult ar = commCheckService.checkOneSeatState(seatStart, true); if (ar.isError()) { return ar; } } // Step:查询模拟器在线状态,纯DB查询。 { AjaxResult ar = commCheckService.checkOneSimOnlineState(seatStart.getCurrentSimId()); if (ar.isError()) { return ar; } } // Check:检查是否是出题值使用的模拟器。防止换机器交卷。 re = selectRealExamByExamId(examId); if (!Objects.equals(re.getSimId(), seatNow.getCurrentSimId())) { return AjaxResult.error("没有使用原始模拟器交卷,请使用模拟器[" + simService.selectSimBySimId(re.getSimId()).getSimNum() + "]进行交卷!"); } // Check:检查考试状态 if (StringUtils.equals(re.getExamStatus(), RealExam.State.SUBMITTED)) { return AjaxResult.success("已经成功交卷,跳过交卷!"); } // todo:检查一下模拟器状态。 // Check:检查换学生端交卷的情况。 // // Step:最后读取一下模拟器电阻值。 commSendService.readOneExamAtLast(re); // Step: if (realExamFaultService.isType2ExamPrepareSubmitOk(re.getExamId())) { re.setExamStatus(RealExam.State.SUBMITTED); // 修改真实考试结束时间。 re.setEndTime(DateUtils.getNowDate()); updateRealExam(re); return AjaxResult.success("交卷成功!"); } else { return AjaxResult.error("交卷失败!考试数据不完整。"); } } /** * [轮询][学生]结束考试界面。 * * @param examId * @return StudentRealExamPostVo */ public AjaxResult studentLoopPostRealExam(Long examId) { RealExam re = selectRealExamByExamId(examId); StudentRealExamPostVo vo = new StudentRealExamPostVo(); { } vo.setRealExam(re); vo.setListPart1(realExamFaultService.getReportListPart1(examId)); vo.setListPart2(realExamFaultService.getReportListPart2(examId)); vo.setPart3(realExamFaultService.getReportPart3(examId)); return AjaxResult.success(vo); } /** * [学员]登录成功后调用 * * @param userId * @param ip * @return */ public AjaxResult studentLoginSuccess(final Long userId, final String ip) { return AjaxResult.success(); } }