RealExamService.java 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. package com.ruoyi.sim.service.impl;
  2. import java.util.*;
  3. import com.ruoyi.common.core.domain.AjaxResult;
  4. import com.ruoyi.common.utils.DateUtils;
  5. import com.ruoyi.sim.config.SimConfig;
  6. import com.ruoyi.sim.config.SimDebugConfig;
  7. import com.ruoyi.sim.domain.*;
  8. import com.ruoyi.sim.domain.vo.*;
  9. import org.apache.commons.lang3.StringUtils;
  10. import org.slf4j.Logger;
  11. import org.slf4j.LoggerFactory;
  12. import org.springframework.beans.factory.annotation.Autowired;
  13. import org.springframework.context.annotation.Lazy;
  14. import org.springframework.stereotype.Service;
  15. import com.ruoyi.sim.mapper.RealExamMapper;
  16. import org.springframework.transaction.annotation.Transactional;
  17. /**
  18. * 考试Service业务层处理
  19. *
  20. * @author tom
  21. * @date 2024-12-15
  22. */
  23. @Service
  24. public class RealExamService {
  25. @Autowired
  26. private RealExamMapper realExamMapper;
  27. /**
  28. * 查询考试
  29. *
  30. * @param examId 考试主键
  31. * @return 考试
  32. */
  33. public RealExam selectRealExamByExamId(Long examId) {
  34. return realExamMapper.selectRealExamByExamId(examId);
  35. }
  36. /**
  37. * 查询考试列表
  38. *
  39. * @param realExam 考试
  40. * @return 考试
  41. */
  42. public List<RealExam> selectRealExamList(RealExam realExam) {
  43. return realExamMapper.selectRealExamList(realExam);
  44. }
  45. /**
  46. * 新增考试
  47. *
  48. * @param realExam 考试
  49. * @return 结果
  50. */
  51. public int insertRealExam(RealExam realExam) {
  52. realExam.setCreateTime(DateUtils.getNowDate());
  53. return realExamMapper.insertRealExam(realExam);
  54. }
  55. /**
  56. * 修改考试
  57. *
  58. * @param realExam 考试
  59. * @return 结果
  60. */
  61. public int updateRealExam(RealExam realExam) {
  62. realExam.setUpdateTime(DateUtils.getNowDate());
  63. return realExamMapper.updateRealExam(realExam);
  64. }
  65. /**
  66. * 批量删除考试
  67. *
  68. * @param examIds 需要删除的考试主键
  69. * @return 结果
  70. */
  71. public int deleteRealExamByExamIds(Long[] examIds) {
  72. return realExamMapper.deleteRealExamByExamIds(examIds);
  73. }
  74. /**
  75. * 删除考试信息
  76. *
  77. * @param examId 考试主键
  78. * @return 结果
  79. */
  80. public int deleteRealExamByExamId(Long examId) {
  81. return realExamMapper.deleteRealExamByExamId(examId);
  82. }
  83. // -------------------------------- tom add --------------------------------
  84. private static final Logger l = LoggerFactory.getLogger(CommSendService.class);
  85. @Autowired
  86. private CommReceiveService commReceiveService;
  87. @Autowired
  88. private StudentService studentService;
  89. @Autowired
  90. private SimService simService;
  91. @Autowired
  92. private SeatService seatService;
  93. @Autowired
  94. private RealExamCollectionService realExamCollectionService;
  95. @Autowired
  96. private RealExamFaultService realExamFaultService;
  97. @Autowired
  98. @Lazy
  99. private CommSendService commSendService;
  100. @Autowired
  101. @Lazy
  102. private CommCheckService commCheckService;
  103. @Autowired
  104. private SimConfig simConfig;
  105. @Autowired
  106. private SocketService socketService;
  107. /**
  108. * examId 是否有效。
  109. *
  110. * @param examId
  111. * @return
  112. */
  113. public boolean exist(Long examId) {
  114. if (examId == null) {
  115. return false;
  116. }
  117. if (examId == 0) {
  118. return false;
  119. }
  120. RealExam re = selectRealExamByExamId(examId);
  121. if (re == null) {
  122. return false;
  123. }
  124. return true;
  125. }
  126. public List<RealExamVo> list(RealExam q) {
  127. List<RealExamVo> list = new ArrayList<>();
  128. realExamMapper.selectRealExamList(q).forEach(re -> {
  129. RealExamVo v = new RealExamVo();
  130. RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  131. v.setRealExam(re);
  132. v.setRealExamCollection(rec);
  133. list.add(v);
  134. });
  135. return list;
  136. }
  137. public List<RealExam> listAllByStatus(String state) {
  138. RealExam q = new RealExam();
  139. q.setExamStatus(state);
  140. return selectRealExamList(q);
  141. }
  142. /**
  143. * 交卷自动修改关联状态
  144. *
  145. * @param examId
  146. * @param state
  147. * @return
  148. */
  149. public int updateOneState(long examId, final String state) {
  150. RealExam q = selectRealExamByExamId(examId);
  151. // todo:屏蔽
  152. if (false && RealExam.State.SUBMITTED.equals(state)) {
  153. // 关联故障list同步锁死。
  154. realExamFaultService.listAllType2State2and3ByExamId(q.getExamId())
  155. .forEach(ref -> {
  156. ref.setRefState(RealExamFault.State.FINISH);
  157. realExamFaultService.updateRealExamFault(ref);
  158. });
  159. }
  160. q.setExamStatus(state);
  161. return updateRealExam(q);
  162. }
  163. /**
  164. * [学生]进入考试。
  165. *
  166. * @return RealExam
  167. */
  168. public AjaxResult studentEnterRealExam(Long examId) {
  169. RealExam re = selectRealExamByExamId(examId);
  170. if (re == null) {
  171. AjaxResult.error("realExamId error!");
  172. }
  173. // todo:应该在登录位置实现
  174. // todo: temp
  175. updateOneState(examId, RealExam.State.LOGGED_IN);
  176. // todo: temp
  177. realExamFaultService.resetAllType2(examId);
  178. return AjaxResult.success(re);
  179. }
  180. /**
  181. * [轮询][学生]准备考试界面。
  182. *
  183. * @param realExamId
  184. * @return StudentRealExamPreVo
  185. */
  186. public AjaxResult studentLoopPrepareRealExam(Long realExamId) {
  187. l.info("studentLoopPrepareRealExam");
  188. // check
  189. if (realExamId == null || realExamId == 0) {
  190. // todo:
  191. }
  192. //
  193. RealExam re = selectRealExamByExamId(realExamId);
  194. if (re == null) {
  195. // todo:
  196. }
  197. // todo: 日期,不可进入考试。
  198. //
  199. // todo: 验证学生登录身份`
  200. Objects.requireNonNull(re);
  201. RealExamCollection collection =
  202. realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  203. // check collection
  204. Sim sim = simService.selectSimBySimId(re.getSimId());
  205. // check sim
  206. Student student = studentService.selectStudentByUserId(re.getUserId());
  207. // check student
  208. Seat seat = seatService.selectSeatBySeatId(re.getSeatId());
  209. // check seat
  210. StudentRealExamPreVo vo = new StudentRealExamPreVo();
  211. vo.setRealExam(re);
  212. vo.setRealExamCollection(collection);
  213. vo.setSim(sim);
  214. vo.setStudent(student);
  215. vo.setSeat(seat);
  216. // todo:多人请求同时进入的问题
  217. boolean next = studentPrepareRealExamCheck(vo);
  218. vo.setNext(next);
  219. if (!next) {
  220. // 执行模拟器通信,让模拟器准备好。
  221. // 异步执行
  222. commSendService.clearListFaultByRealExamAsync(re);
  223. }
  224. l.info("vo = {}", vo);
  225. return AjaxResult.success(vo);
  226. }
  227. public boolean studentPrepareRealExamCheck(StudentRealExamPreVo v) {
  228. if (v == null ||
  229. v.getRealExam() == null ||
  230. v.getRealExamCollection() == null ||
  231. v.getSim() == null ||
  232. v.getStudent() == null ||
  233. v.getSeat() == null) {
  234. return false;
  235. }
  236. // todo:在考试日期内。
  237. // check 一个模拟器的所有选中故障点位都下发成功,准备是否可以
  238. //
  239. // todo:??
  240. // 学生答题中可以再次进入。
  241. String examStatus = v.getRealExam().getExamStatus();
  242. String simStatus = v.getSim().getSimState();
  243. // 兜底
  244. {
  245. if (realExamFaultService.isAllType2StateXiaFa(v.getRealExam().getExamId())) {
  246. // updateOneState(v.getRealExam().getExamId(), RealExam.State.SIM_PREPARE_OK);
  247. }
  248. }
  249. // 模拟器不通信。
  250. if (!simConfig.isCommGlobal()) {
  251. return true;
  252. }
  253. if ((RealExam.State.SIM_PREPARE_OK.equals(examStatus) ||
  254. RealExam.State.ANSWERING.equals(examStatus)) &&
  255. Sim.State.ONLINE.equals(simStatus)
  256. ) {
  257. return true;
  258. }
  259. return false;
  260. }
  261. /**
  262. * [学生]开始 考试、训练、练习
  263. *
  264. * @param examId 考试Id
  265. * @param studentBindIp 考试学员IP
  266. * @param examCollectionType 考试集合类型
  267. * @return
  268. */
  269. public AjaxResult studentStartRealExam(final Long examId, final String studentBindIp, final String examCollectionType) {
  270. // Check:针对训练,进行特殊检查。
  271. if (StringUtils.equals(RealExamCollection.Type.EXERCISE, examCollectionType)) {
  272. // 已经open的考试。
  273. if (realExamCollectionService.existOpenedByType(RealExamCollection.Type.EXAM)) {
  274. return AjaxResult.error("存在打开的考试,无法开启训练!<br/>请向教员说明情况。");
  275. }
  276. } else {
  277. l.info("type EXERCISE,没有打开的考试,校验正确");
  278. }
  279. // Check:针对练习(old叫自主练习),进行特殊检查。
  280. if (StringUtils.equals(RealExamCollection.Type.SELF_EXERCISE, examCollectionType)) {
  281. // 已经open的考试。
  282. if (realExamCollectionService.existOpenedByType(RealExamCollection.Type.EXAM)) {
  283. return AjaxResult.error("存在打开的考试,无法开启练习!<br/>请向教员说明情况。");
  284. } else {
  285. l.info("type SELF_EXERCISE,没有打开的考试,校验正确");
  286. }
  287. // 已经open的训练。
  288. if (realExamCollectionService.existOpenedByType(RealExamCollection.Type.EXERCISE)) {
  289. return AjaxResult.error("存在打开的训练,无法开启练习!<br/>请向教员说明情况。");
  290. } else {
  291. l.info("type SELF_EXERCISE,没有打开的训练,校验正确");
  292. }
  293. }
  294. // Check:检查参数examId有效性
  295. {
  296. AjaxResult ar = checkExamId(examId);
  297. if (ar.isError()) {
  298. return ar;
  299. }
  300. }
  301. RealExam re = selectRealExamByExamId(examId);
  302. // 执行到开始考试,肯定已经登录了。
  303. {
  304. // 学员Id
  305. Long userId = re.getUserId();
  306. studentLoginSuccess(userId, studentBindIp);
  307. }
  308. // check:考试状态
  309. if (StringUtils.equals(re.getExamStatus(), RealExam.State.SUBMITTED) ||
  310. StringUtils.equals(re.getExamStatus(), RealExam.State.CALCULATING_SCORE) ||
  311. StringUtils.equals(re.getExamStatus(), RealExam.State.GOT_REPORT)) {
  312. return AjaxResult.error("已经交卷,禁止重复开始考试!");
  313. } else {
  314. l.info("没有重复交卷校验正确");
  315. }
  316. RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  317. // Check:考试集合数据有效性。
  318. if (rec == null) {
  319. return AjaxResult.error("考试集合数据异常!");
  320. } else {
  321. l.info("考试集合数据校验正确");
  322. }
  323. if (!StringUtils.equals(rec.getExamCollectionState(), RealExamCollection.State.OPENED)) {
  324. if (StringUtils.equals(rec.getExamCollectionType(), RealExamCollection.Type.SELF_EXERCISE)) {
  325. l.info("练习类型考试集合,不需要检查 考试集合 开关状态。");
  326. } else {
  327. return AjaxResult.error("教师端对应考试/训练尚未打开!<br/>请向教员说明情况。");
  328. }
  329. } else {
  330. l.info("考试集合开启校验正确");
  331. }
  332. // Check:检查参数examCollectionType有效性
  333. if (!StringUtils.equals(examCollectionType, rec.getExamCollectionType())) {
  334. return AjaxResult.error("考试集合类型不对应!");
  335. } else {
  336. l.info("考试集合类型校验正确");
  337. }
  338. // Check:检查参数studentBindIp有效性
  339. if (StringUtils.isBlank(studentBindIp)) {
  340. return AjaxResult.error("IP地址无效");
  341. } else {
  342. l.info("IP地址检验正确");
  343. }
  344. Seat seat = seatService.uniqueByBindIp(studentBindIp);
  345. if (seat == null) {
  346. return AjaxResult.error("没有IP对应座次数据!");
  347. } else {
  348. l.info("座次数据检验正确");
  349. }
  350. // Check:ping通 路由器。
  351. if (false) {
  352. AjaxResult ar = commCheckService.checkRouterState(simConfig.getRouterIp());
  353. if (ar.isError()) {
  354. return ar;
  355. } else {
  356. l.info("局域网通信校验正确");
  357. }
  358. }
  359. // Check:ping通 学员端电脑。
  360. // 不检查。
  361. if (false) {
  362. AjaxResult ar = commCheckService.checkPingStudentPcState(studentBindIp);
  363. if (ar.isError()) {
  364. return ar;
  365. }
  366. }
  367. // Check:ping通 RS485。
  368. {
  369. AjaxResult ar = commCheckService.checkPingRs485State(seat.getSeatRs485Ip());
  370. if (ar.isError()) {
  371. // todo:重复
  372. // 更新SimId
  373. seatService.updateCurrentSimIdBySeatNum(seat.getSeatNum(), Seat.ID_0);
  374. // 更新SocketState
  375. seatService.updateSocketStateBySeatNum(seat.getSeatNum(), Seat.SocketState.OFFLINE);
  376. return ar;
  377. } else {
  378. // Ping通不代表在线,Socket连接建立表示在线。
  379. l.info("RS485通信校验正确");
  380. }
  381. }
  382. // Check:如果有缓存Socket并且可用,使用缓存Socket,检查并建立Socket连接;否则返回对应错误。
  383. {
  384. AjaxResult ar = socketService.openOne(seat.toSimSocketParamVo());
  385. if (ar.isError()) {
  386. return ar;
  387. } else {
  388. l.info("Socket校验正确");
  389. }
  390. }
  391. // Check:发送通用询问指令,询问是连接的哪种型号的哪一台模拟器;否则返回对应错误。
  392. {
  393. AjaxResult ar = commCheckService.checkOneSeatState(seat, true);
  394. l.debug("ar = {}", ar);
  395. // 没有连接模拟器。
  396. if (ar.get(AjaxResult.DATA_TAG) == null ||
  397. !StringUtils.equals(((Sim) ar.get(AjaxResult.DATA_TAG)).getSimState(), Sim.State.ONLINE)) {
  398. return AjaxResult.error((String) ar.get(AjaxResult.MSG_TAG));
  399. } else {
  400. l.info("Who模拟器校验正确");
  401. }
  402. // 其他的异常情况。
  403. if (ar.isError()) {
  404. return ar;
  405. }
  406. }
  407. // Step:正式开始考试。锁定 座次 和 模拟器。
  408. // Step:重新查询。已经确定simId和simState了。
  409. {
  410. // 修改exam表对应examId的一条数据,填充并锁定seat_id和sim_id值。
  411. re = selectRealExamByExamId(examId);
  412. seat = seatService.uniqueByBindIp(studentBindIp);
  413. l.debug("seat = {}", seat);
  414. // 设置上seatId和simId
  415. re.setSeatId(seat.getSeatId());
  416. re.setSimId(seat.getCurrentSimId());
  417. updateRealExam(re);
  418. }
  419. // Check: seat_id 和 current_sim_id
  420. {
  421. RealExam reF = selectRealExamByExamId(examId);
  422. if (reF == null ||
  423. reF.getSeatId() == null ||
  424. reF.getSeatId() == 0L ||
  425. reF.getSimId() == null ||
  426. reF.getSimId() == 0) {
  427. return AjaxResult.error("开始考试异常!<br/>请刷新页面重试!");
  428. }
  429. }
  430. // Step:查询模拟器在线状态,纯DB查询。
  431. {
  432. AjaxResult ar = commCheckService.checkOneSimOnlineState(seat.getCurrentSimId());
  433. if (ar.isError()) {
  434. return ar;
  435. } else {
  436. l.info("模拟器在线校验正确");
  437. }
  438. }
  439. Sim sim = simService.selectSimBySimId(re.getSimId());
  440. // Check:检查模拟器类型
  441. final String targetSimType = re.getSimType();
  442. {
  443. AjaxResult ar = commCheckService.checkOneSimType(seat, true, targetSimType);
  444. if (ar.isError()) {
  445. return ar;
  446. } else {
  447. l.info("模拟器类型校验正确");
  448. }
  449. }
  450. // Step:可换件检查,读取对应一台模拟器 所有故障部位值。
  451. // 检查模拟器所有的 真实的 故障部位 是否异常 或者 空值。特殊的故障部位要单独判断。
  452. if (SimDebugConfig.CHECK_REPLACE_EMPTY) {
  453. AjaxResult ar = commSendService.readOneSimAllFaultCheck(seat, sim);
  454. if (ar.isError()) {
  455. return ar;
  456. } else {
  457. l.info("模拟器可换件校验正确");
  458. }
  459. }
  460. // 虚假的练习清单日志。
  461. {
  462. // if (rec != null) {
  463. // l.info("start exam,exam id = {},sim type = {},使用练习清单task id = {}.",
  464. // re.getExamId(), re.getSimType(), RandomUtils.nextInt(100, 300));
  465. // }
  466. }
  467. // Step:清除对应一台模拟器 所有 真实的 故障部位故障。
  468. {
  469. commSendService.clearOneSimAllFaultByExam(re);
  470. l.info("清除对应一台模拟器 所有 真实的 故障部位故障");
  471. }
  472. // Step:下发对应一台模拟器 出题选中的 故障位置故障。
  473. {
  474. commSendService.writeOneSimAllSelectFaultByExam(re);
  475. l.info("下发对应一台模拟器 出题选中的 故障位置故障");
  476. }
  477. // Step:读取对应一台模拟器 所有的 真实的 故障部位 电阻值代表值 作为出题值。
  478. {
  479. commSendService.readOneSimAllFaultFirstTimeByExam(re);
  480. l.info("读取对应一台模拟器 所有的 真实的 故障部位 电阻值代表值 作为出题值");
  481. }
  482. // Step:修改当前exam_id的考试状态。
  483. // 修改关联状态
  484. if (realExamFaultService.isType2ExamPrepareStartOk(re.getExamId())) {
  485. updateOneState(re.getExamId(), RealExam.State.SIM_PREPARE_OK);
  486. updateOneState(re.getExamId(), RealExam.State.ANSWERING);
  487. // 修改真实考试开始时间。
  488. re.setStartTime(DateUtils.getNowDate());
  489. updateRealExam(re);
  490. l.info("开始考试成功");
  491. return AjaxResult.success("开始考试成功!");
  492. } else {
  493. return AjaxResult.error("开始考试失败,<br/>请重新尝试开始考试!");
  494. }
  495. }
  496. public AjaxResult studentRefreshSimState(final String studentBindIp) {
  497. Seat seat = seatService.uniqueByBindIp(studentBindIp);
  498. // Check:Seat有效性。
  499. {
  500. if (seat == null) {
  501. return AjaxResult.error("没有IP对应座次数据!");
  502. }
  503. }
  504. return commCheckService.checkOneSeatState(seat, true);
  505. }
  506. public AjaxResult checkExamId(final Long examId) {
  507. // Check:检查 examId 是否正确存在
  508. if (!exist(examId)) {
  509. return AjaxResult.error("对应考试Id不存在!");
  510. } else {
  511. return AjaxResult.success();
  512. }
  513. }
  514. /**
  515. * [轮询][学生]正在考试界面。
  516. *
  517. * @param realExamId
  518. * @return StudentRealExamIngVo
  519. */
  520. public AjaxResult studentLoopAnsweringRealExam(Long realExamId) {
  521. RealExam re = selectRealExamByExamId(realExamId);
  522. RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  523. StudentRealExamIngVo vo = new StudentRealExamIngVo();
  524. vo.setRealExam(re);
  525. long remaining = (re.getStartTime().getTime() + rec.getLimitDuration() * 60 * 1000) - DateUtils.getNowDate().getTime();
  526. vo.setRemainingMilliseconds(remaining);
  527. vo.setCompulsiveSubmit(remaining >= RealExam.EXAM_TIMEOUT_LIMIT);
  528. l.info("studentLoopAnsweringRealExam vo = {}", vo);
  529. return AjaxResult.success(vo);
  530. }
  531. /**
  532. * 10分钟延长时间。
  533. */
  534. public static final Long DURATION_10_MIN = 1000L * 60 * 10;
  535. /**
  536. * [学生]交卷 考试、训练、练习
  537. *
  538. * @param examId
  539. * @return RealExam
  540. */
  541. @Transactional
  542. public AjaxResult studentSubmitRealExam(final Long examId, final String studentBindIp, final String examCollectionType) {
  543. // Check:检查参数examId有效性
  544. {
  545. AjaxResult ar = checkExamId(examId);
  546. if (ar.isError()) {
  547. return ar;
  548. }
  549. }
  550. RealExam re = selectRealExamByExamId(examId);
  551. if (re == null ||
  552. re.getExamId() == 0L ||
  553. re.getSimId() == null ||
  554. re.getSimId() == 0L ||
  555. re.getExamCollectionId() == null ||
  556. re.getExamCollectionId() == 0L) {
  557. l.debug("RealExam = {}", re);
  558. return AjaxResult.error("交卷数据错误!");
  559. }
  560. if (re.getStartTime() == null) {
  561. return AjaxResult.error("考试开始时间异常!");
  562. }
  563. // check:考试状态
  564. if (StringUtils.equals(re.getExamStatus(), RealExam.State.SUBMITTED)) {
  565. return AjaxResult.error("已经交卷,禁止重复交卷,<br/>请刷新自动结束考试!");
  566. }
  567. // Check:已经超时的交卷。
  568. if (checkRealExamIsTimeout(re.getExamId())) {
  569. // 修改考试状态
  570. re.setExamStatus(RealExam.State.SUBMITTED);
  571. // 修改真实考试结束时间。
  572. re.setEndTime(DateUtils.getNowDate());
  573. updateRealExam(re);
  574. return AjaxResult.success("考试时间已经超时,自动结束考试!");
  575. }
  576. // Check:检查参数studentBindIp有效性
  577. if (StringUtils.isBlank(studentBindIp)) {
  578. return AjaxResult.error("IP地址无效!");
  579. }
  580. // 现在交卷的座次
  581. Seat seatNow = seatService.uniqueByBindIp(studentBindIp);
  582. // 开始考试的座次
  583. Seat seatStart = seatService.selectSeatBySeatId(re.getSeatId());
  584. if (seatNow == null || seatStart == null) {
  585. return AjaxResult.error("没有IP对应座次数据!");
  586. }
  587. // Check:防止换座位交卷。
  588. if (!Objects.equals(seatStart.getSeatId(), seatNow.getSeatId())) {
  589. return AjaxResult.error("没有在原始座次上交卷,请回到原座次[" + seatStart.getSeatNum() + "]上进行交卷!");
  590. }
  591. RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  592. // Check:检查参数examCollectionType有效性
  593. if (!StringUtils.equals(examCollectionType, rec.getExamCollectionType())) {
  594. return AjaxResult.error("考试集合类型不对应!");
  595. }
  596. // Check:ping通 RS485。
  597. {
  598. AjaxResult ar = commCheckService.checkPingRs485State(seatStart.getSeatRs485Ip());
  599. if (ar.isError()) {
  600. // todo:重复
  601. // 更新SimId
  602. seatService.updateCurrentSimIdBySeatNum(seatStart.getSeatNum(), Seat.ID_0);
  603. // 更新SocketState
  604. seatService.updateSocketStateBySeatNum(seatStart.getSeatNum(), Seat.SocketState.OFFLINE);
  605. return ar;
  606. } else {
  607. // Ping通不代表在线,Socket连接建立表示在线。
  608. }
  609. }
  610. // Check:如果有缓存Socket并且可用,使用缓存Socket,检查并建立Socket连接;否则返回对应错误。
  611. {
  612. AjaxResult ar = socketService.openOne(seatStart.toSimSocketParamVo());
  613. if (ar.isError()) {
  614. return ar;
  615. }
  616. }
  617. // Check:发送通用询问指令,询问是连接的哪种型号的哪一台模拟器;否则返回对应错误。
  618. {
  619. AjaxResult ar = commCheckService.checkOneSeatState(seatStart, true);
  620. if (ar.isError()) {
  621. return ar;
  622. }
  623. }
  624. // Step:查询模拟器在线状态,纯DB查询。
  625. {
  626. AjaxResult ar = commCheckService.checkOneSimOnlineState(seatStart.getCurrentSimId());
  627. if (ar.isError()) {
  628. return ar;
  629. }
  630. }
  631. // Check:检查模拟器类型
  632. final String targetSimType = re.getSimType();
  633. {
  634. AjaxResult ar = commCheckService.checkOneSimType(seatStart, true, targetSimType);
  635. if (ar.isError()) {
  636. return ar;
  637. }
  638. }
  639. // Check:检查是否是出题值使用的模拟器。防止换机器交卷。
  640. re = selectRealExamByExamId(examId);
  641. if (!Objects.equals(re.getSimId(), seatNow.getCurrentSimId())) {
  642. return AjaxResult.error("没有使用原始模拟器交卷,请使用模拟器[" +
  643. simService.selectSimBySimId(re.getSimId()).getSimNum() +
  644. "]进行交卷!");
  645. }
  646. // Check:检查考试状态
  647. if (StringUtils.equals(re.getExamStatus(), RealExam.State.SUBMITTED)) {
  648. return AjaxResult.success("已经成功交卷,跳过交卷!");
  649. }
  650. // todo:检查一下模拟器状态。
  651. // Check:检查换学生端交卷的情况。
  652. // Step:最后读取一下模拟器电阻值。
  653. commSendService.readOneExamAtLast(re);
  654. // Step:
  655. if (realExamFaultService.isType2ExamPrepareSubmitOk(re.getExamId())) {
  656. re.setExamStatus(RealExam.State.SUBMITTED);
  657. // 修改真实考试结束时间。
  658. re.setEndTime(DateUtils.getNowDate());
  659. updateRealExam(re);
  660. // 修改sim State为 OFFLINE,顺带 ChargingCountReset
  661. simService.updateSimStateBySimId(re.getSimId(), Sim.State.OFFLINE);
  662. return AjaxResult.success("交卷成功!");
  663. } else {
  664. return AjaxResult.error("交卷失败!考试数据不完整。");
  665. }
  666. }
  667. public void systemSubmitTimeoutRealExam(Long examId) {
  668. RealExam re = selectRealExamByExamId(examId);
  669. if (re != null &&
  670. re.getExamId() != 0L &&
  671. RealExam.State.ANSWERING.equals(re.getExamStatus()) &&
  672. checkRealExamIsTimeout(re.getExamId())) {
  673. re.setExamStatus(RealExam.State.SUBMITTED);
  674. updateRealExam(re);
  675. }
  676. }
  677. /**
  678. * @param examId
  679. * @return true 已经超时
  680. */
  681. public boolean checkRealExamIsTimeout(Long examId) {
  682. RealExam re = selectRealExamByExamId(examId);
  683. if (re == null || re.getExamId() == 0L) {
  684. return false;
  685. }
  686. RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  687. // 允许考试时长,毫秒
  688. Long millisecondsAllowed = rec.getLimitDuration() * 60 * 1000 + DURATION_10_MIN;
  689. return DateUtils.getNowDate().getTime() > (re.getStartTime().getTime() + millisecondsAllowed);
  690. }
  691. /**
  692. * [轮询][学生]结束考试界面。
  693. *
  694. * @param examId
  695. * @return StudentRealExamPostVo
  696. */
  697. public AjaxResult studentLoopPostRealExam(Long examId) {
  698. RealExam re = selectRealExamByExamId(examId);
  699. StudentRealExamPostVo vo = new StudentRealExamPostVo();
  700. {
  701. }
  702. vo.setRealExam(re);
  703. vo.setListPart1(realExamFaultService.getReportListPart1(examId));
  704. vo.setListPart2(realExamFaultService.getReportListPart2(examId));
  705. vo.setPart3(realExamFaultService.getReportPart3(examId));
  706. return AjaxResult.success(vo);
  707. }
  708. /**
  709. * 仅仅针对先打开考试集合,后登录的情况有效。
  710. * 表 mx_real_exam 中写入 seat_id,修改exam_status
  711. * <p>
  712. * [学员]登录成功后调用
  713. *
  714. * @param userId
  715. * @param ip
  716. * @return
  717. */
  718. public AjaxResult studentLoginSuccess(final Long userId, final String ip) {
  719. l.info("studentLoginSuccess userId = {},ip = {}", userId, ip);
  720. RealExam q = new RealExam();
  721. q.setUserId(userId);
  722. q.setExamStatus(RealExam.State.NOT_LOGGED_IN);
  723. List<RealExam> list = selectRealExamList(q);
  724. if (list.isEmpty()) {
  725. return AjaxResult.success("没有学生考试数据");
  726. }
  727. for (RealExam re : list) {
  728. RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  729. // 如果考试集合状态是OPENED。理论上只有一个。
  730. // 考虑到一个学员在教室不会有很多场考试。
  731. if (StringUtils.equals(rec.getExamCollectionState(), RealExamCollection.State.OPENED)) { // 考试集合状态是OPENED
  732. if (StringUtils.equalsAny(re.getExamStatus(),
  733. RealExam.State.NOT_LOGGED_IN,
  734. RealExam.State.LOGGED_IN)) { // 初始化状态 或 已登录状态
  735. Seat seat = seatService.uniqueByBindIp(ip);
  736. if (seat == null || seat.getSeatId() == 0L) {
  737. return AjaxResult.error("没有座次信息");
  738. }
  739. // 覆盖 seat_id 字段
  740. re.setSeatId(seat.getSeatId());
  741. // 覆盖 exam_status 字段
  742. re.setExamStatus(RealExam.State.LOGGED_IN);
  743. // 覆盖 login_time 字段
  744. re.setLoginTime(new Date());
  745. // 更新 mx_real_exam 表
  746. updateRealExam(re);
  747. // 更新 mx_seat 表 user_id 字段
  748. seatService.updateCurrentUserIdBySeatId(seat.getSeatId(), userId);
  749. return AjaxResult.success("成功");
  750. }
  751. } else {
  752. l.info("RealExam not OPENED = {}", re.getExamId());
  753. }
  754. }
  755. return AjaxResult.success("没有学生考试数据");
  756. }
  757. /**
  758. * 清除异常考试
  759. */
  760. public void systemAutoCleanExam() {
  761. RealExam reQ = new RealExam();
  762. reQ.setExamStatus(RealExam.State.ANSWERING);
  763. List<RealExam> list = selectRealExamList(reQ);
  764. if (!list.isEmpty()) {
  765. for (RealExam re : list) {
  766. boolean timeout = checkRealExamIsTimeout(re.getExamId());
  767. RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  768. if (rec != null &&
  769. StringUtils.equals(rec.getExamCollectionType(), RealExamCollection.Type.SELF_EXERCISE) &&
  770. timeout) {
  771. re.setExamStatus(RealExam.State.SUBMITTED);
  772. updateRealExam(re);
  773. }
  774. }
  775. }
  776. }
  777. public void deleteRefByExamCollectionId(Long examCollectionId) {
  778. RealExam q = new RealExam();
  779. q.setExamCollectionId(examCollectionId);
  780. List<RealExam> list = selectRealExamList(q);
  781. list.forEach(e -> {
  782. // delete ref exam fault data.
  783. realExamFaultService.deleteRefByExamId(e.getExamId());
  784. deleteRealExamByExamId(e.getExamId());
  785. });
  786. }
  787. }