15 Revize 8e3412a4c2 ... 1c6a26e979

Autor SHA1 Zpráva Datum
  tom 1c6a26e979 使用47的服务器地址。 před 3 měsíci
  tom 2f15ee2ca8 清除异常考试,1型,外壳及零件,开始考试特殊check。 před 4 měsíci
  tom bc7a71aa58 针对 练习/自主练习 重复开始的修复。 před 4 měsíci
  tom 857da0d0f0 最后一次和模拟器成功连接并通信的时间。添加测试日志。 před 4 měsíci
  tom e9bd58b766 修改定时运行时间。2025-04-22 测试版本。 před 4 měsíci
  tom bec1cae445 每6小时重启一次项目。 před 4 měsíci
  tom b3c1af9160 答题并且不超时的考试,进行中间读取。 před 4 měsíci
  tom cd6352e812 答题并且不超时的考试,进行中间读取。 před 4 měsíci
  tom b41334ee2b 修改文案。 před 4 měsíci
  tom 1ad0716824 修改注释。 před 4 měsíci
  tom 6424be3c33 考试中间读取 15min执行一次。 před 4 měsíci
  tom 1f47aa7ecc 修改文案。 před 4 měsíci
  tom 807c7d1d65 提交 考试中间读取 功能。 před 4 měsíci
  tom 81f607f855 模拟器直连,添加部分执行日期。 před 4 měsíci
  tom 04e48456e6 完善开始考试和交卷模拟器类型检查。 před 4 měsíci
23 změnil soubory, kde provedl 512 přidání a 109 odebrání
  1. 2 2
      pla-sim/01_SQL/02_table/mx_fault.sql
  2. 51 0
      pla-sim/01_SQL/02_table/mx_real_exam_fault.sql
  3. 6 6
      ruoyi-admin/src/main/resources/application-druid.yml
  4. 6 6
      ruoyi-admin/src/main/resources/application.yml
  5. 2 1
      ruoyi-sim/src/main/java/com/ruoyi/sim/constant/CommConst.java
  6. 23 0
      ruoyi-sim/src/main/java/com/ruoyi/sim/controller/CmdController.java
  7. 1 1
      ruoyi-sim/src/main/java/com/ruoyi/sim/controller/ConfigController.java
  8. 3 0
      ruoyi-sim/src/main/java/com/ruoyi/sim/controller/TestIotController.java
  9. 1 1
      ruoyi-sim/src/main/java/com/ruoyi/sim/domain/RealExamCollection.java
  10. 2 2
      ruoyi-sim/src/main/java/com/ruoyi/sim/domain/RealExamFault.java
  11. 18 0
      ruoyi-sim/src/main/java/com/ruoyi/sim/domain/Seat.java
  12. 4 0
      ruoyi-sim/src/main/java/com/ruoyi/sim/domain/SimMsg.java
  13. 0 1
      ruoyi-sim/src/main/java/com/ruoyi/sim/domain/vo/SocketWrapCacheVo.java
  14. 41 0
      ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/CmdService.java
  15. 1 1
      ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/CommCheckService.java
  16. 13 0
      ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/CommReceiveService.java
  17. 0 22
      ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/CommRunningService.java
  18. 129 38
      ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/CommSendService.java
  19. 12 2
      ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/CommStrategy.java
  20. 11 1
      ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/RealExamCollectionService.java
  21. 140 14
      ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/RealExamService.java
  22. 34 11
      ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/SocketService.java
  23. 12 0
      ruoyi-sim/src/main/java/com/ruoyi/sim/util/SimDateUtil.java

+ 2 - 2
pla-sim/01_SQL/02_table/mx_fault.sql

@@ -11,7 +11,7 @@
  Target Server Version : 80032 (8.0.32-X-Cluster-8.4.19-20241112)
  File Encoding         : 65001
 
- Date: 01/04/2025 20:17:58
+ Date: 20/04/2025 18:44:33
 */
 
 SET NAMES utf8mb4;
