RealExamService.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. package com.ruoyi.sim.service.impl;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. import java.util.Objects;
  5. import com.ruoyi.common.core.domain.AjaxResult;
  6. import com.ruoyi.common.utils.DateUtils;
  7. import com.ruoyi.sim.config.SimConfig;
  8. import com.ruoyi.sim.config.SimDebugConfig;
  9. import com.ruoyi.sim.domain.*;
  10. import com.ruoyi.sim.domain.vo.RealExamVo;
  11. import com.ruoyi.sim.domain.vo.StudentRealExamIngVo;
  12. import com.ruoyi.sim.domain.vo.StudentRealExamPostVo;
  13. import com.ruoyi.sim.domain.vo.StudentRealExamPreVo;
  14. import org.apache.commons.lang3.StringUtils;
  15. import org.slf4j.Logger;
  16. import org.slf4j.LoggerFactory;
  17. import org.springframework.beans.factory.annotation.Autowired;
  18. import org.springframework.context.annotation.Lazy;
  19. import org.springframework.stereotype.Service;
  20. import com.ruoyi.sim.mapper.RealExamMapper;
  21. import org.springframework.transaction.annotation.Transactional;
  22. /**
  23. * 考试Service业务层处理
  24. *
  25. * @author tom
  26. * @date 2024-12-15
  27. */
  28. @Service
  29. public class RealExamService {
  30. @Autowired
  31. private RealExamMapper realExamMapper;
  32. /**
  33. * 查询考试
  34. *
  35. * @param examId 考试主键
  36. * @return 考试
  37. */
  38. public RealExam selectRealExamByExamId(Long examId) {
  39. return realExamMapper.selectRealExamByExamId(examId);
  40. }
  41. /**
  42. * 查询考试列表
  43. *
  44. * @param realExam 考试
  45. * @return 考试
  46. */
  47. public List<RealExam> selectRealExamList(RealExam realExam) {
  48. return realExamMapper.selectRealExamList(realExam);
  49. }
  50. /**
  51. * 新增考试
  52. *
  53. * @param realExam 考试
  54. * @return 结果
  55. */
  56. public int insertRealExam(RealExam realExam) {
  57. realExam.setCreateTime(DateUtils.getNowDate());
  58. return realExamMapper.insertRealExam(realExam);
  59. }
  60. /**
  61. * 修改考试
  62. *
  63. * @param realExam 考试
  64. * @return 结果
  65. */
  66. public int updateRealExam(RealExam realExam) {
  67. realExam.setUpdateTime(DateUtils.getNowDate());
  68. return realExamMapper.updateRealExam(realExam);
  69. }
  70. /**
  71. * 批量删除考试
  72. *
  73. * @param examIds 需要删除的考试主键
  74. * @return 结果
  75. */
  76. public int deleteRealExamByExamIds(Long[] examIds) {
  77. return realExamMapper.deleteRealExamByExamIds(examIds);
  78. }
  79. /**
  80. * 删除考试信息
  81. *
  82. * @param examId 考试主键
  83. * @return 结果
  84. */
  85. public int deleteRealExamByExamId(Long examId) {
  86. return realExamMapper.deleteRealExamByExamId(examId);
  87. }
  88. // -------------------------------- tom add --------------------------------
  89. private static final Logger l = LoggerFactory.getLogger(CommSendService.class);
  90. @Autowired
  91. private CommReceiveService commReceiveService;
  92. @Autowired
  93. private StudentService studentService;
  94. @Autowired
  95. private SimService simService;
  96. @Autowired
  97. private SeatService seatService;
  98. @Autowired
  99. private RealExamCollectionService realExamCollectionService;
  100. @Autowired
  101. private RealExamFaultService realExamFaultService;
  102. @Autowired
  103. @Lazy
  104. private CommSendService commSendService;
  105. @Autowired
  106. @Lazy
  107. private CommCheckService commCheckService;
  108. @Autowired
  109. private SimConfig simConfig;
  110. @Autowired
  111. private SocketService socketService;
  112. /**
  113. * examId 是否有效。
  114. *
  115. * @param examId
  116. * @return
  117. */
  118. public boolean exist(Long examId) {
  119. if (examId == null) {
  120. return false;
  121. }
  122. if (examId == 0) {
  123. return false;
  124. }
  125. RealExam re = selectRealExamByExamId(examId);
  126. if (re == null) {
  127. return false;
  128. }
  129. return true;
  130. }
  131. public List<RealExamVo> list(RealExam q) {
  132. List<RealExamVo> list = new ArrayList<>();
  133. realExamMapper.selectRealExamList(q).forEach(re -> {
  134. RealExamVo v = new RealExamVo();
  135. RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  136. v.setRealExam(re);
  137. v.setRealExamCollection(rec);
  138. list.add(v);
  139. });
  140. return list;
  141. }
  142. public List<RealExam> listAllByStatus(String state) {
  143. RealExam q = new RealExam();
  144. q.setExamStatus(state);
  145. return selectRealExamList(q);
  146. }
  147. /**
  148. * 交卷自动修改关联状态
  149. *
  150. * @param examId
  151. * @param state
  152. * @return
  153. */
  154. public int updateOneState(long examId, final String state) {
  155. RealExam q = selectRealExamByExamId(examId);
  156. // todo:屏蔽
  157. if (false && RealExam.State.SUBMITTED.equals(state)) {
  158. // 关联故障list同步锁死。
  159. realExamFaultService.listAllType2State2and3ByExamId(q.getExamId())
  160. .forEach(ref -> {
  161. ref.setRefState(RealExamFault.State.FINISH);
  162. realExamFaultService.updateRealExamFault(ref);
  163. });
  164. }
  165. q.setExamStatus(state);
  166. return updateRealExam(q);
  167. }
  168. /**
  169. * [学生]进入考试。
  170. *
  171. * @return RealExam
  172. */
  173. public AjaxResult studentEnterRealExam(Long examId) {
  174. RealExam re = selectRealExamByExamId(examId);
  175. if (re == null) {
  176. AjaxResult.error("realExamId error!");
  177. }
  178. // todo:应该在登录位置实现
  179. // todo: temp
  180. updateOneState(examId, RealExam.State.LOGGED_IN);
  181. // todo: temp
  182. realExamFaultService.resetAllType2(examId);
  183. return AjaxResult.success(re);
  184. }
  185. /**
  186. * [轮询][学生]准备考试界面。
  187. *
  188. * @param realExamId
  189. * @return StudentRealExamPreVo
  190. */
  191. public AjaxResult studentLoopPrepareRealExam(Long realExamId) {
  192. l.info("studentLoopPrepareRealExam");
  193. // check
  194. if (realExamId == null || realExamId == 0) {
  195. // todo:
  196. }
  197. //
  198. RealExam re = selectRealExamByExamId(realExamId);
  199. if (re == null) {
  200. // todo:
  201. }
  202. // todo: 日期,不可进入考试。
  203. //
  204. // todo: 验证学生登录身份`
  205. Objects.requireNonNull(re);
  206. RealExamCollection collection =
  207. realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  208. // check collection
  209. Sim sim = simService.selectSimBySimId(re.getSimId());
  210. // check sim
  211. Student student = studentService.selectStudentByUserId(re.getUserId());
  212. // check student
  213. Seat seat = seatService.selectSeatBySeatId(re.getSeatId());
  214. // check seat
  215. StudentRealExamPreVo vo = new StudentRealExamPreVo();
  216. vo.setRealExam(re);
  217. vo.setRealExamCollection(collection);
  218. vo.setSim(sim);
  219. vo.setStudent(student);
  220. vo.setSeat(seat);
  221. // todo:多人请求同时进入的问题
  222. boolean next = studentPrepareRealExamCheck(vo);
  223. vo.setNext(next);
  224. if (!next) {
  225. // 执行模拟器通信,让模拟器准备好。
  226. // 异步执行
  227. commSendService.clearListFaultByRealExamAsync(re);
  228. }
  229. l.info("vo = {}", vo);
  230. return AjaxResult.success(vo);
  231. }
  232. public boolean studentPrepareRealExamCheck(StudentRealExamPreVo v) {
  233. if (v == null ||
  234. v.getRealExam() == null ||
  235. v.getRealExamCollection() == null ||
  236. v.getSim() == null ||
  237. v.getStudent() == null ||
  238. v.getSeat() == null) {
  239. return false;
  240. }
  241. // todo:在考试日期内。
  242. // check 一个模拟器的所有选中故障点位都下发成功,准备是否可以
  243. //
  244. // todo:??
  245. // 学生答题中可以再次进入。
  246. String examStatus = v.getRealExam().getExamStatus();
  247. String simStatus = v.getSim().getSimState();
  248. // 兜底
  249. {
  250. if (realExamFaultService.isAllType2StateXiaFa(v.getRealExam().getExamId())) {
  251. // updateOneState(v.getRealExam().getExamId(), RealExam.State.SIM_PREPARE_OK);
  252. }
  253. }
  254. // 模拟器不通信。
  255. if (!simConfig.isCommGlobal()) {
  256. return true;
  257. }
  258. if ((RealExam.State.SIM_PREPARE_OK.equals(examStatus) ||
  259. RealExam.State.ANSWERING.equals(examStatus)) &&
  260. Sim.State.ONLINE.equals(simStatus)
  261. ) {
  262. return true;
  263. }
  264. return false;
  265. }
  266. /**
  267. * [学生]开始考试、训练、练习
  268. *
  269. * @param examId 考试Id
  270. * @param studentBindIp 考试学员IP
  271. * @param examCollectionType 考试集合类型
  272. * @return
  273. */
  274. public AjaxResult studentStartRealExam(final Long examId, final String studentBindIp, final String examCollectionType) {
  275. // Check:检查参数examId有效性
  276. {
  277. AjaxResult ar = checkExamId(examId);
  278. if (ar.isError()) {
  279. return ar;
  280. }
  281. }
  282. RealExam re = selectRealExamByExamId(examId);
  283. RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  284. // Check:检查参数studentBindIp有效性
  285. if (StringUtils.isBlank(studentBindIp)) {
  286. return AjaxResult.error("IP地址无效");
  287. }
  288. Seat seat = seatService.uniqueByBindIp(studentBindIp);
  289. if (seat == null) {
  290. return AjaxResult.error("没有IP对应座次数据!");
  291. }
  292. // Check:检查参数examCollectionType有效性
  293. if (!StringUtils.equals(examCollectionType, rec.getExamCollectionType())) {
  294. return AjaxResult.error("考试集合类型不对应!");
  295. }
  296. // Check:ping通 路由器。
  297. {
  298. AjaxResult ar = commCheckService.checkRouterState(simConfig.getRouterIp());
  299. if (ar.isError()) {
  300. return ar;
  301. }
  302. }
  303. // Check:ping通 学员端电脑。
  304. if (false) {
  305. AjaxResult ar = commCheckService.checkPingStudentPcState(studentBindIp);
  306. if (ar.isError()) {
  307. return ar;
  308. }
  309. }
  310. // Check:ping通 RS485。
  311. {
  312. AjaxResult ar = commCheckService.checkPingRs485State(seat.getSeatRs485Ip());
  313. if (ar.isError()) {
  314. // todo:重复
  315. // 更新SimId
  316. seatService.updateSimIdBySeatNum(seat.getSeatNum(), Seat.ID_0);
  317. // 更新SocketState
  318. seatService.updateSocketStateBySeatNum(seat.getSeatNum(), Seat.SocketState.OFFLINE);
  319. return ar;
  320. } else {
  321. // Ping通不代表在线,Socket连接建立表示在线。
  322. }
  323. }
  324. // Check:如果有缓存Socket并且可用,使用缓存Socket,检查并建立Socket连接;否则返回对应错误。
  325. {
  326. AjaxResult ar = socketService.openOne(seat.toSimSocketParamVo(), true);
  327. if (ar.isError()) {
  328. return ar;
  329. }
  330. }
  331. // Check:发送通用询问指令,询问是连接的哪种型号的哪一台模拟器;否则返回对应错误。
  332. {
  333. AjaxResult ar = commCheckService.checkOneSeatState(seat, true);
  334. if (ar.isError()) {
  335. return ar;
  336. }
  337. }
  338. // Step:重新查询。已经确定simId和simState了。
  339. {
  340. // 修改exam表对应examId的一条数据,填充并锁定seat_id和sim_id值。
  341. // 设置上seatId和simId
  342. re = selectRealExamByExamId(examId);
  343. seat = seatService.uniqueByBindIp(studentBindIp);
  344. re.setSeatId(seat.getSeatId());
  345. re.setSimId(seat.getCurrentSimId());
  346. updateRealExam(re);
  347. }
  348. // Step:查询模拟器在线状态,纯DB查询。
  349. {
  350. AjaxResult ar = commCheckService.checkOneSimOnlineState(seat.getCurrentSimId());
  351. if (ar.isError()) {
  352. return ar;
  353. }
  354. }
  355. Sim sim = simService.selectSimBySimId(re.getSimId());
  356. // Check:检查模拟器类型
  357. final String targetSimType = re.getSimType();
  358. {
  359. AjaxResult ar = commCheckService.checkOneSimType(seat, true, targetSimType);
  360. if (ar.isError()) {
  361. return ar;
  362. }
  363. }
  364. // 针对练习(自主练习),进行特殊检查。
  365. if (StringUtils.equals(RealExamCollection.Type.SELF_EXERCISE, examCollectionType)) {
  366. // 已经open的考试。
  367. // 已经open的训练。
  368. }
  369. // Step:可换件检查,读取对应一台模拟器 所有故障部位值。
  370. // 检查模拟器所有的 真实的 故障部位 是否异常 或者 空值。特殊的故障部位要单独判断。
  371. if (SimDebugConfig.CHECK_REPLACE_EMPTY) {
  372. AjaxResult ar = commSendService.readOneSimAllFaultCheck(seat, sim);
  373. if (ar.isError()) {
  374. return ar;
  375. }
  376. }
  377. // Step:正式开始考试。
  378. // Step:清除对应一台模拟器 所有 真实的 故障部位故障。
  379. {
  380. commSendService.clearOneSimAllFaultByExam(re);
  381. }
  382. // Step:下发对应一台模拟器 出题选中的 故障位置故障。
  383. {
  384. commSendService.writeOneSimAllSelectFaultByExam(re);
  385. }
  386. // Step:读取对应一台模拟器 所有的 真实的 故障部位 电阻值代表值 作为出题值。
  387. {
  388. commSendService.readOneSimAllFaultFirstTimeByExam(re);
  389. }
  390. // Step:修改当前exam_id的考试状态。
  391. // 修改关联状态
  392. if (realExamFaultService.isType2ExamPrepareStartOk(re.getExamId())) {
  393. updateOneState(re.getExamId(), RealExam.State.SIM_PREPARE_OK);
  394. updateOneState(re.getExamId(), RealExam.State.ANSWERING);
  395. // 修改真实考试开始时间。
  396. re.setStartTime(DateUtils.getNowDate());
  397. updateRealExam(re);
  398. return AjaxResult.success("开始考试成功!");
  399. } else {
  400. return AjaxResult.error("执行超时!");
  401. }
  402. }
  403. public AjaxResult studentRefreshSimState(final String studentBindIp) {
  404. Seat seat = seatService.uniqueByBindIp(studentBindIp);
  405. // Check:Seat有效性。
  406. {
  407. if (seat == null) {
  408. return AjaxResult.error("没有IP对应座次数据!");
  409. }
  410. }
  411. return commCheckService.checkOneSeatState(seat, true);
  412. }
  413. public AjaxResult checkExamId(final Long examId) {
  414. // Check:检查 examId 是否正确存在
  415. if (!exist(examId)) {
  416. return AjaxResult.error("对应考试Id不存在!");
  417. } else {
  418. return AjaxResult.success();
  419. }
  420. }
  421. /**
  422. * [轮询][学生]正在考试界面。
  423. *
  424. * @param realExamId
  425. * @return StudentRealExamIngVo
  426. */
  427. public AjaxResult studentLoopAnsweringRealExam(Long realExamId) {
  428. RealExam re = selectRealExamByExamId(realExamId);
  429. RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  430. StudentRealExamIngVo vo = new StudentRealExamIngVo();
  431. vo.setRealExam(re);
  432. long remaining = (re.getStartTime().getTime() + rec.getLimitDuration() * 60 * 1000) - DateUtils.getNowDate().getTime();
  433. vo.setRemainingMilliseconds(remaining);
  434. vo.setCompulsiveSubmit(remaining >= RealExam.EXAM_TIMEOUT_LIMIT);
  435. l.info("studentLoopAnsweringRealExam vo = {}", vo);
  436. return AjaxResult.success(vo);
  437. }
  438. /**
  439. * [学生]交卷考试
  440. *
  441. * @param examId
  442. * @return RealExam
  443. */
  444. @Transactional
  445. public AjaxResult studentSubmitRealExam(final Long examId, final String studentBindIp, final String examCollectionType) {
  446. // Check:检查参数examId有效性
  447. {
  448. AjaxResult ar = checkExamId(examId);
  449. if (ar.isError()) {
  450. return ar;
  451. }
  452. }
  453. RealExam re = selectRealExamByExamId(examId);
  454. RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
  455. // Check:检查参数studentBindIp有效性
  456. if (StringUtils.isBlank(studentBindIp)) {
  457. return AjaxResult.error("IP地址无效");
  458. }
  459. // 现在交卷的座次
  460. Seat seatNow = seatService.uniqueByBindIp(studentBindIp);
  461. // 开始考试的座次
  462. Seat seatStart = seatService.selectSeatBySeatId(re.getSeatId());
  463. if (seatNow == null || seatStart == null) {
  464. return AjaxResult.error("没有IP对应座次数据!");
  465. }
  466. // Check:防止换座位交卷。
  467. if (!Objects.equals(seatStart.getSeatId(), seatNow.getSeatId())) {
  468. return AjaxResult.error("没有在原始座次上交卷,请回到原座次[" + seatStart.getSeatNum() + "]上进行交卷!");
  469. }
  470. // Check:检查参数examCollectionType有效性
  471. if (!StringUtils.equals(examCollectionType, rec.getExamCollectionType())) {
  472. return AjaxResult.error("考试集合类型不对应!");
  473. }
  474. // Check:ping通 RS485。
  475. {
  476. AjaxResult ar = commCheckService.checkPingRs485State(seatStart.getSeatRs485Ip());
  477. if (ar.isError()) {
  478. // todo:重复
  479. // 更新SimId
  480. seatService.updateSimIdBySeatNum(seatStart.getSeatNum(), Seat.ID_0);
  481. // 更新SocketState
  482. seatService.updateSocketStateBySeatNum(seatStart.getSeatNum(), Seat.SocketState.OFFLINE);
  483. return ar;
  484. } else {
  485. // Ping通不代表在线,Socket连接建立表示在线。
  486. }
  487. }
  488. // Check:如果有缓存Socket并且可用,使用缓存Socket,检查并建立Socket连接;否则返回对应错误。
  489. {
  490. AjaxResult ar = socketService.openOne(seatStart.toSimSocketParamVo(), true);
  491. if (ar.isError()) {
  492. return ar;
  493. }
  494. }
  495. // Check:发送通用询问指令,询问是连接的哪种型号的哪一台模拟器;否则返回对应错误。
  496. {
  497. AjaxResult ar = commCheckService.checkOneSeatState(seatStart, true);
  498. if (ar.isError()) {
  499. return ar;
  500. }
  501. }
  502. // Step:查询模拟器在线状态,纯DB查询。
  503. {
  504. AjaxResult ar = commCheckService.checkOneSimOnlineState(seatStart.getCurrentSimId());
  505. if (ar.isError()) {
  506. return ar;
  507. }
  508. }
  509. // Check:检查是否是出题值使用的模拟器。防止换机器交卷。
  510. re = selectRealExamByExamId(examId);
  511. if (!Objects.equals(re.getSimId(), seatNow.getCurrentSimId())) {
  512. return AjaxResult.error("没有使用原始模拟器交卷,请使用模拟器[" +
  513. simService.selectSimBySimId(re.getSimId()).getSimNum() +
  514. "]进行交卷!");
  515. }
  516. // Check:检查考试状态
  517. if (StringUtils.equals(re.getExamStatus(), RealExam.State.SUBMITTED)) {
  518. return AjaxResult.success("已经成功交卷,跳过交卷!");
  519. }
  520. // todo:检查一下模拟器状态。
  521. // Check:检查换学生端交卷的情况。
  522. //
  523. // Step:最后读取一下模拟器电阻值。
  524. commSendService.readOneExamAtLast(re);
  525. // Step:
  526. if (realExamFaultService.isType2ExamPrepareSubmitOk(re.getExamId())) {
  527. re.setExamStatus(RealExam.State.SUBMITTED);
  528. // 修改真实考试结束时间。
  529. re.setEndTime(DateUtils.getNowDate());
  530. updateRealExam(re);
  531. return AjaxResult.success("成功交卷!");
  532. } else {
  533. return AjaxResult.error("失败交卷!");
  534. }
  535. }
  536. /**
  537. * [轮询][学生]结束考试界面。
  538. *
  539. * @param examId
  540. * @return StudentRealExamPostVo
  541. */
  542. public AjaxResult studentLoopPostRealExam(Long examId) {
  543. RealExam re = selectRealExamByExamId(examId);
  544. StudentRealExamPostVo vo = new StudentRealExamPostVo();
  545. {
  546. }
  547. vo.setRealExam(re);
  548. vo.setListPart1(realExamFaultService.getReportListPart1(examId));
  549. vo.setListPart2(realExamFaultService.getReportListPart2(examId));
  550. vo.setPart3(realExamFaultService.getReportPart3(examId));
  551. return AjaxResult.success(vo);
  552. }
  553. }