15 Commits 8e3412a4c2 ... 1c6a26e979

Tác giả SHA1 Thông báo Ngày
  tom 1c6a26e979 使用47的服务器地址。 8 tháng trước cách đây
  tom 2f15ee2ca8 清除异常考试,1型,外壳及零件,开始考试特殊check。 9 tháng trước cách đây
  tom bc7a71aa58 针对 练习/自主练习 重复开始的修复。 9 tháng trước cách đây
  tom 857da0d0f0 最后一次和模拟器成功连接并通信的时间。添加测试日志。 9 tháng trước cách đây
  tom e9bd58b766 修改定时运行时间。2025-04-22 测试版本。 9 tháng trước cách đây
  tom bec1cae445 每6小时重启一次项目。 9 tháng trước cách đây
  tom b3c1af9160 答题并且不超时的考试,进行中间读取。 9 tháng trước cách đây
  tom cd6352e812 答题并且不超时的考试,进行中间读取。 9 tháng trước cách đây
  tom b41334ee2b 修改文案。 9 tháng trước cách đây
  tom 1ad0716824 修改注释。 9 tháng trước cách đây
  tom 6424be3c33 考试中间读取 15min执行一次。 9 tháng trước cách đây
  tom 1f47aa7ecc 修改文案。 9 tháng trước cách đây
  tom 807c7d1d65 提交 考试中间读取 功能。 9 tháng trước cách đây
  tom 81f607f855 模拟器直连,添加部分执行日期。 10 tháng trước cách đây
  tom 04e48456e6 完善开始考试和交卷模拟器类型检查。 10 tháng trước cách đây
23 tập tin đã thay đổi với 512 bổ sung109 xóa
  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());
+    }
+}