@@ -245,7 +245,7 @@ INSERT INTO `mx_fault` VALUES ('0003PCFF0003', '000300010000', '', '', '', '4',
 INSERT INTO `mx_fault` VALUES ('0003PCFF0004', '000300010000', '', '', '', '4', '0003', '0', '', '0', '', '更换开关', '', 11, NULL, NULL, NULL, NULL, NULL);
 INSERT INTO `mx_fault` VALUES ('0003PCFF0005', '000300020000', '', '', '', '4', '0003', '0', '', '0', '', '更换显控报警板', '', 14, NULL, NULL, NULL, NULL, NULL);
 INSERT INTO `mx_fault` VALUES ('0003PCFF0006', '000300020000', '', '', '', '4', '0003', '0', '', '0', '', '更换显示屏', '', 10, NULL, NULL, NULL, NULL, NULL);
-INSERT INTO `mx_fault` VALUES ('0003PCFF0007', '000300030000', '', '', '', '4', '0003', '0', '', '0', '', '检测剂更换后若仍无SIG的信号变化,认为是信号采集电路故障,更换信号采集电路模块', '', 9, NULL, NULL, NULL, NULL, NULL);
+INSERT INTO `mx_fault` VALUES ('0003PCFF0007', '000300030000', '', '', '', '4', '0003', '0', '', '0', '', '更换信号采集电路模块', '', 9, NULL, NULL, NULL, NULL, NULL);
 INSERT INTO `mx_fault` VALUES ('0003PCFF0008', '000300030000', '', '', '', '4', '0003', '0', '', '0', '', '更换检测剂', '', 8, NULL, NULL, NULL, NULL, NULL);
 INSERT INTO `mx_fault` VALUES ('0003PCFF0009', '000300040000', '', '', '', '4', '0003', '0', '', '0', '', '更换干燥管', '', 7, NULL, NULL, NULL, NULL, NULL);
 INSERT INTO `mx_fault` VALUES ('0003PCFF0010', '000300040000', '', '', '', '4', '0003', '0', '', '0', '', '更换维护管', '', 1, NULL, NULL, NULL, NULL, NULL);

+ 51 - 0
pla-sim/01_SQL/02_table/mx_real_exam_fault.sql

@@ -0,0 +1,51 @@
+/*
+ Navicat Premium Dump SQL
+
+ Source Server         : fhxy-192.168.1.61-polardbx
+ Source Server Type    : MySQL
+ Source Server Version : 80032 (8.0.32-X-Cluster-8.4.19-20241112)
+ Source Host           : 192.168.1.61:4886
+ Source Schema         : pla-chem-sim-dev-2
+
+ Target Server Type    : MySQL
+ Target Server Version : 80032 (8.0.32-X-Cluster-8.4.19-20241112)
+ File Encoding         : 65001
+
+ Date: 20/04/2025 17:38:24
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for mx_real_exam_fault
+-- ----------------------------
+DROP TABLE IF EXISTS `mx_real_exam_fault`;
+CREATE TABLE `mx_real_exam_fault`  (
+  `ref_id` bigint NOT NULL AUTO_INCREMENT COMMENT '关联ID',
+  `exam_id` bigint NOT NULL COMMENT '考试ID',
+  `fault_id` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '故障ID',
+  `ref_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '关联类型:[1]-选择题,[2]-模拟器维修故障',
+  `flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '7' COMMENT '选择状态:[7]-未知,[1]-选中,[0]-没有选中',
+  `ref_state` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '故障ID关联状态:[0]-初始化,[1]-已经清除故障,[2]-故障已经下发~出题值填充,[3]-中间轮询读取~中间答题值填充,[4]-交卷~最终答题值填充',
+  `answer_right` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '答题正确:[0]-初始化,[1]-正确,[2]-错误',
+  `choice_question_value` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '选择题的出题数值/正确答案',
+  `choice_answer_value` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '选择题的答题数值/学员填写答案',
+  `sim_fault_question_value` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '模拟器出题值/电阻代表值',
+  `sim_fault_answer_value` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '模拟器答题值/电阻代表值',
+  `minus` int NULL DEFAULT 0 COMMENT '减分值,计正数',
+  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+  `createtime` int NULL DEFAULT 0,
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+  `updatetime` int NULL DEFAULT 0,
+  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+  `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+  PRIMARY KEY (`ref_id`) USING BTREE,
+  INDEX `exam_id`(`exam_id` ASC) USING BTREE,
+  INDEX `flag`(`flag` ASC) USING BTREE,
+  INDEX `fault_id`(`fault_id` ASC) USING BTREE,
+  INDEX `answer_right`(`answer_right` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 19763 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'sim-考试故障关联表' ROW_FORMAT = DYNAMIC;
+
+SET FOREIGN_KEY_CHECKS = 1;

+ 6 - 6
ruoyi-admin/src/main/resources/application-druid.yml

@@ -8,9 +8,9 @@ spring:
             master:
                 # server-阿里云47服务器内网
                 # url: jdbc:mysql://127.0.0.1:3306/pla-chem-sim-dev-1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                # url: jdbc:mysql://47.104.188.84:65006/pla-chem-sim-dev-1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                # username: sim
-                # password: 6JwWnz6PEXRGYLr3
+                url: jdbc:mysql://47.104.188.84:65006/pla-chem-sim-dev-1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                username: sim
+                password: 6JwWnz6PEXRGYLr3
 
                 # server-
                 # url: jdbc:mysql://192.168.1.40:3306/pla-chem-sim-dev-1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
@@ -19,9 +19,9 @@ spring:
 
                 # server-现场实验室
                 # url: jdbc:mysql://192.168.1.61:4886/pla-chem-sim-dev-1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                url: jdbc:mysql://192.168.1.61:4886/pla-chem-sim-dev-2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                username: root
-                password: 7ZNo#9Arn3DFBN8N
+                # url: jdbc:mysql://192.168.1.61:4886/pla-chem-sim-dev-2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                # username: root
+                # password: 7ZNo#9Arn3DFBN8N
 
                 # url: jdbc:mysql://127.0.0.1:3306/pla-chem-sim-dev-1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                 # username: root

+ 6 - 6
ruoyi-admin/src/main/resources/application.yml

@@ -76,12 +76,12 @@ spring:
     # server-阿里云47服务器内网
     # host: 127.0.0.1
     # host: 192.168.1.40
-    host: 192.168.1.61
+    # host: 192.168.1.61
     # server-阿里云47服务器外网
-    # host: 47.104.188.84
+    host: 47.104.188.84
     # 端口,默认为6379
-    port: 6379
-    # port: 65007
+    # port: 6379
+    port: 65007
     # 数据库索引
     # server-阿里云47服务器内网
     # database: 2
@@ -90,9 +90,9 @@ spring:
     database: 2
     # 密码
     # server-阿里云47
-    # password: Z*eQ8xXK7ryYynFv
+    password: Z*eQ8xXK7ryYynFv
     # server-现场实验室
-    password: x2fs#W3rZ9dZXiMb
+    # password: x2fs#W3rZ9dZXiMb
     # server-李硕红米本机
     # password: redis123456
     # 连接超时时间

+ 2 - 1
ruoyi-sim/src/main/java/com/ruoyi/sim/constant/CommConst.java

@@ -89,7 +89,8 @@ public interface CommConst {
     int RETRY_COUNT_CHECK_ONE_FAULT = 1;
     int RETRY_COUNT_CLEAR_ONE_FAULT = 2;
     int RETRY_COUNT_WRITE_ONE_FAULT = 2;
-    int RETRY_COUNT_READ_ONE_RESISTANCE = 4;
+    int RETRY_COUNT_READ_ONE_RESISTANCE_MIDDLE = 1;
+    int RETRY_COUNT_READ_ONE_RESISTANCE_FINAL = 4;
     int RETRY_COUNT_QUERY_SN_IMPORTANT = 1;
     int RETRY_COUNT_WHICH_SIM_IMPORTANT = 3;
     int RETRY_COUNT_0 = 0;

+ 23 - 0
ruoyi-sim/src/main/java/com/ruoyi/sim/controller/CmdController.java

@@ -0,0 +1,23 @@
+package com.ruoyi.sim.controller;
+
+import com.ruoyi.sim.service.impl.CmdService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/sim/cmd/")
+public class CmdController {
+
+    @Autowired
+    private CmdService cmdService;
+
+    /**
+     *
+     */
+    @GetMapping(value = "/restart")
+    public void restart() {
+        cmdService.restart();
+    }
+}

+ 1 - 1
ruoyi-sim/src/main/java/com/ruoyi/sim/controller/ConfigController.java

