RealExamService.java 33 KB

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