@@ -14,7 +14,7 @@ public class ConfigController {
 
     /**
      * http://192.168.1.151:8080/sim/config/set?key=CHECK_REPLACE_EMPTY&value=false
-     * http://192.168.1.60:8080/sim/config/set?key=CHECK_REPLACE_EMPTY&value=false
+     * http://192.168.1.110:8080/sim/config/set?key=CHECK_REPLACE_EMPTY&value=false
      * http://192.168.1.60:8080/sim/config/set?key=SCHEDULED_CONNECT&value=false
      *
      * @param key

+ 3 - 0
ruoyi-sim/src/main/java/com/ruoyi/sim/controller/TestIotController.java

@@ -116,6 +116,9 @@ public class TestIotController extends BaseController {
                 String ip = "192.168.1.201";
                 return commCheckService.checkPingRs485State(ip);
             }
+            case 201: {
+                realExamService.studentMiddleReadRealExam();
+            }
         }
         return AjaxResult.success("ZZZZZZZZZZZZZZZZZZZZ");
     }

+ 1 - 1
ruoyi-sim/src/main/java/com/ruoyi/sim/domain/RealExamCollection.java

@@ -208,7 +208,7 @@ public class RealExamCollection extends BaseEntity {
          */
         String EXERCISE = "1";
         /**
-         * 练习
+         * 练习(自主练习)
          */
         String SELF_EXERCISE = "2";
         /**

+ 2 - 2
ruoyi-sim/src/main/java/com/ruoyi/sim/domain/RealExamFault.java

@@ -217,7 +217,7 @@ public class RealExamFault extends BaseEntity {
      * <p>
      * <p>
      * 选中Yes的故障:0 1 2 3 4
-     * 未选中No:0 1 2 3 4
+     * 未选中No:0 1 4
      * 故障ID关联状态:[0]-初始化,[1]-已经清除故障,[2]-故障已经下发~出题值填充,[3]-轮询读取~中间答题值填充,[4]-交卷~最终答题值填充
      */
     public interface State {
@@ -234,7 +234,7 @@ public class RealExamFault extends BaseEntity {
          */
         String WRITTEN = "2";
         /**
-         * [3]-轮询读取~中间答题值填充
+         * [3]-中间轮询读取~中间答题值填充
          */
         String LOOP_READ = "3";
         /**

+ 18 - 0
ruoyi-sim/src/main/java/com/ruoyi/sim/domain/Seat.java

@@ -1,11 +1,14 @@
 package com.ruoyi.sim.domain;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.ruoyi.sim.domain.vo.SimSocketParamVo;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 import com.ruoyi.common.annotation.Excel;
 import com.ruoyi.common.core.domain.BaseEntity;
 
+import java.util.Date;
+
 /**
  * 座对象 sim_seat
  *
@@ -63,6 +66,12 @@ public class Seat extends BaseEntity {
     @Excel(name = "模拟器的ID:[0]没有连接任何模拟器,[xx]:具体某台模拟器")
     private Long currentSimId;
 
+    /**
+     * 最后一次和模拟器成功连接并通信的时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date lastConnectedTime;
+
     public void setSeatId(Long seatId) {
         this.seatId = seatId;
     }
@@ -127,6 +136,14 @@ public class Seat extends BaseEntity {
         return currentSimId;
     }
 
+    public void setLastConnectedTime(Date lastConnectedTime) {
+        this.lastConnectedTime = lastConnectedTime;
+    }
+
+    public Date getLastConnectedTime() {
+        return lastConnectedTime;
+    }
+
     @Override
     public String toString() {
         return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
@@ -138,6 +155,7 @@ public class Seat extends BaseEntity {
                 .append("seatRs485SocketState", getSeatRs485SocketState())
                 .append("currentUserId", getCurrentUserId())
                 .append("currentSimId", getCurrentSimId())
+                .append("lastConnectedTime", getLastConnectedTime())
                 .append("createBy", getCreateBy())
                 .append("createTime", getCreateTime())
                 .append("updateBy", getUpdateBy())

+ 4 - 0
ruoyi-sim/src/main/java/com/ruoyi/sim/domain/SimMsg.java

@@ -305,6 +305,10 @@ public class SimMsg extends BaseEntity {
          * 接收报文格式不匹配。
          */
         Integer RECEIVE_NOT_MATCH = 530;
+        /**
+         * 重要报文在运行,低优先级的跳过
+         */
+        Integer SKIP = 600;
     }
 
     public SimMsg() {

+ 0 - 1
ruoyi-sim/src/main/java/com/ruoyi/sim/domain/vo/SocketWrapCacheVo.java

@@ -4,7 +4,6 @@ import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 
 import java.net.Socket;
-import java.util.concurrent.atomic.AtomicInteger;
 
 public class SocketWrapCacheVo {
 

+ 41 - 0
ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/CmdService.java

@@ -0,0 +1,41 @@
+package com.ruoyi.sim.service.impl;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+@Service
+public class CmdService {
+
+    private static final Logger l = LoggerFactory.getLogger(CmdService.class);
+
+    /**
+     * java-service [项目名称] [start|stop|restart]
+     * java-service ruoyi-admin restart
+     */
+    public void restart() {
+        // 定义要执行的命令
+        String command = "java-service ruoyi-admin restart";
+        // 使用 ProcessBuilder 执行命令
+        ProcessBuilder processBuilder = new ProcessBuilder(command.split(" "));
+        try {
+            // 启动进程
+            Process process = processBuilder.start();
+            // 获取命令的输出
+            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+            String line;
+            while ((line = reader.readLine()) != null) {
+                l.info("line = {}", line);
+            }
+            // 等待命令执行完成
+            int exitCode = process.waitFor();
+            l.info("命令执行完成,退出码:" + exitCode);
+        } catch (InterruptedException | IOException e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 1 - 1
ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/CommCheckService.java

@@ -257,7 +257,7 @@ public class CommCheckService {
                 default -> throw new IllegalStateException("Unexpected value: " + targetSimType);
             }
         }
-        return AjaxResult.error("失败,检查一个模拟器[" + sim.getSimNum() + "]型号或序列号执行错误!");
+        return AjaxResult.error("失败,检查一个模拟器[" + sim.getSimNum() + "]型号或序列号执行错误!<br/>请检查模拟器线缆连接情况和连接模拟器型号!");
     }
 
     /**

+ 13 - 0
ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/CommReceiveService.java

@@ -118,6 +118,11 @@ public class CommReceiveService {
             if (StringUtils.isNotBlank(refState)) {
                 reF.setRefState(refState);
             }
+            // LOOP_READ 没有成功的结果,跳过数据库更新。
+            if (RealExamFault.State.LOOP_READ.equals(refState) && sm.isResultNotOk()) {
+                l.info("nothing change.");
+                return;
+            }
             reF.setSimFaultAnswerValue(faultAnswerValue);
             realExamFaultService.updateRealExamFault(reF);
         } else {
@@ -149,6 +154,14 @@ public class CommReceiveService {
         if (s == null) {
             return AjaxResult.error("没有对应模拟器!");
         }
+        // 1型 外壳及零件,特殊处理
+        if (StringUtils.equals(s.getSimType(), Sim.TYPE_0001) &&
+                StringUtils.equals(f.getFaultId(), "0001GZBW0009") &&
+                StringUtils.equals(checkValue, "00000002")) {
+            String msg = "故障部位[" + f.getBindHardwareMsg() + "][" + f.getReplaceName() + "]电池仓门被关闭,请保证电池仓门开启;";
+            l.info(msg);
+            return AjaxResult.success(msg, f);
+        }
         // 是否在 故障部位 跳过检查 白名单中。
         if (FaultConst.FAULT_SET_CHECK_PASS.contains(f.getFaultId())) {
             // 跳过检查,直接成功。

+ 0 - 22
ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/CommRunningService.java

@@ -1,22 +0,0 @@
-package com.ruoyi.sim.service.impl;
-
-import org.springframework.stereotype.Service;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
-@Service
-public class CommRunningService {
-
-    /**
-     * 是否有关键指令运行。
-     */
-    private AtomicBoolean keyIsRunning = new AtomicBoolean(false);
-
-    public boolean getKeyIsRunning() {
-        return keyIsRunning.get();
-    }
-
-    public void setKeyIsRunning(Boolean keyIsRunning) {
-        this.keyIsRunning.set(keyIsRunning);
-    }
-}

+ 129 - 38
ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/CommSendService.java

@@ -7,6 +7,7 @@ import com.ruoyi.sim.constant.CommConst;
 import com.ruoyi.sim.domain.*;
 import com.ruoyi.sim.domain.vo.ScanSeatVo;
 import com.ruoyi.sim.domain.vo.SimSocketParamVo;
+import com.ruoyi.sim.util.SimDateUtil;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.LoggerFactory;
 import org.slf4j.Logger;
@@ -61,6 +62,8 @@ public class CommSendService {
     private SimConfig config;
     @Autowired
     private CommStrategy commStrategy;
+    @Autowired
+    private CmdService cmdService;
 
     /**
      * 定时任务。
@@ -97,8 +100,39 @@ public class CommSendService {
         readAll();
     }
 
+    /**
+     * 开始考试以后,交卷之前,定时任务中间读取值,作为答题值。有优先级高的任务可能跳过执行。
+     *
+     * @param re
+     */
+    public void readOneExamAtMiddle(RealExam re) {
+        l.info("readOneExamAtMiddle getExamId = {}", re.getExamId());
+        List<RealExamFault> list = realExamFaultService.listAllType2State2and3ByExamId(re.getExamId());
+        for (RealExamFault ref : list) {
+            {
+                RealExamFault refQ = realExamFaultService.selectRealExamFaultByRefId(ref.getRefId());
+                if (refQ == null || refQ.getExamId() == 0L) {
+                    continue;
+                }
+                if (!RealExamFault.State.WRITTEN.equals(ref.getRefState()) &&
+                        !RealExamFault.State.LOOP_READ.equals(ref.getRefState())) {
+                    continue;
+                }
+            }
+            Seat seat = seatService.selectSeatBySeatId(re.getSeatId());
+            Sim sim = simService.selectSimBySimId(re.getSimId());
+            Fault f = faultService.selectFaultByFaultId(ref.getFaultId());
+            readOneSimOneFaultResistance(seat, sim, ref, f, RealExamFault.State.LOOP_READ);
+        }
+    }
+
+    /**
+     * 交卷最后读取值,作为答题值。
+     *
+     * @param re
+     */
     public void readOneExamAtLast(RealExam re) {
-        l.info("readOneExamAtLast");
+        l.info("readOneExamAtLast getExamId = {}", re.getExamId());
         List<RealExamFault> list = realExamFaultService.listAllType2State2and3ByExamId(re.getExamId());
         for (RealExamFault ref : list) {
             Seat seat = seatService.selectSeatBySeatId(re.getSeatId());
@@ -265,11 +299,36 @@ public class CommSendService {
      * 每天06:00/12:00/18:00/00:00执行
      */
     public void scheduledCloseAllSocket() {
+        l.info("scheduledCloseAllSocket");
         AjaxResult ar = socketService.closeAll();
         l.info("AjaxResult = {}", ar);
     }
 
     /**
+     * 每10min运行一次。
+     */
+    public void scheduledExamMiddleRead() {
+        l.info("scheduledExamMiddleRead");
+        realExamService.studentMiddleReadRealExam();
+    }
+
+    /**
+     * 每30min运行一次。
+     */
+    public void scheduledSystemAutoCleanExam() {
+        l.info("scheduledSystemAutoCleanExam");
+        realExamService.systemAutoCleanExam();
+    }
+
+    /**
+     * 每6hour运行一次。
+     */
+    public void scheduledProjectRestart() {
+        l.info("scheduledProjectRestart");
+        cmdService.restart();
+    }
+
+    /**
      * 主动更新模拟器状态。
      * <p>
      * <p>
@@ -421,7 +480,7 @@ public class CommSendService {
                 return ar;
             }
         }
-        return AjaxResult.success("清除成功,清除所有座次正确连接的模拟器的,所有的故障!");
+        return AjaxResult.success("清除成功,清除所有座次正确连接的模拟器的,所有的故障!" + SimDateUtil.getNow());
     }
 
     /**
@@ -455,7 +514,7 @@ public class CommSendService {
             }
             list.add(sm);
         }
-        return AjaxResult.success("清除成功,清除当前模拟器所有的故障!");
+        return AjaxResult.success("清除成功,清除当前模拟器所有的故障!" + SimDateUtil.getNow());
     }
 
     /**
@@ -592,7 +651,7 @@ public class CommSendService {
         {
             readOneSimAllFaultFirstTimeBySim(seat, sim, faultIds);
         }
-        return AjaxResult.success("下发故障流程执行成功!");
+        return AjaxResult.success("下发故障流程执行成功!" + SimDateUtil.getNow());
     }
 
     /**
@@ -703,9 +762,9 @@ public class CommSendService {
             AjaxResult ar = readOneSimOneFaultCheck(seat, sim, f);
             if (!StringUtils.isBlank((String) ar.get(AjaxResult.MSG_TAG))) {
                 listNG.add(f);
-                l.info("故障部位[" + f.getBindHardwareMsg() + "][" + f.getReplaceName() + "]未正确安装;");
+                l.info("log 故障部位[" + f.getBindHardwareMsg() + "][" + f.getReplaceName() + "]未正确安装;");
             } else {
-                l.info("故障部位[" + f.getBindHardwareMsg() + "][" + f.getReplaceName() + "]安装ok;");
+                l.info("log 故障部位[" + f.getBindHardwareMsg() + "][" + f.getReplaceName() + "]安装ok;");
             }
             if (ar.isError()) {
                 return ar;
@@ -716,7 +775,11 @@ public class CommSendService {
         } else {
             StringBuilder sbNG = new StringBuilder();
             for (Fault f : listNG) {
-                sbNG.append("[" + f.getReplaceName() + "]可换件异常;<br>");
+                if (StringUtils.equals(f.getFaultId(), "0001GZBW0009")) {
+                    sbNG.append("[" + f.getReplaceName() + "]可换件异常;电池仓门被关闭,请确保电池舱门打开!<br>");
+                } else {
+                    sbNG.append("[" + f.getReplaceName() + "]可换件异常;<br>");
+                }
             }
             sbNG.append("请正确安装可换件,检查后重新开始考试!");
             return AjaxResult.error(sbNG.toString());
@@ -822,7 +885,7 @@ public class CommSendService {
      * @param sim
      * @param reF
      * @param f
-     * @param refState 中间轮询是null,交卷最后一次读取为finish状态。用来修改状态。debug模式下执行为null。
+     * @param refState 修改的目标状态。debug模式下执行为null。
      */
     public void readOneSimOneFaultResistance(Seat seat, Sim sim, RealExamFault reF, Fault f, String refState) {
         l.info("readOneSimOneFaultResistance");
@@ -830,16 +893,26 @@ public class CommSendService {
         SimMsg sm2 = null;
         if (reF != null && refState != null) {
             if (RealExamFault.State.FINISH.equals(refState)) { // 是否最后一次读取。
-                sm2 = send(sm1, seat, sim, RETRY_COUNT_READ_ONE_RESISTANCE, commStrategy.getSleepShort());
+                sm2 = send(sm1, seat, sim,
+                        RETRY_COUNT_READ_ONE_RESISTANCE_FINAL, commStrategy.getSleepShort());
+            } else if (RealExamFault.State.LOOP_READ.equals(refState)) { // 是否是中间读取
+                sm2 = send(sm1, seat, sim,
+                        RETRY_COUNT_READ_ONE_RESISTANCE_MIDDLE, commStrategy.getSleepShort(), false);
             } else {
-                sm2 = send(sm1, seat, sim, RETRY_COUNT_0, commStrategy.getSleepShort());
+                sm2 = send(sm1, seat, sim,
+                        RETRY_COUNT_0, commStrategy.getSleepShort());
             }
         } else {
-            sm2 = send(sm1, seat, sim, RETRY_COUNT_READ_ONE_RESISTANCE, commStrategy.getSleepShort());
+            sm2 = send(sm1, seat, sim, RETRY_COUNT_READ_ONE_RESISTANCE_FINAL, commStrategy.getSleepShort());
         }
         simReceiveService.setFaultAnswerValue(sm2, sim, reF, f, refState);
     }
 
+    public SimMsg send(final SimMsg sm, final Seat seat, final Sim sim,
+                       final int retryTotalCount, final long sleep) {
+        return send(sm, seat, sim, retryTotalCount, sleep, true);
+    }
+
     /**
      * 最基本的通信方法。
      * send hex message
@@ -851,51 +924,66 @@ public class CommSendService {
      * @param sim             可以为空!更新最后发送/接收时间 用。
      * @param retryTotalCount 重试次数
      * @param sleep           不使用传入0,不进行挂起。
+     * @param importantTask
      * @return
      */
-    public SimMsg send(final SimMsg sm, final Seat seat, final Sim sim, final int retryTotalCount, final long sleep) {
+    public SimMsg send(final SimMsg sm, final Seat seat, final Sim sim,
+                       final int retryTotalCount, final long sleep, final boolean importantTask) {
+        if (!config.isCommGlobal()) {
+            l.warn("isCommGlobal == false [模拟器通信被禁用!]");
+            return sm;
+        }
+        if (sm == null || sm.getSendMsg() == null || StringUtils.isBlank(sm.getSendMsg())) {
+            throw new IllegalArgumentException("SimMsg IllegalArgument");
+        }
+        // sim
+        if (seat == null) {
+            throw new IllegalArgumentException("seat is null");
+        }
+        if (sleep < 0) {
+            throw new IllegalArgumentException("SimMsg sleep");
+        }
+        // log.
+        {
+            l.info("####发送#### == Seat[{}],SimMsg[{}]", seat, sm);
+        }
+        SimSocketParamVo sspv = seat.toSimSocketParamVo();
         try {
-            if (!config.isCommGlobal()) {
-                l.warn("isCommGlobal == false [模拟器通信被禁用!]");
-                return sm;
-            }
-            if (sm == null || sm.getSendMsg() == null || StringUtils.isBlank(sm.getSendMsg())) {
-                throw new IllegalArgumentException("SimMsg IllegalArgument");
-            }
-            // sim
-            if (seat == null) {
-                throw new IllegalArgumentException("seat is null");
-            }
-            if (sleep < 0) {
-                throw new IllegalArgumentException("SimMsg sleep");
-            }
-            // log.
-            {
-                l.info("####发送#### == Seat[{}],SimMsg[{}]", seat, sm);
-            }
             // 如果没有打开socket,顺道打开。正好后面要sleep。
             // 不强制重开Socket。
             // 先进行Socket相关处理。
-            SimSocketParamVo sspv = seat.toSimSocketParamVo();
+
+            // 优先级高的在运行,跳过
+            if (importantTask == false && socketService.getImportantTaskRunning(sspv)) {
+                sm.setResult(SimMsg.Result.SKIP);
+                socketService.setImportantTaskRunning(sspv, false);
+                l.warn("####跳过运行#### sm = {}", sm);
+                return sm;
+            }
+            if (importantTask) {
+                socketService.setImportantTaskRunning(sspv, true);
+            }
             socketService.openOne(sspv);
             // Socket情况不正确,直接返回。
             if (socketService.isNotOk(sspv)) {
                 sm.setResult(SimMsg.Result.SOCKET_CONNECT_EXCEPTION);
+                socketService.setImportantTaskRunning(sspv, false);
                 return sm;
             }
             {
                 // sleep挂起线程,追求顺序请求。
                 // 大于0才挂起。
-                if (sleep > 0 && socketService.get(sspv).getPreviousSendSleep() > 0L) {
+                if (sleep > 0 && socketService.getVo(sspv).getPreviousSendSleep() > 0L) {
                     // 时间间隔挂起。
-                    Thread.sleep(socketService.get(sspv).getPreviousSendSleep());
+                    Thread.sleep(socketService.getVo(sspv).getPreviousSendSleep());
                 }
             }
-            socketService.get(sspv).setPreviousSendSleep(sleep);
-            Socket socket = socketService.get(sspv).getSocket();
+            socketService.getVo(sspv).setPreviousSendSleep(sleep);
+            Socket socket = socketService.getVo(sspv).getSocket();
             InputStream is = socket.getInputStream();
             OutputStream os = socket.getOutputStream();
-            socket.setSoTimeout(commStrategy.getSoTimeout());
+            Sim simSeat = simService.selectSimBySimId(seat.getCurrentSimId());
+            socket.setSoTimeout(commStrategy.getSoTimeout(simSeat));
             os.write(hexStrToByteArrs(sm.getSendMsg()));
             sm.setSendTime(DateUtils.getNowDate());
             if (sim != null) {
@@ -919,6 +1007,7 @@ public class CommSendService {
                     // todo:
                     l.warn("####接收错误@格式错误#### = sm = {},ar = {}", sm, ar);
                     sm.setResult(SimMsg.Result.RECEIVE_CHECK_FAIL);
+                    socketService.setImportantTaskRunning(sspv, false);
                     return sm;
                 }
             }
@@ -927,6 +1016,7 @@ public class CommSendService {
                 if (ar.isError()) {
                     l.warn("####接收错误@匹配错误#### sm = {},ar = {}", sm, ar);
                     sm.setResult(SimMsg.Result.RECEIVE_NOT_MATCH);
+                    socketService.setImportantTaskRunning(sspv, false);
                     return sm;
                 }
             }
@@ -935,6 +1025,7 @@ public class CommSendService {
                 simService.updateLastReceivedTime(sim);
             }
             sm.setResult(SimMsg.Result.SUCCESS);
+            socketService.setImportantTaskRunning(sspv, false);
             // 最后返回报文实体。
             return sm;
         } catch (InterruptedException | IOException e) {
@@ -942,10 +1033,10 @@ public class CommSendService {
             l.error("SocketTimeoutException");
             e.printStackTrace();
             sm.setResult(SimMsg.Result.READ_TIMEOUT_EXCEPTION);
+            socketService.setImportantTaskRunning(sspv, false);
             if (sim != null) {
                 l.info("fail sim.getSimId() = {}", sim.getSimId());
             }
-            SimSocketParamVo sspv = seat.toSimSocketParamVo();
             // Socket失败计数
             socketService.failedPlus1(sspv);
             if (socketService.failedIsReachedMax(sspv, SocketService.SOCKET_CONNECT_RETRY_COUNT_LIMIT)) {
@@ -1083,6 +1174,6 @@ public class CommSendService {
 
         // 删除debug表中所有数据。
         debugFaultService.deleteAll();
-        return AjaxResult.success("全部重置成功。");
+        return AjaxResult.success("全部重置成功。" + SimDateUtil.getNow());
     }
 }

+ 12 - 2
ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/CommStrategy.java

@@ -1,5 +1,7 @@
 package com.ruoyi.sim.service.impl;
 
+import com.ruoyi.sim.domain.Sim;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.stereotype.Service;
 
 @Service
@@ -49,7 +51,15 @@ public class CommStrategy {
         }
     }
 
-    public int getSoTimeout() {
-        return 4000;
+    public int getSoTimeout(Sim sim) {
+        if (sim == null) {
+            return 4000;
+        }
+        return switch (sim.getSimType()) {
+            case Sim.TYPE_0001 -> 4000;
+            case Sim.TYPE_0002 -> 2000;
+            case Sim.TYPE_0003 -> 2000;
+            default -> 4000;
+        };
     }
 }

+ 11 - 1
ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/RealExamCollectionService.java

@@ -7,7 +7,7 @@ import cn.ele6.catalyzer.ruoyi.vue.custom.Ele6RYBaseService;
 import cn.ele6.catalyzer.ruoyi.vue.enhance.TableDataInfo;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.utils.DateUtils;
-import com.ruoyi.sim.controller.RealExamCollectionController;
+import com.ruoyi.sim.domain.RealExam;
 import com.ruoyi.sim.domain.vo.RealExamCollectionVo;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
@@ -40,6 +40,8 @@ public class RealExamCollectionService extends Ele6RYBaseService {
     private CommSendService commSendService;
     @Autowired
     private SocketService socketService;
+    @Autowired
+    private RealExamService realExamService;
 
     /**
      * 查询考试集合
@@ -390,6 +392,14 @@ public class RealExamCollectionService extends Ele6RYBaseService {
                 return AjaxResult.error("已经关闭!");
             }
         }
+        // 处理没有点击交卷,后续没有任何处理的情况。
+        if (false) {
+            RealExam reQ = new RealExam();
+            reQ.setExamCollectionId(examCollectionId);
+            realExamService.selectRealExamList(reQ).forEach(re -> {
+                realExamService.systemSubmitTimeoutRealExam(re.getExamId());
+            });
+        }
         // todo:是否还有正在进行的考试
         // 修改为socket常开,直接返回成功结果。
         // Step:修改考试集合状态。

+ 140 - 14
ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/RealExamService.java

@@ -1,8 +1,6 @@
 package com.ruoyi.sim.service.impl;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
+import java.util.*;
 
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.utils.DateUtils;
@@ -13,6 +11,7 @@ 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.RandomUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -298,16 +297,22 @@ public class RealExamService {
             if (realExamCollectionService.existOpenedByType(RealExamCollection.Type.EXAM)) {
                 return AjaxResult.error("存在打开的考试,无法开启训练!<br/>请向教员说明情况。");
             }
+        } else {
+            l.info("type EXERCISE,没有打开的考试,校验正确");
         }
         // Check:针对练习(自主练习),进行特殊检查。
         if (StringUtils.equals(RealExamCollection.Type.SELF_EXERCISE, examCollectionType)) {
             // 已经open的考试。
             if (realExamCollectionService.existOpenedByType(RealExamCollection.Type.EXAM)) {
                 return AjaxResult.error("存在打开的考试,无法开启练习!<br/>请向教员说明情况。");
+            } else {
+                l.info("type SELF_EXERCISE,没有打开的考试,校验正确");
             }
             // 已经open的训练。
             if (realExamCollectionService.existOpenedByType(RealExamCollection.Type.EXERCISE)) {
                 return AjaxResult.error("存在打开的训练,无法开启练习!<br/>请向教员说明情况。");
+            } else {
+                l.info("type SELF_EXERCISE,没有打开的训练,校验正确");
             }
         }
         // Check:检查参数examId有效性
@@ -323,32 +328,50 @@ public class RealExamService {
                 StringUtils.equals(re.getExamStatus(), RealExam.State.CALCULATING_SCORE) ||
                 StringUtils.equals(re.getExamStatus(), RealExam.State.GOT_REPORT)) {
             return AjaxResult.error("已经交卷,禁止重复开始考试!");
+        } else {
+            l.info("没有重复交卷校验正确");
         }
         RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
         // Check:考试集合数据有效性。
         if (rec == null) {
             return AjaxResult.error("考试集合数据异常!");
+        } else {
+            l.info("考试集合数据校验正确");
         }
         if (!StringUtils.equals(rec.getExamCollectionState(), RealExamCollection.State.OPENED)) {
-            return AjaxResult.error("教师端对应考试/训练尚未打开!<br/>请向教员说明情况。");
+            if (StringUtils.equals(rec.getExamCollectionType(), RealExamCollection.Type.SELF_EXERCISE)) {
+                l.info("练习类型考试集合,不需要检查 考试集合 开关状态。");
+            } else {
+                return AjaxResult.error("教师端对应考试/训练尚未打开!<br/>请向教员说明情况。");
+            }
+        } else {
+            l.info("考试集合开启校验正确");
         }
         // Check:检查参数examCollectionType有效性
         if (!StringUtils.equals(examCollectionType, rec.getExamCollectionType())) {
             return AjaxResult.error("考试集合类型不对应!");
+        } else {
+            l.info("考试集合类型校验正确");
         }
         // Check:检查参数studentBindIp有效性
         if (StringUtils.isBlank(studentBindIp)) {
             return AjaxResult.error("IP地址无效");
+        } else {
+            l.info("IP地址检验正确");
         }
         Seat seat = seatService.uniqueByBindIp(studentBindIp);
         if (seat == null) {
             return AjaxResult.error("没有IP对应座次数据!");
+        } else {
+            l.info("座次数据检验正确");
         }
         // Check:ping通 路由器。
         {
             AjaxResult ar = commCheckService.checkRouterState(simConfig.getRouterIp());
             if (ar.isError()) {
                 return ar;
+            } else {
+                l.info("局域网通信校验正确");
             }
         }
         // Check:ping通 学员端电脑。
@@ -370,6 +393,7 @@ public class RealExamService {
                 return ar;
             } else {
                 // Ping通不代表在线,Socket连接建立表示在线。
+                l.info("RS485通信校验正确");
             }
         }
         // Check:如果有缓存Socket并且可用,使用缓存Socket,检查并建立Socket连接;否则返回对应错误。
@@ -377,6 +401,8 @@ public class RealExamService {
             AjaxResult ar = socketService.openOne(seat.toSimSocketParamVo());
             if (ar.isError()) {
                 return ar;
+            } else {
+                l.info("Socket校验正确");
             }
         }
         // Check:发送通用询问指令,询问是连接的哪种型号的哪一台模拟器;否则返回对应错误。
@@ -387,6 +413,8 @@ public class RealExamService {
             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));
+            } else {
+                l.info("Who模拟器校验正确");
             }
             // 其他的异常情况。
             if (ar.isError()) {
@@ -421,15 +449,19 @@ public class RealExamService {
             AjaxResult ar = commCheckService.checkOneSimOnlineState(seat.getCurrentSimId());
             if (ar.isError()) {
                 return ar;
+            } else {
+                l.info("模拟器在线校验正确");
             }
         }
         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;
+            } else {
+                l.info("模拟器类型校验正确");
             }
         }
         // Step:可换件检查,读取对应一台模拟器 所有故障部位值。
@@ -438,19 +470,31 @@ public class RealExamService {
             AjaxResult ar = commSendService.readOneSimAllFaultCheck(seat, sim);
             if (ar.isError()) {
                 return ar;
+            } else {
+                l.info("模拟器可换件校验正确");
             }
         }
+        // 虚假的练习清单日志。
+        {
+//            if (rec != null) {
+//                l.info("start exam,exam id = {},sim type = {},使用练习清单task id = {}.",
+//                        re.getExamId(), re.getSimType(), RandomUtils.nextInt(100, 300));
+//            }
+        }
         // Step:清除对应一台模拟器 所有 真实的 故障部位故障。
         {
             commSendService.clearOneSimAllFaultByExam(re);
+            l.info("清除对应一台模拟器 所有 真实的 故障部位故障");
         }
         // Step:下发对应一台模拟器 出题选中的 故障位置故障。
         {
             commSendService.writeOneSimAllSelectFaultByExam(re);
+            l.info("下发对应一台模拟器 出题选中的 故障位置故障");
         }
         // Step:读取对应一台模拟器 所有的 真实的 故障部位 电阻值代表值 作为出题值。
         {
             commSendService.readOneSimAllFaultFirstTimeByExam(re);
+            l.info("读取对应一台模拟器 所有的 真实的 故障部位 电阻值代表值 作为出题值");
         }
         // Step:修改当前exam_id的考试状态。
         // 修改关联状态
@@ -460,6 +504,7 @@ public class RealExamService {
             // 修改真实考试开始时间。
             re.setStartTime(DateUtils.getNowDate());
             updateRealExam(re);
+            l.info("开始考试成功");
             return AjaxResult.success("开始考试成功!");
         } else {
             return AjaxResult.error("开始考试失败,<br/>请重新尝试开始考试!");
@@ -507,6 +552,39 @@ public class RealExamService {
         return AjaxResult.success(vo);
     }
 
+    public void studentMiddleReadRealExam() {
+        l.info("studentMiddleReadRealExam now = {}", new Date());
+        //
+        RealExamCollection rec = realExamCollectionService.selectRealExamCollectionOpened();
+        if (rec == null ||
+                rec.getExamCollectionId() == null ||
+                rec.getExamCollectionId() == 0L ||
+                !StringUtils.equals(rec.getExamCollectionType(), RealExamCollection.Type.EXAM)) {
+            l.info("考试集合不匹配。不需要中间读取,rec = {}", rec);
+            return;
+        }
+        {
+            RealExam reQ = new RealExam();
+            reQ.setExamCollectionId(rec.getExamCollectionId());
+            List<RealExam> reList = selectRealExamList(reQ);
+            for (RealExam re : reList) {
+                // 答题并且不超时的考试,进行中间读取
+                if (
+                        re != null && re.getExamId() != null && re.getExamId() != 0L &&
+                                StringUtils.equals(re.getExamStatus(), RealExam.State.ANSWERING) &&
+                                !checkRealExamIsTimeout(re.getExamId())
+                ) {
+                    commSendService.readOneExamAtMiddle(re);
+                } else {
+                    l.info("skip examId = {}", re != null ? re.getExamId() : null);
+                }
+            }
+        }
+    }
+
+    /**
+     * 10分钟延长时间。
+     */
     public static final Long DURATION_10_MIN = 1000L * 60 * 10;
 
     /**
@@ -541,11 +619,8 @@ public class RealExamService {
         if (StringUtils.equals(re.getExamStatus(), RealExam.State.SUBMITTED)) {
             return AjaxResult.error("已经交卷,禁止重复交卷,<br/>请刷新自动结束考试!");
         }
-        RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
-        // 允许考试时长,毫秒
-        Long millisecondsAllowed = rec.getLimitDuration() * 60 * 1000 + DURATION_10_MIN;
         // Check:已经超时的交卷。
-        if (DateUtils.getNowDate().getTime() > re.getStartTime().getTime() + millisecondsAllowed) {
+        if (checkRealExamIsTimeout(re.getExamId())) {
             // 修改考试状态
             re.setExamStatus(RealExam.State.SUBMITTED);
             // 修改真实考试结束时间。
@@ -553,7 +628,6 @@ public class RealExamService {
             updateRealExam(re);
             return AjaxResult.success("考试时间已经超时,自动结束考试!");
         }
-
         // Check:检查参数studentBindIp有效性
         if (StringUtils.isBlank(studentBindIp)) {
             return AjaxResult.error("IP地址无效!");
@@ -569,7 +643,7 @@ public class RealExamService {
         if (!Objects.equals(seatStart.getSeatId(), seatNow.getSeatId())) {
             return AjaxResult.error("没有在原始座次上交卷,请回到原座次[" + seatStart.getSeatNum() + "]上进行交卷!");
         }
-
+        RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
         // Check:检查参数examCollectionType有效性
         if (!StringUtils.equals(examCollectionType, rec.getExamCollectionType())) {
             return AjaxResult.error("考试集合类型不对应!");
@@ -609,6 +683,14 @@ public class RealExamService {
                 return ar;
             }
         }
+        // Check:检查模拟器类型
+        final String targetSimType = re.getSimType();
+        {
+            AjaxResult ar = commCheckService.checkOneSimType(seatStart, true, targetSimType);
+            if (ar.isError()) {
+                return ar;
+            }
+        }
         // Check:检查是否是出题值使用的模拟器。防止换机器交卷。
         re = selectRealExamByExamId(examId);
         if (!Objects.equals(re.getSimId(), seatNow.getCurrentSimId())) {
@@ -625,9 +707,6 @@ public class RealExamService {
 
         // Check:检查换学生端交卷的情况。
 
-        //
-
-
         // Step:最后读取一下模拟器电阻值。
         commSendService.readOneExamAtLast(re);
         // Step:
@@ -642,6 +721,32 @@ public class RealExamService {
         }
     }
 
+    public void systemSubmitTimeoutRealExam(Long examId) {
+        RealExam re = selectRealExamByExamId(examId);
+        if (re != null &&
+                re.getExamId() != 0L &&
+                RealExam.State.ANSWERING.equals(re.getExamStatus()) &&
+                checkRealExamIsTimeout(re.getExamId())) {
+            re.setExamStatus(RealExam.State.SUBMITTED);
+            updateRealExam(re);
+        }
+    }
+
+    /**
+     * @param examId
+     * @return true 已经超时
+     */
+    public boolean checkRealExamIsTimeout(Long examId) {
+        RealExam re = selectRealExamByExamId(examId);
+        if (re == null || re.getExamId() == 0L) {
+            return false;
+        }
+        RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
+        // 允许考试时长,毫秒
+        Long millisecondsAllowed = rec.getLimitDuration() * 60 * 1000 + DURATION_10_MIN;
+        return DateUtils.getNowDate().getTime() > (re.getStartTime().getTime() + millisecondsAllowed);
+    }
+
     /**
      * [轮询][学生]结束考试界面。
      *
@@ -696,4 +801,25 @@ public class RealExamService {
         }
         return AjaxResult.success("考试数据错误");
     }
+
+    /**
+     * 清除异常考试
+     */
+    public void systemAutoCleanExam() {
+        RealExam reQ = new RealExam();
+        reQ.setExamStatus(RealExam.State.ANSWERING);
+        List<RealExam> list = selectRealExamList(reQ);
+        if (!list.isEmpty()) {
+            for (RealExam re : list) {
+                boolean timeout = checkRealExamIsTimeout(re.getExamId());
+                RealExamCollection rec = realExamCollectionService.selectRealExamCollectionByExamCollectionId(re.getExamCollectionId());
+                if (rec != null &&
+                        StringUtils.equals(rec.getExamCollectionType(), RealExamCollection.Type.SELF_EXERCISE) &&
+                        timeout) {
+                    re.setExamStatus(RealExam.State.SUBMITTED);
+                    updateRealExam(re);
+                }
+            }
+        }
+    }
 }

+ 34 - 11
ruoyi-sim/src/main/java/com/ruoyi/sim/service/impl/SocketService.java

@@ -2,21 +2,19 @@ package com.ruoyi.sim.service.impl;
 
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.sim.config.SimConfig;
-import com.ruoyi.sim.config.SimDebugConfig;
 import com.ruoyi.sim.domain.Seat;
 import com.ruoyi.sim.domain.vo.SimSocketParamVo;
 import com.ruoyi.sim.domain.vo.SocketWrapCacheVo;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.core.parameters.P;
 import org.springframework.stereotype.Service;
 
 import java.io.IOException;
-import java.net.InetAddress;
 import java.net.Socket;
 import java.util.HashMap;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static com.ruoyi.sim.constant.CommConst.SOCKET_TIME_OUT;
@@ -58,13 +56,19 @@ public class SocketService {
      * key: ip:port
      * value:
      */
-    private static HashMap<String, SocketWrapCacheVo> cachedMap = new HashMap<>(INIT_SIZE);
+    private static final HashMap<String, SocketWrapCacheVo> cachedMap = new HashMap<>(INIT_SIZE);
     /**
      * 每个Socket都有。
      * 重试次数。
      * default 0.
      */
-    private static HashMap<String, AtomicInteger> failedMap = new HashMap<>(INIT_SIZE);
+    private static final HashMap<String, AtomicInteger> failedMap = new HashMap<>(INIT_SIZE);
+
+    /**
+     * 是否有重要任务在运行。
+     * 除中间答题值读取外,都为重要任务。
+     */
+    private static final HashMap<String, AtomicBoolean> importantTaskRunningMap = new HashMap<>(INIT_SIZE);
 
     @Autowired
     private SimConfig config;
@@ -77,7 +81,7 @@ public class SocketService {
      */
     public boolean isOk(final SimSocketParamVo sspv) {
         final String key = sspv.toKey();
-        if (cachedMap.containsKey(key) && cachedMap.get(key) != null) {
+        if (cachedMap != null && cachedMap.containsKey(key) && cachedMap.get(key) != null) {
             Socket s = cachedMap.get(key).getSocket();
             if (s != null) {
                 return (s.isConnected() &&
@@ -135,8 +139,8 @@ public class SocketService {
                 // Socket s = new Socket(sspv.getIp(), sspv.getPort(), InetAddress.getLocalHost(), SimDebugConfig.TCP_LOCAL_PORT);
                 s.setSoTimeout(SOCKET_TIME_OUT);
                 SocketWrapCacheVo value = new SocketWrapCacheVo(sspv.getIp(), sspv.getPort(), s, System.currentTimeMillis());
-                // 新建Socket需要挂起 2s后再发指令。
-                value.setPreviousSendSleep(2000L);
+                // 新建Socket需要挂起 500ms后再发指令。
+                value.setPreviousSendSleep(500L);
                 cachedMap.put(key, value);
                 // socket failed count reset.
                 failedReset0(sspv);
@@ -233,18 +237,18 @@ public class SocketService {
      * @param seat
      * @return todo:null
      */
-    public SocketWrapCacheVo get(final Seat seat) {
+    public SocketWrapCacheVo getVo(final Seat seat) {
         if (seat == null) {
             throw new IllegalArgumentException("seat为空。");
         }
-        return get(new SimSocketParamVo(seat.getSeatRs485Ip(), seat.getSeatRs485Port()));
+        return getVo(new SimSocketParamVo(seat.getSeatRs485Ip(), seat.getSeatRs485Port()));
     }
 
     /**
      * @param sspv
      * @return
      */
-    public SocketWrapCacheVo get(final SimSocketParamVo sspv) {
+    public SocketWrapCacheVo getVo(final SimSocketParamVo sspv) {
         if (isNotOk(sspv)) {
             AjaxResult ar = openOne(sspv);
             if (ar.isError()) {
@@ -301,4 +305,23 @@ public class SocketService {
             l.debug("not containsKey SimSocketParamVo sspv:" + sspv);
         }
     }
+
+    public void setImportantTaskRunning(final SimSocketParamVo sspv, final boolean running) {
+        final String key = sspv.toKey();
+        if (!importantTaskRunningMap.containsKey(key)) {
+            importantTaskRunningMap.put(key, new AtomicBoolean(running));
+        } else {
+            importantTaskRunningMap.get(key).set(running);
+        }
+    }
+
+    public boolean getImportantTaskRunning(final SimSocketParamVo sspv) {
+        final String key = sspv.toKey();
+        if (!importantTaskRunningMap.containsKey(key)) {
+            importantTaskRunningMap.put(key, new AtomicBoolean(false));
+            return false;
+        } else {
+            return importantTaskRunningMap.get(key).get();
+        }
+    }
 }

+ 12 - 0
ruoyi-sim/src/main/java/com/ruoyi/sim/util/SimDateUtil.java

@@ -0,0 +1,12 @@
+package com.ruoyi.sim.util;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class SimDateUtil {
+
+    public static String getNow() {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        return sdf.format(new Date());
+    }
+}