12 Incheckningar

Upphovsman SHA1 Meddelande Datum
  humanleft 470204fa50 docs: update CLAUDE.md with security, MQTT, and lock improvements 3 veckor sedan
  humanleft 0fd6162ee1 refactor: CRITICAL/HIGH fixes — security, concurrency, resource leaks, input validation 3 veckor sedan
  humanleft e6338b3ef4 refactor: P0-P1 complete — field injection, logging, hardcoded values, resource mgmt 4 veckor sedan
  humanleft 28470869a3 test(P1): 新增3个测试类覆盖动态消费者基类与子类 4 veckor sedan
  humanleft 5cbb11d169 chore(P2): 替换mysql驱动,移除emoji统一日志格式 4 veckor sedan
  humanleft 7753e41899 refactor(P1): 字段注入→构造函数注入,扩展配置校验 4 veckor sedan
  humanleft 66e22bd4a7 refactor(P0): 提取 AbstractDynamicMqttConsumer 基类消除400+行重复 4 veckor sedan
  humanleft d0cff8b7f2 fix(P0): 5项安全与稳定性修复 4 veckor sedan
  humanleft c5351f22bf fix: resolve merge conflict in MqttGenericConsumer 4 veckor sedan
  humanleft ef91bae363 test: add TDengineService, MqttStatusConsumer, MqttFaultConsumer, VehicleSyncTask tests 4 veckor sedan
  humanleft 8c8fe4baed refactor: migrate MQTT Consumer thread pools to Spring-managed beans 4 veckor sedan
  humanleft a0d8471a2f refactor: remove legacy RuoYi artifacts and unused dependencies 4 veckor sedan
41 ändrade filer med 2367 tillägg och 1776 borttagningar
  1. 23
    11
      CLAUDE.md
  2. 0
    12
      bin/clean.bat
  3. 0
    12
      bin/package.bat
  4. 0
    14
      bin/run.bat
  5. 32
    14
      iot-platform/pom.xml
  6. 1
    1
      iot-platform/src/main/java/com/iot/platform/common/utils/NumericIdGenerator.java
  7. 0
    10
      iot-platform/src/main/java/com/iot/platform/config/ApplicationConfig.java
  8. 4
    1
      iot-platform/src/main/java/com/iot/platform/config/DruidConfig.java
  9. 32
    0
      iot-platform/src/main/java/com/iot/platform/config/ExecutorConfig.java
  10. 29
    0
      iot-platform/src/main/java/com/iot/platform/config/IotProperties.java
  11. 3
    3
      iot-platform/src/main/java/com/iot/platform/mapper/SysControllerMapper.java
  12. 449
    0
      iot-platform/src/main/java/com/iot/platform/mqtt/AbstractDynamicMqttConsumer.java
  13. 54
    23
      iot-platform/src/main/java/com/iot/platform/mqtt/AbstractMqttConsumer.java
  14. 32
    465
      iot-platform/src/main/java/com/iot/platform/mqtt/MqttChargeStationConsumer.java
  15. 31
    469
      iot-platform/src/main/java/com/iot/platform/mqtt/MqttDynamicConsumer.java
  16. 103
    45
      iot-platform/src/main/java/com/iot/platform/mqtt/MqttFaultConsumer.java
  17. 100
    251
      iot-platform/src/main/java/com/iot/platform/mqtt/MqttGenericConsumer.java
  18. 30
    10
      iot-platform/src/main/java/com/iot/platform/mqtt/MqttStatusConsumer.java
  19. 7
    0
      iot-platform/src/main/java/com/iot/platform/service/SysAlarmService.java
  20. 13
    6
      iot-platform/src/main/java/com/iot/platform/service/SysControllerService.java
  21. 7
    1
      iot-platform/src/main/java/com/iot/platform/service/SysFaultService.java
  22. 5
    1
      iot-platform/src/main/java/com/iot/platform/service/SysIndicatorsService.java
  23. 6
    1
      iot-platform/src/main/java/com/iot/platform/service/SysWorkorderService.java
  24. 50
    50
      iot-platform/src/main/java/com/iot/platform/service/TDegnineAlarm.java
  25. 41
    51
      iot-platform/src/main/java/com/iot/platform/service/TDengineService.java
  26. 112
    76
      iot-platform/src/main/java/com/iot/platform/task/VehicleSyncTask.java
  27. 3
    3
      iot-platform/src/main/resources/application-druid.yml
  28. 1
    0
      iot-platform/src/main/resources/application.yml
  29. 1
    1
      iot-platform/src/main/resources/mapper/SysFaultMapper.xml
  30. 0
    20
      iot-platform/src/main/resources/mybatis-config.xml
  31. 105
    0
      iot-platform/src/test/java/com/iot/platform/mqtt/AbstractDynamicMqttConsumerTest.java
  32. 86
    0
      iot-platform/src/test/java/com/iot/platform/mqtt/MqttChargeStationConsumerTest.java
  33. 138
    0
      iot-platform/src/test/java/com/iot/platform/mqtt/MqttDynamicConsumerTest.java
  34. 225
    0
      iot-platform/src/test/java/com/iot/platform/mqtt/MqttFaultConsumerTest.java
  35. 19
    2
      iot-platform/src/test/java/com/iot/platform/mqtt/MqttGenericConsumerTest.java
  36. 204
    0
      iot-platform/src/test/java/com/iot/platform/mqtt/MqttStatusConsumerTest.java
  37. 200
    0
      iot-platform/src/test/java/com/iot/platform/service/TDengineServiceTest.java
  38. 221
    0
      iot-platform/src/test/java/com/iot/platform/task/VehicleSyncTaskTest.java
  39. 0
    70
      pom.xml
  40. 0
    67
      ry.bat
  41. 0
    86
      ry.sh

+ 23
- 11
CLAUDE.md Visa fil

@@ -90,7 +90,7 @@ DRUID_USERNAME=ruoyi
90 90
 DRUID_PASSWORD=...
91 91
 ```
92 92
 
93
-Spring Boot config uses `${ENV_NAME:default}` syntax for all sensitive values.
93
+Spring Boot config uses `${ENV_NAME:default}` syntax for non-sensitive defaults only. **MySQL and Druid passwords have no fallback defaults** — missing env vars will cause startup failures.
94 94
 
95 95
 ## IoT Data Flow
96 96
 
@@ -103,25 +103,34 @@ MqttGenericConsumer / MqttStatusConsumer / MqttFaultConsumer
103 103
     ├─→ MySQL (sys_controller, sys_device tables)
104 104
     └─→ TDengine (time-series telemetry tables)
105 105
 
106
-VehicleSyncTask (@Scheduled every 30s)
106
+VehicleSyncTask (@Scheduled every 30s, Redis distributed lock)
107 107
     ↓ reads Redis DSB:* keys
108 108
     ├─→ syncs to sysrealtime (MySQL)
109 109
     ├─→ updates vehicle GPS in sys_car (MySQL)
110 110
     ├─→ updates sys_indicators KPIs (MySQL)
111
-    └─→ triggers external webhook
111
+    └─→ triggers external webhook (URL from `iot.mqtt.vehicle-trigger-url` config)
112 112
 ```
113 113
 
114 114
 ## MQTT Consumers
115 115
 
116
-All MQTT consumers extend `AbstractMqttConsumer` which provides:
117
-- Connection management (reconnect, keepalive)
118
-- `checkServerAvailability()` with retry limits
116
+Two base classes handle MQTT connections:
117
+
118
+**`AbstractMqttConsumer`** (single-topic consumers):
119
+- Connection management with `synchronized` reconnect/disconnect
120
+- Broken-state MqttClient detection and recreation on reconnect
119 121
 - Graceful shutdown via `@PreDestroy`
120 122
 
123
+**`AbstractDynamicMqttConsumer`** (dynamic multi-topic consumers):
124
+- Incremental topic subscription refresh (add/remove only changed topics)
125
+- Batch subscribe with retry logic
126
+- Connection health checks before subscribe
127
+
121 128
 Implementations:
122
-- `MqttGenericConsumer` — General telemetry ingestion
123
-- `MqttStatusConsumer` — Device status messages
124
-- `MqttFaultConsumer` — Fault/alarm messages
129
+- `MqttGenericConsumer` — General telemetry ingestion (single-topic)
130
+- `MqttStatusConsumer` — Device status messages (single-topic)
131
+- `MqttFaultConsumer` — Fault/alarm messages (single-topic)
132
+- `MqttDynamicConsumer` — Dynamic topic subscription (extends AbstractDynamicMqttConsumer)
133
+- `MqttChargeStationConsumer` — Charge station telemetry (extends AbstractDynamicMqttConsumer)
125 134
 
126 135
 ## Deployment
127 136
 
@@ -148,7 +157,9 @@ See `deploy/README.md` for full deployment documentation.
148 157
 - Password max retry: 5 attempts, lock time: 10 minutes
149 158
 - XSS filtering enabled
150 159
 - SQL injection prevention: field whitelist + parameterized queries in TDengineService
151
-- Table name validation: regex `^[a-zA-Z_][a-zA-Z0-9_]*$` in SysrealtimeService
160
+- Table name validation: regex `^[a-zA-Z_][a-zA-Z0-9_]*$` in SysControllerService, SysFaultService, SysAlarmService
161
+- Input validation at MQTT consumer boundaries (null/empty checks on controllerId, path, timestamp, deviceId)
162
+- No hardcoded password fallbacks in config files — env vars required at startup
152 163
 
153 164
 ## API Response Format
154 165
 
@@ -184,5 +195,6 @@ curl http://localhost:8887/actuator/health
184 195
 - **MyBatis mappers are Java interfaces** with XML mapping files in `src/main/resources/mapper/`
185 196
 - **Service naming**: Custom IoT services often use concrete classes directly (no `I` prefix)
186 197
 - **TDengine SQL**: Built with string concatenation but protected by field whitelist (`ALLOWED_COLUMNS`) and `escapeValue()`
187
-- **No distributed locks**: `VehicleSyncTask` scheduled tasks may duplicate in clustered environments
198
+- **Distributed locks**: `VehicleSyncTask` uses Redis `SET NX` for per-method distributed locking (30s delay, 60s TTL)
199
+- **TDengine cache bounds**: `stableColumnCache` capped at 1000 entries to prevent unbounded growth
188 200
 - **Alibaba Maven mirror** (`https://maven.aliyun.com/repository/public`) configured in root `pom.xml`

+ 0
- 12
bin/clean.bat Visa fil

@@ -1,12 +0,0 @@
1
-@echo off
2
-echo.
3
-echo [信息] 清理工程target生成路径。
4
-echo.
5
-
6
-%~d0
7
-cd %~dp0
8
-
9
-cd ..
10
-call mvn clean
11
-
12
-pause

+ 0
- 12
bin/package.bat Visa fil

@@ -1,12 +0,0 @@
1
-@echo off
2
-echo.
3
-echo [信息] 打包Web工程,生成war/jar包文件。
4
-echo.
5
-
6
-%~d0
7
-cd %~dp0
8
-
9
-cd ..
10
-call mvn clean package -Dmaven.test.skip=true
11
-
12
-pause

+ 0
- 14
bin/run.bat Visa fil

@@ -1,14 +0,0 @@
1
-@echo off
2
-echo.
3
-echo [信息] 使用Jar命令运行Web工程。
4
-echo.
5
-
6
-cd %~dp0
7
-cd ../ruoyi-admin/target
8
-
9
-set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
10
-
11
-java -jar %JAVA_OPTS% ruoyi-admin.jar
12
-
13
-cd bin
14
-pause

+ 32
- 14
iot-platform/pom.xml Visa fil

@@ -65,8 +65,9 @@
65 65
 
66 66
         <!-- Mysql驱动 -->
67 67
         <dependency>
68
-            <groupId>mysql</groupId>
69
-            <artifactId>mysql-connector-java</artifactId>
68
+            <groupId>com.mysql</groupId>
69
+            <artifactId>mysql-connector-j</artifactId>
70
+            <version>8.0.33</version>
70 71
         </dependency>
71 72
 
72 73
         <!-- TDengine JDBC -->
@@ -95,12 +96,6 @@
95 96
             <artifactId>jackson-databind</artifactId>
96 97
         </dependency>
97 98
 
98
-        <!-- FastJSON2 -->
99
-        <dependency>
100
-            <groupId>com.alibaba.fastjson2</groupId>
101
-            <artifactId>fastjson2</artifactId>
102
-        </dependency>
103
-
104 99
         <!-- Commons -->
105 100
         <dependency>
106 101
             <groupId>org.apache.commons</groupId>
@@ -112,12 +107,6 @@
112 107
             <artifactId>commons-io</artifactId>
113 108
         </dependency>
114 109
 
115
-        <!-- Excel工具 -->
116
-        <dependency>
117
-            <groupId>org.apache.poi</groupId>
118
-            <artifactId>poi-ooxml</artifactId>
119
-        </dependency>
120
-
121 110
         <!-- DevTools -->
122 111
         <dependency>
123 112
             <groupId>org.springframework.boot</groupId>
@@ -172,6 +161,35 @@
172 161
                             <goal>report</goal>
173 162
                         </goals>
174 163
                     </execution>
164
+                    <execution>
165
+                        <id>check</id>
166
+                        <phase>test</phase>
167
+                        <goals>
168
+                            <goal>check</goal>
169
+                        </goals>
170
+                        <configuration>
171
+                            <rules>
172
+                                <rule>
173
+                                    <element>BUNDLE</element>
174
+                                    <limits>
175
+                                        <limit>
176
+                                            <counter>LINE</counter>
177
+                                            <value>COVEREDRATIO</value>
178
+                                            <minimum>0.05</minimum>
179
+                                        </limit>
180
+                                    </limits>
181
+                                </rule>
182
+                            </rules>
183
+                            <excludes>
184
+                                <exclude>com/iot/platform/domain/**</exclude>
185
+                                <exclude>com/iot/platform/domain/vo/**</exclude>
186
+                                <exclude>com/iot/platform/mapper/**</exclude>
187
+                                <exclude>com/iot/platform/common/enums/**</exclude>
188
+                                <exclude>com/iot/platform/config/properties/**</exclude>
189
+                                <exclude>com/iot/platform/datasource/**</exclude>
190
+                            </excludes>
191
+                        </configuration>
192
+                    </execution>
175 193
                 </executions>
176 194
             </plugin>
177 195
         </plugins>

+ 1
- 1
iot-platform/src/main/java/com/iot/platform/common/utils/NumericIdGenerator.java Visa fil

@@ -16,7 +16,7 @@ public class NumericIdGenerator {
16 16
     private long sequence = 0L;
17 17
     private long lastTimestamp = -1L;
18 18
 
19
-    public String nextId() {
19
+    public synchronized String nextId() {
20 20
         long timestamp = timeGen();
21 21
         if (timestamp < lastTimestamp) {
22 22
             throw new RuntimeException("Clock moved backwards!");

+ 0
- 10
iot-platform/src/main/java/com/iot/platform/config/ApplicationConfig.java Visa fil

@@ -6,7 +6,6 @@ import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilde
6 6
 import org.springframework.context.annotation.Bean;
7 7
 import org.springframework.context.annotation.Configuration;
8 8
 import org.springframework.context.annotation.EnableAspectJAutoProxy;
9
-import org.springframework.web.client.RestTemplate;
10 9
 
11 10
 /**
12 11
  * 程序注解配置
@@ -24,13 +23,4 @@ public class ApplicationConfig
24 23
     {
25 24
         return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
26 25
     }
27
-
28
-    /**
29
-     * RestTemplate
30
-     */
31
-    @Bean
32
-    public RestTemplate restTemplate()
33
-    {
34
-        return new RestTemplate();
35
-    }
36 26
 }

+ 4
- 1
iot-platform/src/main/java/com/iot/platform/config/DruidConfig.java Visa fil

@@ -3,6 +3,8 @@ package com.iot.platform.config;
3 3
 import java.util.HashMap;
4 4
 import java.util.Map;
5 5
 import javax.sql.DataSource;
6
+import org.slf4j.Logger;
7
+import org.slf4j.LoggerFactory;
6 8
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
7 9
 import org.springframework.boot.context.properties.ConfigurationProperties;
8 10
 import org.springframework.context.annotation.Bean;
@@ -21,6 +23,7 @@ import com.iot.platform.datasource.DynamicDataSource;
21 23
 @Configuration
22 24
 public class DruidConfig
23 25
 {
26
+    private static final Logger log = LoggerFactory.getLogger(DruidConfig.class);
24 27
     @Bean
25 28
     @ConfigurationProperties("spring.datasource.druid.master")
26 29
     public DataSource masterDataSource(DruidProperties druidProperties)
@@ -57,7 +60,7 @@ public class DruidConfig
57 60
         }
58 61
         catch (Exception e)
59 62
         {
60
-            // slave datasource may not be available
63
+            log.warn("从数据源 {} 不可用: {}", beanName, e.getMessage());
61 64
         }
62 65
     }
63 66
 }

+ 32
- 0
iot-platform/src/main/java/com/iot/platform/config/ExecutorConfig.java Visa fil

@@ -39,4 +39,36 @@ public class ExecutorConfig {
39 39
                 new ThreadPoolExecutor.CallerRunsPolicy()
40 40
         );
41 41
     }
42
+
43
+    @Bean(destroyMethod = "shutdown")
44
+    public ScheduledExecutorService mqttCoreExecutor() {
45
+        return Executors.newSingleThreadScheduledExecutor(r -> {
46
+            Thread t = new Thread(r, "mqtt-core-scheduler");
47
+            t.setDaemon(true);
48
+            return t;
49
+        });
50
+    }
51
+
52
+    @Bean(destroyMethod = "shutdown")
53
+    public ExecutorService mqttWriteExecutor() {
54
+        return new ThreadPoolExecutor(
55
+                4, 8, 60L, TimeUnit.SECONDS,
56
+                new LinkedBlockingQueue<>(5000),
57
+                r -> {
58
+                    Thread t = new Thread(r, "mqtt-write-" + UUID.randomUUID().toString().substring(0, 4));
59
+                    t.setDaemon(true);
60
+                    return t;
61
+                },
62
+                new ThreadPoolExecutor.CallerRunsPolicy()
63
+        );
64
+    }
65
+
66
+    @Bean(destroyMethod = "shutdown")
67
+    public ExecutorService abstractConsumerExecutor() {
68
+        return Executors.newSingleThreadExecutor(r -> {
69
+            Thread t = new Thread(r, "mqtt-abstract-consumer");
70
+            t.setDaemon(true);
71
+            return t;
72
+        });
73
+    }
42 74
 }

+ 29
- 0
iot-platform/src/main/java/com/iot/platform/config/IotProperties.java Visa fil

@@ -20,8 +20,10 @@ public class IotProperties {
20 20
     @PostConstruct
21 21
     public void validate() {
22 22
         List<String> errors = new ArrayList<>();
23
+        if (isBlank(mqtt.getBrokerUrl())) errors.add("iot.mqtt.broker-url 不能为空");
23 24
         if (isBlank(mqtt.getUsername())) errors.add("iot.mqtt.username 不能为空");
24 25
         if (isBlank(mqtt.getPassword())) errors.add("iot.mqtt.password 不能为空");
26
+        if (isBlank(tdengine.getUrl())) errors.add("iot.tdengine.url 不能为空");
25 27
         if (isBlank(tdengine.getUsername())) errors.add("iot.tdengine.username 不能为空");
26 28
         if (isBlank(tdengine.getPassword())) errors.add("iot.tdengine.password 不能为空");
27 29
         if (!errors.isEmpty()) {
@@ -56,6 +58,9 @@ public class IotProperties {
56 58
         private String brokerUrl = "tcp://47.104.204.180:1883";
57 59
         private String username = "";
58 60
         private String password = "";
61
+        private String chargeStationTopic = "station/ChargeStation/device/+/post/json";
62
+        private String alarmWebhookUrl = "https://esos-iot.com:9443/syscar/gaojing";
63
+        private String vehicleTriggerUrl = "https://esos-iot.com:9443/syscar/trigger";
59 64
 
60 65
         public String getBrokerUrl() {
61 66
             return brokerUrl;
@@ -80,6 +85,30 @@ public class IotProperties {
80 85
         public void setPassword(String password) {
81 86
             this.password = password;
82 87
         }
88
+
89
+        public String getChargeStationTopic() {
90
+            return chargeStationTopic;
91
+        }
92
+
93
+        public void setChargeStationTopic(String chargeStationTopic) {
94
+            this.chargeStationTopic = chargeStationTopic;
95
+        }
96
+
97
+        public String getAlarmWebhookUrl() {
98
+            return alarmWebhookUrl;
99
+        }
100
+
101
+        public void setAlarmWebhookUrl(String alarmWebhookUrl) {
102
+            this.alarmWebhookUrl = alarmWebhookUrl;
103
+        }
104
+
105
+        public String getVehicleTriggerUrl() {
106
+            return vehicleTriggerUrl;
107
+        }
108
+
109
+        public void setVehicleTriggerUrl(String vehicleTriggerUrl) {
110
+            this.vehicleTriggerUrl = vehicleTriggerUrl;
111
+        }
83 112
     }
84 113
 
85 114
     /**

+ 3
- 3
iot-platform/src/main/java/com/iot/platform/mapper/SysControllerMapper.java Visa fil

@@ -28,9 +28,9 @@ public interface SysControllerMapper {
28 28
                                     @Param("name")String name,
29 29
                                     @Param("path")String path);
30 30
 
31
-        Integer selectsyscontrollercount(@Param("path")String paht);
32
-        Integer selectsyscontrollercountcmd(@Param("path")String paht);
33
-        Integer selectsyscontrollercountfault(@Param("path")String paht);
31
+        Integer selectsyscontrollercount(@Param("path")String path);
32
+        Integer selectsyscontrollercountcmd(@Param("path")String path);
33
+        Integer selectsyscontrollercountfault(@Param("path")String path);
34 34
 
35 35
 
36 36
         void updatecontrollerAccept(@Param("controllerId")String controllerId,

+ 449
- 0
iot-platform/src/main/java/com/iot/platform/mqtt/AbstractDynamicMqttConsumer.java Visa fil

@@ -0,0 +1,449 @@
1
+package com.iot.platform.mqtt;
2
+
3
+import com.fasterxml.jackson.core.type.TypeReference;
4
+import com.fasterxml.jackson.databind.ObjectMapper;
5
+import com.iot.platform.config.IotProperties;
6
+import org.eclipse.paho.client.mqttv3.*;
7
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
8
+import org.slf4j.Logger;
9
+import org.slf4j.LoggerFactory;
10
+import org.springframework.beans.factory.annotation.Autowired;
11
+
12
+import javax.annotation.PostConstruct;
13
+import javax.annotation.PreDestroy;
14
+import java.net.InetSocketAddress;
15
+import java.net.Socket;
16
+import java.util.*;
17
+import java.util.concurrent.*;
18
+import java.util.concurrent.atomic.AtomicBoolean;
19
+
20
+/**
21
+ * 动态 Topic MQTT 消费者基类。
22
+ * 抽取 MqttDynamicConsumer 与 MqttChargeStationConsumer 的公共连接/订阅管理逻辑,
23
+ * 子类只需实现 {@link #fetchTopics()} 和 {@link #processMessage(String, String)}。
24
+ */
25
+public abstract class AbstractDynamicMqttConsumer {
26
+
27
+    protected final Logger log = LoggerFactory.getLogger(getClass());
28
+
29
+    protected final IotProperties iotProperties;
30
+    protected final ScheduledExecutorService coreExecutor;
31
+    protected final ExecutorService writeExecutor;
32
+
33
+    // MQTT 配置
34
+    protected String brokerUrl;
35
+    protected String brokerHost;
36
+    protected int brokerPort;
37
+    protected String mqttUsername;
38
+    protected String mqttPassword;
39
+
40
+    private static final int QOS = 1;
41
+    private static final int CONNECT_TIMEOUT = 3000;
42
+    private static final int RECONNECT_INTERVAL = 5000;
43
+    private static final int MAX_BATCH_SIZE = 50;
44
+
45
+    protected MqttClient mqttClient;
46
+    private MqttConnectOptions connOpts;
47
+    protected final Set<String> currentTopicSet = new CopyOnWriteArraySet<>();
48
+    protected final AtomicBoolean isConnected = new AtomicBoolean(false);
49
+    protected final Object lock = new Object();
50
+
51
+    protected AbstractDynamicMqttConsumer(IotProperties iotProperties,
52
+                                          ScheduledExecutorService coreExecutor,
53
+                                          ExecutorService writeExecutor) {
54
+        this.iotProperties = iotProperties;
55
+        this.coreExecutor = coreExecutor;
56
+        this.writeExecutor = writeExecutor;
57
+    }
58
+
59
+    /**
60
+     * 子类实现:获取当前需要订阅的 Topic 列表。
61
+     */
62
+    protected abstract List<String> fetchTopics();
63
+
64
+    /**
65
+     * 子类实现:处理收到的 MQTT 消息内容。
66
+     *
67
+     * @param content 消息原始字符串(JSON)
68
+     * @param topic   消息所属 Topic
69
+     */
70
+    protected abstract void processMessage(String content, String topic) throws Exception;
71
+
72
+    @PostConstruct
73
+    public void initMqttConnection() {
74
+        this.brokerUrl = iotProperties.getMqtt().getBrokerUrl();
75
+        String brokerAddr = this.brokerUrl.replace("tcp://", "");
76
+        int colonIdx = brokerAddr.lastIndexOf(':');
77
+        this.brokerHost = brokerAddr.substring(0, colonIdx);
78
+        this.brokerPort = Integer.parseInt(brokerAddr.substring(colonIdx + 1));
79
+        this.mqttUsername = iotProperties.getMqtt().getUsername();
80
+        this.mqttPassword = iotProperties.getMqtt().getPassword();
81
+
82
+        log.info("开始初始化 MQTT 动态订阅服务...");
83
+
84
+        CompletableFuture<Boolean> connectFuture = CompletableFuture.supplyAsync(() -> {
85
+            int initRetry = 0;
86
+            while (initRetry < 3 && !isConnected.get()) {
87
+                try {
88
+                    if (refreshMqttSubscription()) {
89
+                        log.info("MQTT 动态订阅初始化成功");
90
+                        return true;
91
+                    }
92
+                } catch (Exception e) {
93
+                    log.error("MQTT 启动初始化失败(第{}次)", initRetry + 1, e);
94
+                }
95
+                initRetry++;
96
+                try {
97
+                    Thread.sleep(RECONNECT_INTERVAL * initRetry);
98
+                } catch (InterruptedException ex) {
99
+                    Thread.currentThread().interrupt();
100
+                    break;
101
+                }
102
+            }
103
+            return false;
104
+        }, coreExecutor);
105
+
106
+        try {
107
+            Boolean connected = connectFuture.get(10, TimeUnit.SECONDS);
108
+            if (!connected) {
109
+                log.error("MQTT 启动失败(超时),触发后台重连机制");
110
+                triggerReconnect();
111
+            }
112
+        } catch (TimeoutException e) {
113
+            log.error("MQTT 初始化超时(10秒),触发后台重连机制");
114
+            triggerReconnect();
115
+        } catch (InterruptedException e) {
116
+            Thread.currentThread().interrupt();
117
+            log.error("MQTT 初始化被中断");
118
+            triggerReconnect();
119
+        } catch (ExecutionException e) {
120
+            log.error("MQTT 初始化执行异常", e.getCause());
121
+            triggerReconnect();
122
+        }
123
+    }
124
+
125
+    private void initMqttConnectOptions() {
126
+        if (connOpts != null) return;
127
+        connOpts = new MqttConnectOptions();
128
+        connOpts.setCleanSession(false);
129
+        connOpts.setAutomaticReconnect(true);
130
+        connOpts.setConnectionTimeout(10);
131
+        connOpts.setKeepAliveInterval(60);
132
+        connOpts.setMaxInflight(10);
133
+        connOpts.setMaxReconnectDelay(30);
134
+        connOpts.setUserName(mqttUsername);
135
+        connOpts.setPassword(mqttPassword.toCharArray());
136
+    }
137
+
138
+    public boolean refreshMqttSubscription() {
139
+        synchronized (lock) {
140
+            try {
141
+                initMqttConnectOptions();
142
+                List<String> latestTopicList = fetchTopics();
143
+                log.info("查询到 Topic 列表: {}", latestTopicList);
144
+
145
+                if (latestTopicList == null || latestTopicList.isEmpty()) {
146
+                    log.error("未查询到 Topic,取消所有订阅");
147
+                    unsubscribeAll();
148
+                    currentTopicSet.clear();
149
+                    return false;
150
+                }
151
+
152
+                Set<String> latestTopicSet = new HashSet<>();
153
+                for (String topic : latestTopicList) {
154
+                    if (topic != null && !topic.trim().isEmpty()) {
155
+                        latestTopicSet.add(topic.trim());
156
+                    }
157
+                }
158
+
159
+                if (latestTopicSet.equals(currentTopicSet)) {
160
+                    log.info("Topic 列表无变化,无需刷新订阅关系");
161
+                    if (!isConnected.get() && !latestTopicSet.isEmpty()) {
162
+                        return connectAndSubscribe(latestTopicSet);
163
+                    }
164
+                    return true;
165
+                }
166
+
167
+                Set<String> topicsToAdd = new HashSet<>(latestTopicSet);
168
+                topicsToAdd.removeAll(currentTopicSet);
169
+                Set<String> topicsToRemove = new HashSet<>(currentTopicSet);
170
+                topicsToRemove.removeAll(latestTopicSet);
171
+
172
+                if (!isConnected.get()) {
173
+                    return connectAndSubscribe(latestTopicSet);
174
+                } else {
175
+                    if (!topicsToRemove.isEmpty()) unsubscribeTopics(topicsToRemove);
176
+                    if (!topicsToAdd.isEmpty()) subscribeTopics(topicsToAdd);
177
+                }
178
+
179
+                currentTopicSet.clear();
180
+                currentTopicSet.addAll(latestTopicSet);
181
+                log.info("MQTT 订阅刷新完成,当前数量:{}", currentTopicSet.size());
182
+                return true;
183
+
184
+            } catch (Exception e) {
185
+                log.error("MQTT 订阅刷新失败", e);
186
+                return false;
187
+            }
188
+        }
189
+    }
190
+
191
+    private boolean connectAndSubscribe(Set<String> topicSet) {
192
+        synchronized (lock) {
193
+            if (isConnected.get() && mqttClient != null && mqttClient.isConnected()) {
194
+                return true;
195
+            }
196
+            try {
197
+                if (!checkServerAvailability()) {
198
+                    log.error("Broker 不可达,连接失败");
199
+                    return false;
200
+                }
201
+
202
+                if (mqttClient == null || !mqttClient.isConnected()) {
203
+                    if (mqttClient != null) {
204
+                        try {
205
+                            mqttClient.close();
206
+                        } catch (MqttException ignored) {
207
+                        }
208
+                    }
209
+                    String clientId = generateUniqueClientId();
210
+                    MemoryPersistence persistence = new MemoryPersistence();
211
+                    mqttClient = new MqttClient(brokerUrl, clientId, persistence);
212
+                    setMqttCallback();
213
+                }
214
+
215
+                int connectRetry = 0;
216
+                while (connectRetry < 3 && !mqttClient.isConnected()) {
217
+                    try {
218
+                        mqttClient.connect(connOpts);
219
+                        if (mqttClient.isConnected()) {
220
+                            isConnected.set(true);
221
+                            log.info("MQTT 连接成功,客户端 ID:{}", mqttClient.getClientId());
222
+                            break;
223
+                        }
224
+                    } catch (MqttException e) {
225
+                        log.error("MQTT 连接失败(第{}次):{}", connectRetry + 1, e.getMessage());
226
+                        connectRetry++;
227
+                        if (connectRetry < 3) Thread.sleep(RECONNECT_INTERVAL * (connectRetry + 1));
228
+                    }
229
+                }
230
+
231
+                if (!mqttClient.isConnected()) {
232
+                    isConnected.set(false);
233
+                    return false;
234
+                }
235
+
236
+                if (!topicSet.isEmpty()) {
237
+                    List<String> topicList = new ArrayList<>(topicSet);
238
+                    for (int i = 0; i < topicList.size(); i += MAX_BATCH_SIZE) {
239
+                        int end = Math.min(i + MAX_BATCH_SIZE, topicList.size());
240
+                        List<String> batch = topicList.subList(i, end);
241
+                        batchSubscribeTopics(new HashSet<>(batch));
242
+                        Thread.sleep(100);
243
+                    }
244
+                }
245
+                return true;
246
+
247
+            } catch (MqttException e) {
248
+                log.error("MQTT 连接 + 订阅失败", e);
249
+                isConnected.set(false);
250
+                triggerReconnect();
251
+                return false;
252
+            } catch (InterruptedException e) {
253
+                Thread.currentThread().interrupt();
254
+                log.error("MQTT 连接 + 订阅被中断");
255
+                isConnected.set(false);
256
+                return false;
257
+            }
258
+        }
259
+    }
260
+
261
+    private void batchSubscribeTopics(Set<String> topicSet) {
262
+        if (topicSet.isEmpty() || !isConnected.get() || mqttClient == null) return;
263
+        try {
264
+            List<String> topicList = new ArrayList<>(topicSet);
265
+            String[] topics = topicList.toArray(new String[0]);
266
+            int[] qosArr = new int[topics.length];
267
+            Arrays.fill(qosArr, QOS);
268
+            mqttClient.subscribe(topics, qosArr);
269
+            log.info("订阅完成:{} 个 Topic", topicList.size());
270
+        } catch (MqttException e) {
271
+            log.error("批量订阅失败:{}", e.getMessage());
272
+            retrySubscribeBatch(new ArrayList<>(topicSet), 1);
273
+        }
274
+    }
275
+
276
+    private void retrySubscribeBatch(List<String> batchTopics, int retryTimes) {
277
+        for (int retry = 1; retry <= retryTimes; retry++) {
278
+            try {
279
+                Thread.sleep(1000 * retry);
280
+                if (!isConnected.get() || mqttClient == null) continue;
281
+                String[] topics = batchTopics.toArray(new String[0]);
282
+                int[] qosArr = new int[topics.length];
283
+                Arrays.fill(qosArr, QOS);
284
+                mqttClient.subscribe(topics, qosArr);
285
+                log.info("重试订阅成功(第{}次),数量:{}", retry, batchTopics.size());
286
+                return;
287
+            } catch (MqttException e) {
288
+                log.error("重试订阅失败(第{}次):{}", retry, e.getMessage());
289
+            } catch (InterruptedException e) {
290
+                Thread.currentThread().interrupt();
291
+                log.error("重试订阅被中断");
292
+                break;
293
+            }
294
+        }
295
+        log.error("批次订阅最终失败:{}", batchTopics);
296
+    }
297
+
298
+    private void subscribeTopics(Set<String> topicsToAdd) {
299
+        if (topicsToAdd.isEmpty() || !isConnected.get()) return;
300
+        batchSubscribeTopics(topicsToAdd);
301
+    }
302
+
303
+    private void unsubscribeTopics(Set<String> topicsToRemove) {
304
+        if (topicsToRemove.isEmpty() || !isConnected.get() || mqttClient == null) return;
305
+        try {
306
+            String[] topics = topicsToRemove.toArray(new String[0]);
307
+            mqttClient.unsubscribe(topics);
308
+            log.info("取消订阅完成,数量:{}", topicsToRemove.size());
309
+        } catch (MqttException e) {
310
+            log.error("取消订阅失败:{}", e.getMessage());
311
+        }
312
+    }
313
+
314
+    private void unsubscribeAll() {
315
+        if (currentTopicSet.isEmpty() || !isConnected.get() || mqttClient == null) return;
316
+        try {
317
+            String[] topics = currentTopicSet.toArray(new String[0]);
318
+            mqttClient.unsubscribe(topics);
319
+            log.info("取消所有订阅,数量:{}", currentTopicSet.size());
320
+        } catch (MqttException e) {
321
+            log.error("取消所有订阅失败:{}", e.getMessage());
322
+        }
323
+    }
324
+
325
+    private void triggerReconnect() {
326
+        coreExecutor.schedule(() -> {
327
+            int maxAttempts = 3;
328
+            int attempt = 1;
329
+            while (attempt <= maxAttempts && !isConnected.get()) {
330
+                try {
331
+                    log.info("MQTT 重连(第{}次)", attempt);
332
+                    if (connectAndSubscribe(currentTopicSet)) {
333
+                        log.info("MQTT 重连成功");
334
+                        break;
335
+                    }
336
+                    attempt++;
337
+                    if (attempt <= maxAttempts) Thread.sleep(RECONNECT_INTERVAL * 2 * attempt);
338
+                } catch (InterruptedException e) {
339
+                    Thread.currentThread().interrupt();
340
+                    break;
341
+                } catch (Exception e) {
342
+                    log.error("MQTT 重连失败(第{}次):{}", attempt, e.getMessage());
343
+                    attempt++;
344
+                }
345
+            }
346
+            if (attempt > maxAttempts) {
347
+                log.error("MQTT 重连达最大次数,停止尝试");
348
+                isConnected.set(false);
349
+            }
350
+        }, 5, TimeUnit.SECONDS);
351
+    }
352
+
353
+    private boolean checkServerAvailability() {
354
+        try (Socket socket = new Socket()) {
355
+            socket.setSoTimeout(CONNECT_TIMEOUT);
356
+            socket.setTcpNoDelay(true);
357
+            socket.connect(new InetSocketAddress(brokerHost, brokerPort), CONNECT_TIMEOUT);
358
+            return true;
359
+        } catch (Exception e) {
360
+            log.error("MQTT Broker 不可达:{}", e.getMessage());
361
+            return false;
362
+        }
363
+    }
364
+
365
+    private String generateUniqueClientId() {
366
+        String osPrefix = System.getProperty("os.name").toLowerCase().contains("windows") ? "mqtt_win_" : "mqtt_linux_";
367
+        return osPrefix + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8);
368
+    }
369
+
370
+    private void setMqttCallback() {
371
+        if (mqttClient == null) return;
372
+        mqttClient.setCallback(new MqttCallback() {
373
+            @Override
374
+            public void connectionLost(Throwable cause) {
375
+                log.error("MQTT 连接断开:{}", cause.getMessage());
376
+                isConnected.set(false);
377
+                coreExecutor.schedule(AbstractDynamicMqttConsumer.this::triggerReconnect, 5, TimeUnit.SECONDS);
378
+            }
379
+
380
+            @Override
381
+            public void messageArrived(String topic, MqttMessage message) {
382
+                writeExecutor.submit(() -> {
383
+                    try {
384
+                        String content = new String(message.getPayload(), java.nio.charset.StandardCharsets.UTF_8);
385
+                        if (content == null || content.trim().isEmpty()) return;
386
+                        processMessage(content, topic);
387
+                    } catch (Exception e) {
388
+                        log.error("MQTT 消息处理失败 (Topic: {}): {}", topic, e.getMessage(), e);
389
+                    }
390
+                });
391
+            }
392
+
393
+            @Override
394
+            public void deliveryComplete(IMqttDeliveryToken token) {
395
+                try {
396
+                    token.waitForCompletion(1000);
397
+                } catch (MqttException e) {
398
+                    log.warn("MQTT 消息投递确认等待超时: {}", e.getMessage());
399
+                }
400
+            }
401
+        });
402
+    }
403
+
404
+    protected Map<String, Object> deepCopyMap(Map<String, Object> original) {
405
+        if (original == null) return new HashMap<>();
406
+        try {
407
+            ObjectMapper mapper = new ObjectMapper();
408
+            String json = mapper.writeValueAsString(original);
409
+            return mapper.readValue(json, new TypeReference<Map<String, Object>>() {
410
+            });
411
+        } catch (Exception e) {
412
+            Map<String, Object> copy = new HashMap<>(original.size());
413
+            original.forEach((k, v) -> {
414
+                if (v instanceof Map) {
415
+                    copy.put(k, deepCopyMap((Map<String, Object>) v));
416
+                } else {
417
+                    copy.put(k, v);
418
+                }
419
+            });
420
+            return copy;
421
+        }
422
+    }
423
+
424
+    public void disconnect() {
425
+        synchronized (lock) {
426
+            if (mqttClient != null) {
427
+                try {
428
+                    if (mqttClient.isConnected()) {
429
+                        unsubscribeAll();
430
+                        mqttClient.disconnect(10000);
431
+                    }
432
+                    mqttClient.close();
433
+                    log.info("MQTT 连接已断开");
434
+                } catch (MqttException e) {
435
+                    log.error("断开 MQTT 连接失败:{}", e.getMessage());
436
+                } finally {
437
+                    isConnected.set(false);
438
+                    mqttClient = null;
439
+                }
440
+            }
441
+        }
442
+    }
443
+
444
+    @PreDestroy
445
+    public void destroy() {
446
+        log.info("服务正在关闭...");
447
+        disconnect();
448
+    }
449
+}

+ 54
- 23
iot-platform/src/main/java/com/iot/platform/mqtt/AbstractMqttConsumer.java Visa fil

@@ -12,14 +12,13 @@ import javax.annotation.PreDestroy;
12 12
 import java.net.InetSocketAddress;
13 13
 import java.net.Socket;
14 14
 import java.util.concurrent.ExecutorService;
15
-import java.util.concurrent.Executors;
16 15
 
17 16
 public abstract class AbstractMqttConsumer {
18 17
 
19 18
     protected final Logger log = LoggerFactory.getLogger(getClass());
20 19
 
21
-    @Autowired
22
-    protected IotProperties iotProperties;
20
+    protected final IotProperties iotProperties;
21
+    protected final ExecutorService executorService;
23 22
 
24 23
     private String brokerUrl;
25 24
     private String brokerHost;
@@ -35,7 +34,11 @@ public abstract class AbstractMqttConsumer {
35 34
     protected MqttClient mqttClient;
36 35
     protected MqttConnectOptions connOpts;
37 36
     protected volatile boolean isMqttConnected = false;
38
-    protected final ExecutorService executorService = Executors.newSingleThreadExecutor();
37
+
38
+    protected AbstractMqttConsumer(ExecutorService executorService, IotProperties iotProperties) {
39
+        this.executorService = executorService;
40
+        this.iotProperties = iotProperties;
41
+    }
39 42
 
40 43
     protected abstract String getSubscribeTopic();
41 44
 
@@ -52,6 +55,9 @@ public abstract class AbstractMqttConsumer {
52 55
         this.brokerUrl = iotProperties.getMqtt().getBrokerUrl();
53 56
         String brokerAddr = this.brokerUrl.replace("tcp://", "");
54 57
         int colonIdx = brokerAddr.lastIndexOf(':');
58
+        if (colonIdx <= 0 || colonIdx == brokerAddr.length() - 1) {
59
+            throw new IllegalArgumentException("MQTT broker-url 格式无效,期望 tcp://host:port,实际: " + this.brokerUrl);
60
+        }
55 61
         this.brokerHost = brokerAddr.substring(0, colonIdx);
56 62
         this.brokerPort = Integer.parseInt(brokerAddr.substring(colonIdx + 1));
57 63
         this.mqttUsername = iotProperties.getMqtt().getUsername();
@@ -64,8 +70,11 @@ public abstract class AbstractMqttConsumer {
64 70
             initMqttConnectOptions();
65 71
             setMqttCallback();
66 72
             connectAndSubscribeTopic();
67
-        } catch (MqttException | InterruptedException e) {
68
-            log.error("MQTT客户端初始化失败:", e);
73
+        } catch (InterruptedException e) {
74
+            Thread.currentThread().interrupt();
75
+            throw new IllegalStateException("MQTT初始化被中断", e);
76
+        } catch (MqttException e) {
77
+            throw new IllegalStateException("MQTT客户端初始化失败", e);
69 78
         }
70 79
     }
71 80
 
@@ -95,6 +104,9 @@ public abstract class AbstractMqttConsumer {
95 104
         connOpts.setAutomaticReconnect(true);
96 105
         connOpts.setConnectionTimeout(10);
97 106
         connOpts.setUserName(mqttUsername);
107
+        if (mqttPassword == null) {
108
+            mqttPassword = "";
109
+        }
98 110
         connOpts.setPassword(mqttPassword.toCharArray());
99 111
     }
100 112
 
@@ -102,7 +114,7 @@ public abstract class AbstractMqttConsumer {
102 114
         mqttClient.setCallback(new MqttCallback() {
103 115
             @Override
104 116
             public void connectionLost(Throwable cause) {
105
-                log.error("MQTT连接断开,开始重连:" + cause.getMessage());
117
+                log.error("MQTT连接断开,开始重连: {}", cause.getMessage(), cause);
106 118
                 isMqttConnected = false;
107 119
                 reconnect();
108 120
             }
@@ -134,25 +146,35 @@ public abstract class AbstractMqttConsumer {
134 146
             if (connectToken.isComplete()) {
135 147
                 mqttClient.subscribe(getSubscribeTopic(), QOS);
136 148
                 isMqttConnected = true;
137
-                log.info("MQTT连接成功,已订阅主题:" + getSubscribeTopic());
149
+                log.info("MQTT连接成功,已订阅主题: {}", getSubscribeTopic());
138 150
             }
139 151
         }
140 152
     }
141 153
 
142
-    public void reconnect() {
154
+    public synchronized void reconnect() {
143 155
         int maxReconnectAttempts = 3;
144 156
         for (int attempt = 1; attempt <= maxReconnectAttempts; attempt++) {
145 157
             try {
146 158
                 Thread.sleep(RECONNECT_INTERVAL);
147
-                if (mqttClient != null && !mqttClient.isConnected()) {
148
-                    mqttClient.connect(connOpts);
149
-                    mqttClient.subscribe(getSubscribeTopic(), QOS);
150
-                    isMqttConnected = true;
151
-                    log.info("MQTT重连成功(第" + attempt + "次尝试)");
152
-                    break;
159
+                if (mqttClient == null || !mqttClient.isConnected()) {
160
+                    if (mqttClient != null) {
161
+                        try {
162
+                            mqttClient.close();
163
+                        } catch (MqttException ignored) {
164
+                        }
165
+                    }
166
+                    String clientId = generateClientId();
167
+                    mqttClient = new MqttClient(brokerUrl, clientId, new MemoryPersistence());
168
+                    initMqttConnectOptions();
169
+                    setMqttCallback();
153 170
                 }
171
+                mqttClient.connect(connOpts);
172
+                mqttClient.subscribe(getSubscribeTopic(), QOS);
173
+                isMqttConnected = true;
174
+                log.info("MQTT重连成功(第{}次尝试)", attempt);
175
+                break;
154 176
             } catch (MqttException | InterruptedException e) {
155
-                log.error("MQTT重连失败(第" + attempt + "次尝试):" + e.getMessage());
177
+                log.error("MQTT重连失败(第{}次尝试): {}", attempt, e.getMessage());
156 178
                 if (attempt == maxReconnectAttempts) {
157 179
                     log.error("已达最大重连次数,停止重连");
158 180
                 }
@@ -161,17 +183,26 @@ public abstract class AbstractMqttConsumer {
161 183
     }
162 184
 
163 185
     @PreDestroy
164
-    public void disconnect() {
186
+    public synchronized void disconnect() {
165 187
         try {
166
-            if (mqttClient != null && mqttClient.isConnected()) {
167
-                mqttClient.disconnect();
168
-                mqttClient.close();
188
+            if (mqttClient != null) {
189
+                if (mqttClient.isConnected()) {
190
+                    try {
191
+                        mqttClient.disconnect();
192
+                    } catch (MqttException e) {
193
+                        log.error("MQTT断开连接失败: {}", e.getMessage());
194
+                    }
195
+                }
196
+                try {
197
+                    mqttClient.close();
198
+                } catch (MqttException e) {
199
+                    log.error("MQTT客户端关闭失败: {}", e.getMessage());
200
+                }
169 201
                 log.info("MQTT连接已断开");
170 202
             }
171
-            executorService.shutdown();
172 203
             onDestroy();
173
-        } catch (MqttException e) {
174
-            log.error("MQTT断开连接失败:", e);
204
+        } catch (Exception e) {
205
+            log.error("MQTT资源释放异常: {}", e.getMessage(), e);
175 206
         }
176 207
     }
177 208
 }

+ 32
- 465
iot-platform/src/main/java/com/iot/platform/mqtt/MqttChargeStationConsumer.java Visa fil

@@ -1,506 +1,73 @@
1 1
 package com.iot.platform.mqtt;
2
+
2 3
 import com.fasterxml.jackson.core.type.TypeReference;
3 4
 import com.fasterxml.jackson.databind.ObjectMapper;
4
-import com.iot.platform.domain.SysController;
5
-import com.iot.platform.service.*;
6
-import org.eclipse.paho.client.mqttv3.*;
7
-import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
8 5
 import com.iot.platform.config.IotProperties;
6
+import com.iot.platform.service.TDengineService;
9 7
 import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.beans.factory.annotation.Qualifier;
10 9
 import org.springframework.context.annotation.DependsOn;
11
-import org.springframework.data.redis.core.StringRedisTemplate;
12 10
 import org.springframework.stereotype.Component;
13
-import javax.annotation.PostConstruct;
14
-import javax.annotation.PreDestroy;
15
-import java.io.IOException;
16
-import java.net.InetSocketAddress;
17
-import java.net.Socket;
18
-import java.nio.charset.StandardCharsets;
11
+
19 12
 import java.time.LocalDate;
20
-import java.time.LocalDateTime;
21
-import java.time.format.DateTimeFormatter;
22 13
 import java.util.*;
23
-import java.util.concurrent.*;
24
-import java.util.concurrent.atomic.AtomicBoolean;
25
-import org.slf4j.Logger;
26
-import org.slf4j.LoggerFactory;
14
+import java.util.concurrent.ExecutorService;
15
+import java.util.concurrent.ScheduledExecutorService;
27 16
 
28
-/**
29
- * TDegine
30
- * 添加接驳庄数据到TDeinge
31
- */
17
+@SuppressWarnings("unchecked")
32 18
 @Component
33
-public class MqttChargeStationConsumer {
34
-
35
-    private static final Logger log = LoggerFactory.getLogger(MqttChargeStationConsumer.class);
19
+@DependsOn({"tdengineService"})
20
+public class MqttChargeStationConsumer extends AbstractDynamicMqttConsumer {
36 21
 
37
-    @Autowired
38
-    private TDengineService tdengineService;
22
+    private final TDengineService tdengineService;
39 23
     private final ObjectMapper objectMapper = new ObjectMapper();
40
-    @Autowired
41
-    private IotProperties iotProperties;
42
-
43
-    // MQTT 配置
44
-    private String brokerUrl;
45
-    private String brokerHost;
46
-    private int brokerPort;
47
-    private static final int QOS = 1;
48
-    private static final int CONNECT_TIMEOUT = 3000;
49
-    private static final int RECONNECT_INTERVAL = 5000;
50
-    private static final int MAX_BATCH_SIZE = 50;
51
-    private String mqttUsername;
52
-    private String mqttPassword;
53
-    private MqttClient mqttClient;
54
-    private MqttConnectOptions connOpts;
55
-    private final Set<String> currentTopicSet = new CopyOnWriteArraySet<>();
56
-    private final AtomicBoolean isConnected = new AtomicBoolean(false);
57
-    private final Object lock = new Object();
58
-
59
-    // 核心调度线程池
60
-    private final ScheduledExecutorService coreExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
61
-        Thread t = new Thread(r);
62
-        t.setName("mqtt-core-scheduler");
63
-        t.setDaemon(false); // ← 改这里!
64
-        t.setPriority(Thread.NORM_PRIORITY - 1);
65
-        return t;
66
-    });
67
-
68
-    // TDengine 写入线程池
69
-    private final ExecutorService writeExecutor = new ThreadPoolExecutor(
70
-            4, 8, 60L, TimeUnit.SECONDS,
71
-            new LinkedBlockingQueue<>(5000),
72
-            r -> {
73
-                Thread t = new Thread(r);
74
-                t.setName("td-write-worker-" + UUID.randomUUID().toString().substring(0, 4));
75
-                t.setDaemon(false); // ← 改这里!
76
-                return t;
77
-            },
78
-            new ThreadPoolExecutor.CallerRunsPolicy()
79
-    );
80
-
81
-    @PostConstruct
82
-    @DependsOn({"sysControllerService", "tdengineService"})
83
-    public void initMqttConnection() {
84
-        this.brokerUrl = iotProperties.getMqtt().getBrokerUrl();
85
-        String brokerAddr = this.brokerUrl.replace("tcp://", "");
86
-        int colonIdx = brokerAddr.lastIndexOf(':');
87
-        this.brokerHost = brokerAddr.substring(0, colonIdx);
88
-        this.brokerPort = Integer.parseInt(brokerAddr.substring(colonIdx + 1));
89
-        this.mqttUsername = iotProperties.getMqtt().getUsername();
90
-        this.mqttPassword = iotProperties.getMqtt().getPassword();
91
-
92
-        log.info(">>> 开始初始化 MQTT 服务...");
93
-
94
-        CompletableFuture<Boolean> connectFuture = CompletableFuture.supplyAsync(() -> {
95
-            int initRetry = 0;
96
-            while (initRetry < 3 && !isConnected.get()) {
97
-                try {
98
-                    if (refreshMqttSubscription()) {
99
-                        log.info(">>> MQTT 初始化成功");
100
-                        return true;
101
-                    }
102
-                } catch (Exception e) {
103
-                    log.error("MQTT 启动初始化失败(第" + (initRetry + 1) + "次):", e);
104
-                }
105
-                initRetry++;
106
-                try {
107
-                    Thread.sleep(RECONNECT_INTERVAL * initRetry);
108
-                } catch (InterruptedException ex) {
109
-                    Thread.currentThread().interrupt();
110
-                    break;
111
-                }
112
-            }
113
-            return false;
114
-        }, coreExecutor);
115
-
116
-        try {
117
-            Boolean connected = connectFuture.get(10, TimeUnit.SECONDS);
118
-            if (!connected) {
119
-                log.error("!!! MQTT 启动失败(超时),触发后台重连机制");
120
-                triggerReconnect();
121
-            }
122
-        } catch (TimeoutException e) {
123
-            log.error("MQTT 初始化超时(10秒),触发后台重连机制");
124
-            triggerReconnect();
125
-        } catch (InterruptedException e) {
126
-            Thread.currentThread().interrupt();
127
-            log.error("MQTT 初始化被中断");
128
-            triggerReconnect();
129
-        } catch (ExecutionException e) {
130
-            log.error("MQTT 初始化执行异常: ", e.getCause());
131
-            triggerReconnect();
132
-        }
133
-    }
134
-
135
-    private void initMqttConnectOptions() {
136
-        if (connOpts != null) return;
137
-        try {
138
-            connOpts = new MqttConnectOptions();
139
-            connOpts.setCleanSession(false);
140
-            connOpts.setAutomaticReconnect(true);
141
-            connOpts.setConnectionTimeout(10);
142
-            connOpts.setKeepAliveInterval(60);
143
-            connOpts.setMaxInflight(10);
144
-            connOpts.setMaxReconnectDelay(30);
145
-            connOpts.setUserName(mqttUsername);
146
-            connOpts.setPassword(mqttPassword.toCharArray());
147
-        } catch (Exception e) {
148
-            log.error("初始化 MQTT 配置失败:" + e.getMessage());
149
-            throw new RuntimeException(e);
150
-        }
151
-    }
152
-
153
-    public boolean refreshMqttSubscription() {
154
-        synchronized (lock) {
155
-            try {
156
-                initMqttConnectOptions();
157
-                List<String> latestTopicList =new ArrayList<>();
158
-                latestTopicList.add("station/ChargeStation/device/+/post/json");
159
-                log.info("🔍 查询到 Topic 列表: " + latestTopicList);
160
-
161
-                if (latestTopicList == null || latestTopicList.isEmpty()) {
162
-                    log.error("未查询到 Topic,取消所有订阅");
163
-                    unsubscribeAll();
164
-                    currentTopicSet.clear();
165
-                    return false;
166
-                }
167 24
 
168
-                Set<String> latestTopicSet = new HashSet<>();
169
-                for (String topic : latestTopicList) {
170
-                    if (topic != null && !topic.trim().isEmpty()) {
171
-                        latestTopicSet.add(topic.trim());
172
-                    }
173
-                }
174
-
175
-                if (latestTopicSet.equals(currentTopicSet)) {
176
-                    log.info("Topic 列表无变化,无需刷新订阅关系");
177
-                    if (!isConnected.get() && !latestTopicSet.isEmpty()) {
178
-                        return connectAndSubscribe(latestTopicSet);
179
-                    }
180
-                    return true;
181
-                }
182
-
183
-                Set<String> topicsToAdd = new HashSet<>(latestTopicSet);
184
-                topicsToAdd.removeAll(currentTopicSet);
185
-                Set<String> topicsToRemove = new HashSet<>(currentTopicSet);
186
-                topicsToRemove.removeAll(latestTopicSet);
187
-
188
-                if (!isConnected.get()) {
189
-                    return connectAndSubscribe(latestTopicSet);
190
-                } else {
191
-                    if (!topicsToRemove.isEmpty()) unsubscribeTopics(topicsToRemove);
192
-                    if (!topicsToAdd.isEmpty()) subscribeTopics(topicsToAdd);
193
-                }
194
-
195
-                currentTopicSet.clear();
196
-                currentTopicSet.addAll(latestTopicSet);
197
-                log.info("MQTT 订阅刷新完成,当前数量:" + currentTopicSet.size());
198
-                return true;
199
-
200
-            } catch (Exception e) {
201
-                log.error("MQTT 订阅刷新失败:", e);
202
-                return false;
203
-            }
204
-        }
205
-    }
206
-
207
-    private boolean connectAndSubscribe(Set<String> topicSet) {
208
-        synchronized (lock) {
209
-            if (isConnected.get() && mqttClient != null && mqttClient.isConnected()) {
210
-                return true;
211
-            }
212
-            try {
213
-                if (!checkServerAvailability()) {
214
-                    log.error("Broker 不可达,连接失败");
215
-                    return false;
216
-                }
217
-
218
-                if (mqttClient == null) {
219
-                    String clientId = generateUniqueClientId();
220
-                    MemoryPersistence persistence = new MemoryPersistence();
221
-                    mqttClient = new MqttClient(brokerUrl, clientId, persistence);
222
-                    setMqttCallback();
223
-                }
224
-
225
-                int connectRetry = 0;
226
-                while (connectRetry < 3 && !mqttClient.isConnected()) {
227
-                    try {
228
-                        mqttClient.connect(connOpts);
229
-                        if (mqttClient.isConnected()) {
230
-                            isConnected.set(true);
231
-                            log.info("MQTT 连接成功,客户端 ID:" + mqttClient.getClientId());
232
-                            break;
233
-                        }
234
-                    } catch (MqttException e) {
235
-                        log.error("MQTT 连接失败(第" + (connectRetry + 1) + "次):" + e.getMessage());
236
-                        connectRetry++;
237
-                        if (connectRetry < 3) Thread.sleep(RECONNECT_INTERVAL * (connectRetry + 1));
238
-                    }
239
-                }
240
-
241
-                if (!mqttClient.isConnected()) {
242
-                    isConnected.set(false);
243
-                    return false;
244
-                }
245
-
246
-                if (!topicSet.isEmpty()) {
247
-                    List<String> topicList = new ArrayList<>(topicSet);
248
-                    for (int i = 0; i < topicList.size(); i += MAX_BATCH_SIZE) {
249
-                        int end = Math.min(i + MAX_BATCH_SIZE, topicList.size());
250
-                        List<String> batch = topicList.subList(i, end);
251
-                        batchSubscribeTopics(new HashSet<>(batch));
252
-                        Thread.sleep(100);
253
-                    }
254
-                }
255
-                return true;
256
-
257
-            } catch (MqttException e) {
258
-                log.error("MQTT 连接 + 订阅失败:", e);
259
-                isConnected.set(false);
260
-                triggerReconnect();
261
-                return false;
262
-            } catch (InterruptedException e) {
263
-                Thread.currentThread().interrupt();
264
-                log.error("MQTT 连接 + 订阅被中断");
265
-                isConnected.set(false);
266
-                return false;
267
-            }
268
-        }
269
-    }
270
-
271
-    private void batchSubscribeTopics(Set<String> topicSet) {
272
-        if (topicSet.isEmpty() || !isConnected.get() || mqttClient == null) return;
273
-        try {
274
-            List<String> topicList = new ArrayList<>(topicSet);
275
-            String[] topics = topicList.toArray(new String[0]);
276
-            int[] qosArr = new int[topics.length];
277
-            Arrays.fill(qosArr, QOS);
278
-            mqttClient.subscribe(topics, qosArr);
279
-            log.info("订阅完成:" + topicList.size() + "个 Topic");
280
-        } catch (MqttException e) {
281
-            log.error("批量订阅失败:" + e.getMessage());
282
-            retrySubscribeBatch(new ArrayList<>(topicSet), 1);
283
-        }
284
-    }
285
-
286
-    private void retrySubscribeBatch(List<String> batchTopics, int retryTimes) {
287
-        for (int retry = 1; retry <= retryTimes; retry++) {
288
-            try {
289
-                Thread.sleep(1000 * retry);
290
-                if (!isConnected.get() || mqttClient == null) continue;
291
-                String[] topics = batchTopics.toArray(new String[0]);
292
-                int[] qosArr = new int[topics.length];
293
-                Arrays.fill(qosArr, QOS);
294
-                mqttClient.subscribe(topics, qosArr);
295
-                log.info("重试订阅成功(第" + retry + "次),数量:" + batchTopics.size());
296
-                return;
297
-            } catch (MqttException e) {
298
-                log.error("重试订阅失败(第" + retry + "次):" + e.getMessage());
299
-            } catch (InterruptedException e) {
300
-                Thread.currentThread().interrupt();
301
-                log.error("重试订阅被中断");
302
-                break;
303
-            }
304
-        }
305
-        log.error("批次订阅最终失败:" + batchTopics);
306
-    }
307
-
308
-    private void subscribeTopics(Set<String> topicsToAdd) {
309
-        if (topicsToAdd.isEmpty() || !isConnected.get()) return;
310
-        batchSubscribeTopics(topicsToAdd);
311
-    }
312
-
313
-    private void unsubscribeTopics(Set<String> topicsToRemove) {
314
-        if (topicsToRemove.isEmpty() || !isConnected.get() || mqttClient == null) return;
315
-        try {
316
-            String[] topics = topicsToRemove.toArray(new String[0]);
317
-            mqttClient.unsubscribe(topics);
318
-            log.info("取消订阅完成,数量:" + topicsToRemove.size());
319
-        } catch (MqttException e) {
320
-            log.error("取消订阅失败:" + e.getMessage());
321
-        }
322
-    }
323
-
324
-    private void unsubscribeAll() {
325
-        if (currentTopicSet.isEmpty() || !isConnected.get() || mqttClient == null) return;
326
-        try {
327
-            String[] topics = currentTopicSet.toArray(new String[0]);
328
-            mqttClient.unsubscribe(topics);
329
-            log.info("取消所有订阅,数量:" + currentTopicSet.size());
330
-        } catch (MqttException e) {
331
-            log.error("取消所有订阅失败:" + e.getMessage());
332
-        }
333
-    }
334
-
335
-    private void triggerReconnect() {
336
-        coreExecutor.schedule(() -> {
337
-            int maxAttempts = 3;
338
-            int attempt = 1;
339
-            while (attempt <= maxAttempts && !isConnected.get()) {
340
-                try {
341
-                    log.info("MQTT 重连(第" + attempt + "次)");
342
-                    if (connectAndSubscribe(currentTopicSet)) {
343
-                        log.info("MQTT 重连成功");
344
-                        break;
345
-                    }
346
-                    attempt++;
347
-                    if (attempt <= maxAttempts) Thread.sleep(RECONNECT_INTERVAL * 2 * attempt);
348
-                } catch (InterruptedException e) {
349
-                    Thread.currentThread().interrupt();
350
-                    break;
351
-                } catch (Exception e) {
352
-                    log.error("MQTT 重连失败(第" + attempt + "次):" + e.getMessage());
353
-                    attempt++;
354
-                }
355
-            }
356
-            if (attempt > maxAttempts) {
357
-                log.error("MQTT 重连达最大次数,停止尝试");
358
-                isConnected.set(false);
359
-            }
360
-        }, 5, TimeUnit.SECONDS);
361
-    }
362
-
363
-    private boolean checkServerAvailability() {
364
-        try (Socket socket = new Socket()) {
365
-            socket.setSoTimeout(CONNECT_TIMEOUT);
366
-            socket.setTcpNoDelay(true);
367
-            socket.connect(new InetSocketAddress(brokerHost, brokerPort), CONNECT_TIMEOUT);
368
-            return true;
369
-        } catch (Exception e) {
370
-            log.error("MQTT Broker 不可达:" + e.getMessage());
371
-            return false;
25
+    @Autowired
26
+    public MqttChargeStationConsumer(IotProperties iotProperties,
27
+                                     @Qualifier("mqttCoreExecutor") ScheduledExecutorService coreExecutor,
28
+                                     @Qualifier("mqttWriteExecutor") ExecutorService writeExecutor,
29
+                                     TDengineService tdengineService) {
30
+        super(iotProperties, coreExecutor, writeExecutor);
31
+        this.tdengineService = tdengineService;
32
+    }
33
+
34
+    @Override
35
+    protected List<String> fetchTopics() {
36
+        String topic = iotProperties.getMqtt().getChargeStationTopic();
37
+        if (topic == null || topic.trim().isEmpty()) {
38
+            log.warn("ChargeStation topic 未配置");
39
+            return Collections.emptyList();
372 40
         }
41
+        return Collections.singletonList(topic.trim());
373 42
     }
374 43
 
375
-    private String generateUniqueClientId() {
376
-        String osPrefix = System.getProperty("os.name").toLowerCase().contains("windows") ? "mqtt_win_" : "mqtt_linux_";
377
-        return osPrefix + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8);
44
+    @Override
45
+    protected void processMessage(String content, String topic) throws Exception {
46
+        List<Map<String, Object>> messageList = objectMapper.readValue(content, new TypeReference<List<Map<String, Object>>>() {});
47
+        processMessageAndWriteToTDengine(messageList, topic);
378 48
     }
379 49
 
380
-    private void setMqttCallback() {
381
-        if (mqttClient == null) return;
382
-        mqttClient.setCallback(new MqttCallback() {
383
-            @Override
384
-            public void connectionLost(Throwable cause) {
385
-                log.error("MQTT 连接断开:" + cause.getMessage());
386
-                isConnected.set(false);
387
-                coreExecutor.schedule(() -> triggerReconnect(), 5, TimeUnit.SECONDS);
388
-            }
389
-
390
-            @Override
391
-            public void messageArrived(String topic, MqttMessage message) {
392
-                // 直接提交到写入线程池处理
393
-                writeExecutor.submit(() -> {
394
-                    try {
395
-                        String content = new String(message.getPayload(), StandardCharsets.UTF_8);
396
-                        if (content == null || content.trim().isEmpty()) return;
397
-                        List<Map<String, Object>> messageList = objectMapper.readValue(content, new TypeReference<List<Map<String, Object>>>() {});
398
-
399
-                        processMessageAndWriteToTDengine(messageList, topic);
400
-                    } catch (Exception e) {
401
-                        log.error("MQTT 消息处理失败(Topic:" + topic + "):", e);
402
-                    }
403
-                });
404
-            }
405
-
406
-            @Override
407
-            public void deliveryComplete(IMqttDeliveryToken token) {
408
-                try { token.waitForCompletion(1000); } catch (MqttException ignored) {}
409
-            }
410
-        });
411
-    }
412
-
413
-    // 参数改为 List
414 50
     private void processMessageAndWriteToTDengine(List<Map<String, Object>> dataList, String topic) throws Exception {
415 51
         String[] topicParts = topic.split("/");
416 52
         if (topicParts.length < 2) {
417 53
             throw new IllegalArgumentException("无效的 Topic 格式:" + topic);
418 54
         }
419
-        // TDengine 批量插入通常需要一个列表
55
+
420 56
         List<Map<String, Object>> batchToInsert = new ArrayList<>();
421 57
         for (Map<String, Object> dataMap : dataList) {
422
-            // 检查数据是否为空
423 58
             if (dataMap == null || dataMap.isEmpty()) {
424 59
                 continue;
425 60
             }
426
-            // 深拷贝
427 61
             Map<String, Object> list = deepCopyMap(dataMap);
428 62
             batchToInsert.add(list);
429 63
         }
430 64
 
431
-        // 如果有数据,才写入数据库
432 65
         if (!batchToInsert.isEmpty()) {
433 66
             String dbName = topicParts[1];
434 67
             String superTable = topicParts[3];
435 68
             LocalDate date = LocalDate.now();
436 69
             String tableName = superTable + "_" + date.getYear() + date.getMonthValue();
437
-            // 调用写入方法(假设 insertBatch 接收 List)
438 70
             tdengineService.insertBatch(dbName, tableName, batchToInsert);
439 71
         }
440 72
     }
441
-
442
-    private Map<String, Object> deepCopyMap(Map<String, Object> original) {
443
-        if (original == null) return new HashMap<>();
444
-        try {
445
-            String json = objectMapper.writeValueAsString(original);
446
-            return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {});
447
-        } catch (Exception e) {
448
-            Map<String, Object> copy = new HashMap<>(original.size());
449
-            original.forEach((k, v) -> {
450
-                if (v instanceof Map) {
451
-                    copy.put(k, deepCopyMap((Map<String, Object>) v));
452
-                } else {
453
-                    copy.put(k, v);
454
-                }
455
-            });
456
-            return copy;
457
-        }
458
-    }
459
-
460
-    public void disconnect() {
461
-        synchronized (lock) {
462
-            shutdownExecutor(writeExecutor, "TDengine 写入");
463
-
464
-            if (mqttClient != null) {
465
-                try {
466
-                    if (mqttClient.isConnected()) {
467
-                        unsubscribeAll();
468
-                        mqttClient.disconnect(10000);
469
-                    }
470
-                    mqttClient.close();
471
-                    log.info("MQTT 连接已断开");
472
-                } catch (MqttException e) {
473
-                    log.error("断开 MQTT 连接失败:" + e.getMessage());
474
-                } finally {
475
-                    isConnected.set(false);
476
-                    mqttClient = null;
477
-                }
478
-            }
479
-            if (tdengineService != null) tdengineService.close();
480
-        }
481
-    }
482
-
483
-    private void shutdownExecutor(ExecutorService executor, String name) {
484
-        if (executor == null || executor.isShutdown()) return;
485
-        try {
486
-            executor.shutdown();
487
-            if (!executor.awaitTermination(20, TimeUnit.SECONDS)) {
488
-                List<Runnable> remaining = executor.shutdownNow();
489
-                log.error(name + " 线程池强制关闭,剩余任务数:" + remaining.size());
490
-            } else {
491
-                log.info(name + " 线程池已优雅关闭");
492
-            }
493
-        } catch (InterruptedException e) {
494
-            executor.shutdownNow();
495
-            Thread.currentThread().interrupt();
496
-            log.error(name + " 线程池关闭被中断");
497
-        }
498
-    }
499
-
500
-    @PreDestroy
501
-    public void destroy() {
502
-        log.info(">>> 服务正在关闭...");
503
-        disconnect();
504
-        shutdownExecutor(coreExecutor, "MQTT 核心");
505
-    }
506
-}
73
+}

+ 31
- 469
iot-platform/src/main/java/com/iot/platform/mqtt/MqttDynamicConsumer.java Visa fil

@@ -2,422 +2,58 @@ package com.iot.platform.mqtt;
2 2
 
3 3
 import com.fasterxml.jackson.core.type.TypeReference;
4 4
 import com.fasterxml.jackson.databind.ObjectMapper;
5
+import com.iot.platform.config.IotProperties;
5 6
 import com.iot.platform.domain.SysController;
6 7
 import com.iot.platform.service.SysControllerService;
7 8
 import com.iot.platform.service.TDengineService;
8 9
 import org.eclipse.paho.client.mqttv3.*;
9
-import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
10
-import com.iot.platform.config.IotProperties;
11 10
 import org.springframework.beans.factory.annotation.Autowired;
11
+import org.springframework.beans.factory.annotation.Qualifier;
12 12
 import org.springframework.context.annotation.DependsOn;
13 13
 import org.springframework.data.redis.core.StringRedisTemplate;
14 14
 import org.springframework.stereotype.Component;
15 15
 
16
-import javax.annotation.PostConstruct;
17
-import javax.annotation.PreDestroy;
18
-import java.net.InetSocketAddress;
19
-import java.net.Socket;
20
-import java.nio.charset.StandardCharsets;
21 16
 import java.time.LocalDate;
22 17
 import java.time.LocalDateTime;
23 18
 import java.time.format.DateTimeFormatter;
24 19
 import java.util.*;
25
-import java.util.concurrent.*;
26
-import java.util.concurrent.atomic.AtomicBoolean;
27
-import org.slf4j.Logger;
28
-import org.slf4j.LoggerFactory;
20
+import java.util.concurrent.ExecutorService;
21
+import java.util.concurrent.ScheduledExecutorService;
22
+import java.util.concurrent.TimeUnit;
29 23
 
30
-@Component
31 24
 @SuppressWarnings("unchecked")
32
-public class MqttDynamicConsumer {
33
-
34
-    private static final Logger log = LoggerFactory.getLogger(MqttDynamicConsumer.class);
35
-
36
-    @Autowired
37
-    private SysControllerService sysControllerService;
38
-
39
-    @Autowired
40
-    private TDengineService tdengineService;
25
+@Component
26
+@DependsOn({"sysControllerService", "tdengineService"})
27
+public class MqttDynamicConsumer extends AbstractDynamicMqttConsumer {
41 28
 
29
+    private final SysControllerService sysControllerService;
30
+    private final TDengineService tdengineService;
31
+    private final StringRedisTemplate stringRedisTemplate;
42 32
     private final ObjectMapper objectMapper = new ObjectMapper();
43
-    @Autowired
44
-    private StringRedisTemplate stringRedisTemplate;
45 33
 
46 34
     @Autowired
47
-    private IotProperties iotProperties;
48
-
49
-    // MQTT 配置
50
-    private String brokerUrl;
51
-    private String brokerHost;
52
-    private int brokerPort;
53
-    private static final int QOS = 1;
54
-    private static final int CONNECT_TIMEOUT = 3000;
55
-    private static final int RECONNECT_INTERVAL = 5000;
56
-    private static final int MAX_BATCH_SIZE = 50;
57
-
58
-    private String mqttUsername;
59
-    private String mqttPassword;
60
-
61
-    private MqttClient mqttClient;
62
-    private MqttConnectOptions connOpts;
63
-    private final Set<String> currentTopicSet = new CopyOnWriteArraySet<>();
64
-    private final AtomicBoolean isConnected = new AtomicBoolean(false);
65
-    private final Object lock = new Object();
66
-
67
-    // 核心调度线程池
68
-    private final ScheduledExecutorService coreExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
69
-        Thread t = new Thread(r);
70
-        t.setName("mqtt-core-scheduler");
71
-        t.setDaemon(false); // ← 改这里!
72
-        t.setPriority(Thread.NORM_PRIORITY - 1);
73
-        return t;
74
-    });
75
-
76
-    // TDengine 写入线程池
77
-    private final ExecutorService writeExecutor = new ThreadPoolExecutor(
78
-            4, 8, 60L, TimeUnit.SECONDS,
79
-            new LinkedBlockingQueue<>(5000),
80
-            r -> {
81
-                Thread t = new Thread(r);
82
-                t.setName("td-write-worker-" + UUID.randomUUID().toString().substring(0, 4));
83
-                t.setDaemon(false); // ← 改这里!
84
-                return t;
85
-            },
86
-            new ThreadPoolExecutor.CallerRunsPolicy()
87
-    );
88
-
89
-    @PostConstruct
90
-    @DependsOn({"sysControllerService", "tdengineService"})
91
-    public void initMqttConnection() {
92
-        this.brokerUrl = iotProperties.getMqtt().getBrokerUrl();
93
-        String brokerAddr = this.brokerUrl.replace("tcp://", "");
94
-        int colonIdx = brokerAddr.lastIndexOf(':');
95
-        this.brokerHost = brokerAddr.substring(0, colonIdx);
96
-        this.brokerPort = Integer.parseInt(brokerAddr.substring(colonIdx + 1));
97
-        this.mqttUsername = iotProperties.getMqtt().getUsername();
98
-        this.mqttPassword = iotProperties.getMqtt().getPassword();
99
-
100
-        log.info(">>> 开始初始化 MQTT 服务...");
101
-
102
-        CompletableFuture<Boolean> connectFuture = CompletableFuture.supplyAsync(() -> {
103
-            int initRetry = 0;
104
-            while (initRetry < 3 && !isConnected.get()) {
105
-                try {
106
-                    if (refreshMqttSubscription()) {
107
-                        log.info(">>> MQTT 初始化成功");
108
-                        return true;
109
-                    }
110
-                } catch (Exception e) {
111
-                    log.error("MQTT 启动初始化失败(第" + (initRetry + 1) + "次):", e);
112
-                }
113
-                initRetry++;
114
-                try {
115
-                    Thread.sleep(RECONNECT_INTERVAL * initRetry);
116
-                } catch (InterruptedException ex) {
117
-                    Thread.currentThread().interrupt();
118
-                    break;
119
-                }
120
-            }
121
-            return false;
122
-        }, coreExecutor);
123
-
124
-        try {
125
-            Boolean connected = connectFuture.get(10, TimeUnit.SECONDS);
126
-            if (!connected) {
127
-                log.error("!!! MQTT 启动失败(超时),触发后台重连机制");
128
-                triggerReconnect();
129
-            }
130
-        } catch (TimeoutException e) {
131
-            log.error("MQTT 初始化超时(10秒),触发后台重连机制");
132
-            triggerReconnect();
133
-        } catch (InterruptedException e) {
134
-            Thread.currentThread().interrupt();
135
-            log.error("MQTT 初始化被中断");
136
-            triggerReconnect();
137
-        } catch (ExecutionException e) {
138
-            log.error("MQTT 初始化执行异常: ", e.getCause());
139
-            triggerReconnect();
140
-        }
141
-    }
142
-
143
-    private void initMqttConnectOptions() {
144
-        if (connOpts != null) return;
145
-        try {
146
-            connOpts = new MqttConnectOptions();
147
-            connOpts.setCleanSession(false);
148
-            connOpts.setAutomaticReconnect(true);
149
-            connOpts.setConnectionTimeout(10);
150
-            connOpts.setKeepAliveInterval(60);
151
-            connOpts.setMaxInflight(10);
152
-            connOpts.setMaxReconnectDelay(30);
153
-            connOpts.setUserName(mqttUsername);
154
-            connOpts.setPassword(mqttPassword.toCharArray());
155
-        } catch (Exception e) {
156
-            log.error("初始化 MQTT 配置失败:" + e.getMessage());
157
-            throw new RuntimeException(e);
158
-        }
159
-    }
160
-
161
-    public boolean refreshMqttSubscription() {
162
-        synchronized (lock) {
163
-            try {
164
-                initMqttConnectOptions();
165
-                List<String> latestTopicList = sysControllerService.selectall();
166
-                log.info("🔍 查询到 Topic 列表: " + latestTopicList);
167
-
168
-                if (latestTopicList == null || latestTopicList.isEmpty()) {
169
-                    log.error("未查询到 Topic,取消所有订阅");
170
-                    unsubscribeAll();
171
-                    currentTopicSet.clear();
172
-                    return false;
173
-                }
174
-
175
-                Set<String> latestTopicSet = new HashSet<>();
176
-                for (String topic : latestTopicList) {
177
-                    if (topic != null && !topic.trim().isEmpty()) {
178
-                        latestTopicSet.add(topic.trim());
179
-                    }
180
-                }
181
-
182
-                if (latestTopicSet.equals(currentTopicSet)) {
183
-                    log.info("Topic 列表无变化,无需刷新订阅关系");
184
-                    if (!isConnected.get() && !latestTopicSet.isEmpty()) {
185
-                        return connectAndSubscribe(latestTopicSet);
186
-                    }
187
-                    return true;
188
-                }
189
-
190
-                Set<String> topicsToAdd = new HashSet<>(latestTopicSet);
191
-                topicsToAdd.removeAll(currentTopicSet);
192
-                Set<String> topicsToRemove = new HashSet<>(currentTopicSet);
193
-                topicsToRemove.removeAll(latestTopicSet);
194
-
195
-                if (!isConnected.get()) {
196
-                    return connectAndSubscribe(latestTopicSet);
197
-                } else {
198
-                    if (!topicsToRemove.isEmpty()) unsubscribeTopics(topicsToRemove);
199
-                    if (!topicsToAdd.isEmpty()) subscribeTopics(topicsToAdd);
200
-                }
201
-
202
-                currentTopicSet.clear();
203
-                currentTopicSet.addAll(latestTopicSet);
204
-                log.info("MQTT 订阅刷新完成,当前数量:" + currentTopicSet.size());
205
-                return true;
206
-
207
-            } catch (Exception e) {
208
-                log.error("MQTT 订阅刷新失败:", e);
209
-                return false;
210
-            }
211
-        }
212
-    }
213
-
214
-    private boolean connectAndSubscribe(Set<String> topicSet) {
215
-        synchronized (lock) {
216
-            if (isConnected.get() && mqttClient != null && mqttClient.isConnected()) {
217
-                return true;
218
-            }
219
-            try {
220
-                if (!checkServerAvailability()) {
221
-                    log.error("Broker 不可达,连接失败");
222
-                    return false;
223
-                }
224
-
225
-                if (mqttClient == null) {
226
-                    String clientId = generateUniqueClientId();
227
-                    MemoryPersistence persistence = new MemoryPersistence();
228
-                    mqttClient = new MqttClient(brokerUrl, clientId, persistence);
229
-                    setMqttCallback();
230
-                }
231
-
232
-                int connectRetry = 0;
233
-                while (connectRetry < 3 && !mqttClient.isConnected()) {
234
-                    try {
235
-                        mqttClient.connect(connOpts);
236
-                        if (mqttClient.isConnected()) {
237
-                            isConnected.set(true);
238
-                            log.info("MQTT 连接成功,客户端 ID:" + mqttClient.getClientId());
239
-                            break;
240
-                        }
241
-                    } catch (MqttException e) {
242
-                        log.error("MQTT 连接失败(第" + (connectRetry + 1) + "次):" + e.getMessage());
243
-                        connectRetry++;
244
-                        if (connectRetry < 3) Thread.sleep(RECONNECT_INTERVAL * (connectRetry + 1));
245
-                    }
246
-                }
247
-
248
-                if (!mqttClient.isConnected()) {
249
-                    isConnected.set(false);
250
-                    return false;
251
-                }
252
-
253
-                if (!topicSet.isEmpty()) {
254
-                    List<String> topicList = new ArrayList<>(topicSet);
255
-                    for (int i = 0; i < topicList.size(); i += MAX_BATCH_SIZE) {
256
-                        int end = Math.min(i + MAX_BATCH_SIZE, topicList.size());
257
-                        List<String> batch = topicList.subList(i, end);
258
-                        batchSubscribeTopics(new HashSet<>(batch));
259
-                        Thread.sleep(100);
260
-                    }
261
-                }
262
-                return true;
263
-
264
-            } catch (MqttException e) {
265
-                log.error("MQTT 连接 + 订阅失败:", e);
266
-                isConnected.set(false);
267
-                triggerReconnect();
268
-                return false;
269
-            } catch (InterruptedException e) {
270
-                Thread.currentThread().interrupt();
271
-                log.error("MQTT 连接 + 订阅被中断");
272
-                isConnected.set(false);
273
-                return false;
274
-            }
275
-        }
276
-    }
277
-
278
-
279
-
280
-
281
-    private void batchSubscribeTopics(Set<String> topicSet) {
282
-        if (topicSet.isEmpty() || !isConnected.get() || mqttClient == null) return;
283
-        try {
284
-            List<String> topicList = new ArrayList<>(topicSet);
285
-            String[] topics = topicList.toArray(new String[0]);
286
-            int[] qosArr = new int[topics.length];
287
-            Arrays.fill(qosArr, QOS);
288
-            mqttClient.subscribe(topics, qosArr);
289
-            log.info("订阅完成:" + topicList.size() + "个 Topic");
290
-        } catch (MqttException e) {
291
-            log.error("批量订阅失败:" + e.getMessage());
292
-            retrySubscribeBatch(new ArrayList<>(topicSet), 1);
293
-        }
294
-    }
295
-
296
-    private void retrySubscribeBatch(List<String> batchTopics, int retryTimes) {
297
-        for (int retry = 1; retry <= retryTimes; retry++) {
298
-            try {
299
-                Thread.sleep(1000 * retry);
300
-                if (!isConnected.get() || mqttClient == null) continue;
301
-                String[] topics = batchTopics.toArray(new String[0]);
302
-                int[] qosArr = new int[topics.length];
303
-                Arrays.fill(qosArr, QOS);
304
-                mqttClient.subscribe(topics, qosArr);
305
-                log.info("重试订阅成功(第" + retry + "次),数量:" + batchTopics.size());
306
-                return;
307
-            } catch (MqttException e) {
308
-                log.error("重试订阅失败(第" + retry + "次):" + e.getMessage());
309
-            } catch (InterruptedException e) {
310
-                Thread.currentThread().interrupt();
311
-                log.error("重试订阅被中断");
312
-                break;
313
-            }
314
-        }
315
-        log.error("批次订阅最终失败:" + batchTopics);
316
-    }
317
-
318
-    private void subscribeTopics(Set<String> topicsToAdd) {
319
-        if (topicsToAdd.isEmpty() || !isConnected.get()) return;
320
-        batchSubscribeTopics(topicsToAdd);
321
-    }
322
-
323
-    private void unsubscribeTopics(Set<String> topicsToRemove) {
324
-        if (topicsToRemove.isEmpty() || !isConnected.get() || mqttClient == null) return;
325
-        try {
326
-            String[] topics = topicsToRemove.toArray(new String[0]);
327
-            mqttClient.unsubscribe(topics);
328
-            log.info("取消订阅完成,数量:" + topicsToRemove.size());
329
-        } catch (MqttException e) {
330
-            log.error("取消订阅失败:" + e.getMessage());
331
-        }
332
-    }
333
-
334
-    private void unsubscribeAll() {
335
-        if (currentTopicSet.isEmpty() || !isConnected.get() || mqttClient == null) return;
336
-        try {
337
-            String[] topics = currentTopicSet.toArray(new String[0]);
338
-            mqttClient.unsubscribe(topics);
339
-            log.info("取消所有订阅,数量:" + currentTopicSet.size());
340
-        } catch (MqttException e) {
341
-            log.error("取消所有订阅失败:" + e.getMessage());
342
-        }
343
-    }
344
-
345
-    private void triggerReconnect() {
346
-        coreExecutor.schedule(() -> {
347
-            int maxAttempts = 3;
348
-            int attempt = 1;
349
-            while (attempt <= maxAttempts && !isConnected.get()) {
350
-                try {
351
-                    log.info("MQTT 重连(第" + attempt + "次)");
352
-                    if (connectAndSubscribe(currentTopicSet)) {
353
-                        log.info("MQTT 重连成功");
354
-                        break;
355
-                    }
356
-                    attempt++;
357
-                    if (attempt <= maxAttempts) Thread.sleep(RECONNECT_INTERVAL * 2 * attempt);
358
-                } catch (InterruptedException e) {
359
-                    Thread.currentThread().interrupt();
360
-                    break;
361
-                } catch (Exception e) {
362
-                    log.error("MQTT 重连失败(第" + attempt + "次):" + e.getMessage());
363
-                    attempt++;
364
-                }
365
-            }
366
-            if (attempt > maxAttempts) {
367
-                log.error("MQTT 重连达最大次数,停止尝试");
368
-                isConnected.set(false);
369
-            }
370
-        }, 5, TimeUnit.SECONDS);
35
+    public MqttDynamicConsumer(IotProperties iotProperties,
36
+                               @Qualifier("mqttCoreExecutor") ScheduledExecutorService coreExecutor,
37
+                               @Qualifier("mqttWriteExecutor") ExecutorService writeExecutor,
38
+                               SysControllerService sysControllerService,
39
+                               TDengineService tdengineService,
40
+                               StringRedisTemplate stringRedisTemplate) {
41
+        super(iotProperties, coreExecutor, writeExecutor);
42
+        this.sysControllerService = sysControllerService;
43
+        this.tdengineService = tdengineService;
44
+        this.stringRedisTemplate = stringRedisTemplate;
371 45
     }
372 46
 
373
-    private boolean checkServerAvailability() {
374
-        try (Socket socket = new Socket()) {
375
-            socket.setSoTimeout(CONNECT_TIMEOUT);
376
-            socket.setTcpNoDelay(true);
377
-            socket.connect(new InetSocketAddress(brokerHost, brokerPort), CONNECT_TIMEOUT);
378
-            return true;
379
-        } catch (Exception e) {
380
-            log.error("MQTT Broker 不可达:" + e.getMessage());
381
-            return false;
382
-        }
47
+    @Override
48
+    protected List<String> fetchTopics() {
49
+        return sysControllerService.selectall();
383 50
     }
384 51
 
385
-    private String generateUniqueClientId() {
386
-        String osPrefix = System.getProperty("os.name").toLowerCase().contains("windows") ? "mqtt_win_" : "mqtt_linux_";
387
-        return osPrefix + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8);
388
-    }
389
-
390
-    private void setMqttCallback() {
391
-        if (mqttClient == null) return;
392
-        mqttClient.setCallback(new MqttCallback() {
393
-            @Override
394
-            public void connectionLost(Throwable cause) {
395
-                log.error("MQTT 连接断开:" + cause.getMessage());
396
-                isConnected.set(false);
397
-                coreExecutor.schedule(() -> triggerReconnect(), 5, TimeUnit.SECONDS);
398
-            }
399
-
400
-            @Override
401
-            public void messageArrived(String topic, MqttMessage message) {
402
-                // 直接提交到写入线程池处理
403
-                writeExecutor.submit(() -> {
404
-                    try {
405
-                        String content = new String(message.getPayload(), StandardCharsets.UTF_8);
406
-                        if (content == null || content.trim().isEmpty()) return;
407
-
408
-                        Map<String, Object> messageMap = objectMapper.readValue(content, new TypeReference<Map<String, Object>>() {});
409
-                        processMessageAndWriteToTDengine(messageMap, topic);
410
-                    } catch (Exception e) {
411
-                        log.error("MQTT 消息处理失败(Topic:" + topic + "):", e);
412
-                    }
413
-                });
414
-            }
415
-
416
-            @Override
417
-            public void deliveryComplete(IMqttDeliveryToken token) {
418
-                try { token.waitForCompletion(1000); } catch (MqttException ignored) {}
419
-            }
420
-        });
52
+    @Override
53
+    protected void processMessage(String content, String topic) throws Exception {
54
+        Map<String, Object> weather = objectMapper.readValue(content, new TypeReference<Map<String, Object>>() {});
55
+        processMessageAndWriteToTDengine(weather, topic);
56
+        insertredis(weather, topic);
421 57
     }
422 58
 
423 59
     private void processMessageAndWriteToTDengine(Map<String, Object> weather, String topic) throws Exception {
@@ -444,7 +80,6 @@ public class MqttDynamicConsumer {
444 80
         list.put("timestamp", weather.get("timestamp"));
445 81
         list.put("device_id", weather.get("device_id"));
446 82
 
447
-        // 构建单条数据并立即写入(可改为批量缓冲)
448 83
         String dbName = topicParts[0];
449 84
         String superTable = topicParts[1];
450 85
         LocalDate date = LocalDate.now();
@@ -452,16 +87,13 @@ public class MqttDynamicConsumer {
452 87
 
453 88
         List<Map<String, Object>> batch = Collections.singletonList(list);
454 89
         tdengineService.insertBatch(dbName, tableName, batch);
455
-        insertredis(weather,topic);
456 90
     }
457 91
 
458
-
459 92
     public void insertredis(Map<String, Object> weather, String topic) throws Exception {
460 93
         String[] topicParts = topic.split("/", 2);
461 94
         if (topicParts.length < 2) return;
462 95
 
463 96
         String controllerId = topicParts[0];
464
-        String deviceIdFromTopic = topicParts[1]; // 虽然不用在 key 中,但可记录
465 97
 
466 98
         SysController sysController = sysControllerService.selectcontrollerpath(topic);
467 99
         if (sysController == null || sysController.getName() == null) return;
@@ -472,14 +104,12 @@ public class MqttDynamicConsumer {
472 104
         Map<String, Object> metricData = (Map<String, Object>) weather.get(sysController.getName());
473 105
         if (metricData == null) return;
474 106
 
475
-        // ✅ 使用标准格式:DSB:controllerId:metricName
476 107
         String redisKey = "DSB:" + controllerId + ":" + sysController.getName();
477 108
 
478
-        // 准备写入的数据(包含系统字段)
479 109
         Map<String, String> hashData = new HashMap<>();
480 110
         hashData.put("createTime", createTime);
481 111
         hashData.put("timestamp", getStringFromMap(weather, "timestamp"));
482
-        hashData.put("device_id", getStringFromMap(weather, "device_id")); // UUID
112
+        hashData.put("device_id", getStringFromMap(weather, "device_id"));
483 113
 
484 114
         for (Map.Entry<String, Object> entry : metricData.entrySet()) {
485 115
             if (entry.getValue() != null) {
@@ -490,7 +120,6 @@ public class MqttDynamicConsumer {
490 120
         stringRedisTemplate.opsForHash().putAll(redisKey, hashData);
491 121
         stringRedisTemplate.expire(redisKey, 2, TimeUnit.HOURS);
492 122
 
493
-        // 记录到活跃集合
494 123
         stringRedisTemplate.opsForSet().add("DSB:active:devices", redisKey);
495 124
         stringRedisTemplate.expire("DSB:active:devices", 2, TimeUnit.HOURS);
496 125
     }
@@ -499,71 +128,4 @@ public class MqttDynamicConsumer {
499 128
         Object val = map.get(key);
500 129
         return val == null ? "" : val.toString().trim();
501 130
     }
502
-
503
-
504
-
505
-    private Map<String, Object> deepCopyMap(Map<String, Object> original) {
506
-        if (original == null) return new HashMap<>();
507
-        try {
508
-            String json = objectMapper.writeValueAsString(original);
509
-            return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {});
510
-        } catch (Exception e) {
511
-            Map<String, Object> copy = new HashMap<>(original.size());
512
-            original.forEach((k, v) -> {
513
-                if (v instanceof Map) {
514
-                    copy.put(k, deepCopyMap((Map<String, Object>) v));
515
-                } else {
516
-                    copy.put(k, v);
517
-                }
518
-            });
519
-            return copy;
520
-        }
521
-    }
522
-
523
-    public void disconnect() {
524
-        synchronized (lock) {
525
-            shutdownExecutor(writeExecutor, "TDengine 写入");
526
-
527
-            if (mqttClient != null) {
528
-                try {
529
-                    if (mqttClient.isConnected()) {
530
-                        unsubscribeAll();
531
-                        mqttClient.disconnect(10000);
532
-                    }
533
-                    mqttClient.close();
534
-                    log.info("MQTT 连接已断开");
535
-                } catch (MqttException e) {
536
-                    log.error("断开 MQTT 连接失败:" + e.getMessage());
537
-                } finally {
538
-                    isConnected.set(false);
539
-                    mqttClient = null;
540
-                }
541
-            }
542
-            if (tdengineService != null) tdengineService.close();
543
-        }
544
-    }
545
-
546
-    private void shutdownExecutor(ExecutorService executor, String name) {
547
-        if (executor == null || executor.isShutdown()) return;
548
-        try {
549
-            executor.shutdown();
550
-            if (!executor.awaitTermination(20, TimeUnit.SECONDS)) {
551
-                List<Runnable> remaining = executor.shutdownNow();
552
-                log.error(name + " 线程池强制关闭,剩余任务数:" + remaining.size());
553
-            } else {
554
-                log.info(name + " 线程池已优雅关闭");
555
-            }
556
-        } catch (InterruptedException e) {
557
-            executor.shutdownNow();
558
-            Thread.currentThread().interrupt();
559
-            log.error(name + " 线程池关闭被中断");
560
-        }
561
-    }
562
-
563
-    @PreDestroy
564
-    public void destroy() {
565
-        log.info(">>> 服务正在关闭...");
566
-        disconnect();
567
-        shutdownExecutor(coreExecutor, "MQTT 核心");
568
-    }
569
-}
131
+}

+ 103
- 45
iot-platform/src/main/java/com/iot/platform/mqtt/MqttFaultConsumer.java Visa fil

@@ -1,11 +1,13 @@
1 1
 package com.iot.platform.mqtt;
2 2
 
3 3
 import com.fasterxml.jackson.databind.ObjectMapper;
4
+import com.iot.platform.config.IotProperties;
4 5
 import com.iot.platform.domain.SysDevice;
5 6
 import com.iot.platform.domain.SysFault;
6 7
 import com.iot.platform.service.*;
7 8
 import com.iot.platform.common.utils.NumericIdGenerator;
8 9
 import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.beans.factory.annotation.Qualifier;
9 11
 import org.springframework.stereotype.Component;
10 12
 import org.springframework.web.client.RestTemplate;
11 13
 
@@ -22,29 +24,45 @@ import java.util.concurrent.ExecutorService;
22 24
 @Component
23 25
 public class MqttFaultConsumer extends AbstractMqttConsumer {
24 26
 
25
-    @Autowired
26
-    public SysControllerService sysControllerService;
27
-    @Autowired
28
-    public SysFaultService sysFaultService;
29
-    @Autowired
30
-    public SysrealtimeService sysrealtimeService;
31
-    @Autowired
32
-    public SysWorkorderService sysWorkorderService;
33
-    @Autowired
34
-    public SysAlarmService sysAlarmService;
35
-    @Autowired
36
-    public NumericIdGenerator numericIdGenerator;
37
-    @Autowired
38
-    public TDegnineAlarm tDegnineAlarm;
39
-    @Autowired
40
-    private RestTemplate restTemplate;
27
+    private final SysControllerService sysControllerService;
28
+    private final SysFaultService sysFaultService;
29
+    private final SysrealtimeService sysrealtimeService;
30
+    private final SysWorkorderService sysWorkorderService;
31
+    private final SysAlarmService sysAlarmService;
32
+    private final NumericIdGenerator numericIdGenerator;
33
+    private final TDegnineAlarm tDegnineAlarm;
34
+    private final RestTemplate restTemplate;
35
+
36
+    private static final String ALARM_STATUS_TRIGGER = "0";
37
+    private static final String ALARM_STATUS_RECOVERED = "1";
38
+    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
39
+    private static final String COMPANY_ID_PREFIX = "GJ";
41 40
 
42 41
     private final ObjectMapper objectMapper = new ObjectMapper();
43 42
     private final ExecutorService mqttFaultExecutor;
44 43
 
45 44
     @Autowired
46
-    public MqttFaultConsumer(ExecutorService mqttFaultExecutor) {
45
+    public MqttFaultConsumer(@Qualifier("mqttFaultExecutor") ExecutorService mqttFaultExecutor,
46
+                             @Qualifier("abstractConsumerExecutor") ExecutorService executorService,
47
+                             IotProperties iotProperties,
48
+                             SysControllerService sysControllerService,
49
+                             SysFaultService sysFaultService,
50
+                             SysrealtimeService sysrealtimeService,
51
+                             SysWorkorderService sysWorkorderService,
52
+                             SysAlarmService sysAlarmService,
53
+                             NumericIdGenerator numericIdGenerator,
54
+                             TDegnineAlarm tDegnineAlarm,
55
+                             RestTemplate restTemplate) {
56
+        super(executorService, iotProperties);
47 57
         this.mqttFaultExecutor = mqttFaultExecutor;
58
+        this.sysControllerService = sysControllerService;
59
+        this.sysFaultService = sysFaultService;
60
+        this.sysrealtimeService = sysrealtimeService;
61
+        this.sysWorkorderService = sysWorkorderService;
62
+        this.sysAlarmService = sysAlarmService;
63
+        this.numericIdGenerator = numericIdGenerator;
64
+        this.tDegnineAlarm = tDegnineAlarm;
65
+        this.restTemplate = restTemplate;
48 66
     }
49 67
 
50 68
     private static final Map<String, String> KEY_MAPPING = new HashMap<>();
@@ -62,15 +80,27 @@ public class MqttFaultConsumer extends AbstractMqttConsumer {
62 80
     @Override
63 81
     protected String generateClientId() {
64 82
         String osName = System.getProperty("os.name").toLowerCase();
65
-        return osName.contains("windows") ? "mqttx_e216fbf1620" : "mqttx_e216fbf1621";
83
+        String base = osName.contains("windows") ? "mqttx_e216fbf1620" : "mqttx_e216fbf1621";
84
+        return base + "_" + Long.toHexString(System.nanoTime()).substring(0, 6);
66 85
     }
67 86
 
68 87
     @Override
69 88
     protected void handleMessage(String topic, String messageContent) throws Exception {
70 89
         Map<String, Object> messageMap = objectMapper.readValue(messageContent, Map.class);
71
-        insertTDegine(messageMap, topic);
90
+        try {
91
+            insertTDegine(messageMap, topic);
92
+        } catch (Exception e) {
93
+            log.error("告警数据写入TDengine失败, topic={}, error={}", topic, e.getMessage(), e);
94
+        }
72 95
         SysFault sysFault = objectMapper.readValue(messageContent, SysFault.class);
73
-        mqttFaultExecutor.submit(() -> triggermethod(topic, sysFault));
96
+        mqttFaultExecutor.submit(() -> {
97
+            try {
98
+                triggermethod(topic, sysFault);
99
+            } catch (Exception e) {
100
+                log.error("告警业务处理失败, topic={}, controllerId={}, error={}",
101
+                        topic, sysFault.getController_id(), e.getMessage(), e);
102
+            }
103
+        });
74 104
     }
75 105
 
76 106
     @Override
@@ -79,11 +109,16 @@ public class MqttFaultConsumer extends AbstractMqttConsumer {
79 109
     }
80 110
 
81 111
     public void insertTDegine(Map<String, Object> weather, String topic) throws SQLException {
112
+        String[] topicParts = topic.split("/");
113
+        if (topicParts.length < 2) {
114
+            log.warn("无效的topic格式,缺少分隔符: {}", topic);
115
+            return;
116
+        }
82 117
         LocalDate localDate = LocalDate.now();
83 118
         int year = localDate.getYear();
84 119
         int month = localDate.getMonthValue();
85
-        String supertablename = topic.split("/")[0];
86
-        String table = topic.split("/")[0] + "_" + year + month;
120
+        String supertablename = topicParts[0];
121
+        String table = topicParts[0] + "_" + year + month;
87 122
 
88 123
         Map<String, Object> newMap = new HashMap<>();
89 124
         for (Map.Entry<String, Object> entry : weather.entrySet()) {
@@ -95,7 +130,7 @@ public class MqttFaultConsumer extends AbstractMqttConsumer {
95 130
                 newMap.put(originalKey, value);
96 131
             }
97 132
         }
98
-        tDegnineAlarm.shibaihou(newMap, supertablename, table, topic.split("/")[1]);
133
+        tDegnineAlarm.shibaihou(newMap, supertablename, table, topicParts[1]);
99 134
     }
100 135
 
101 136
     public void triggermethod(String topic, SysFault weather) {
@@ -103,47 +138,70 @@ public class MqttFaultConsumer extends AbstractMqttConsumer {
103 138
         String timestamp = weather.getTimestamp();
104 139
         String type = weather.getType();
105 140
         String desc = weather.getDesc();
141
+        String controllerId = weather.getController_id();
142
+
143
+        if (controllerId == null || controllerId.isEmpty()) {
144
+            log.warn("SysFault 缺少 controller_id,跳过处理");
145
+            return;
146
+        }
147
+        if (deviceId == null) {
148
+            deviceId = "";
149
+        }
150
+        if (type == null || desc == null) {
151
+            log.warn("SysFault 缺少 type 或 desc,跳过处理 controllerId={}", controllerId);
152
+            return;
153
+        }
106 154
 
107 155
         LocalDate localDate = LocalDate.now();
108 156
         int year = localDate.getYear();
109 157
         int month = localDate.getMonthValue();
110 158
         String formattedMonth = String.format("%02d", month);
111 159
 
112
-        String controllerId = weather.getController_id();
113 160
         String[] topics = topic.split("/");
161
+        if (topics.length == 0) {
162
+            log.warn("无效的topic格式: {}", topic);
163
+            return;
164
+        }
114 165
 
115 166
         List<String> tablename = sysrealtimeService.selecttables();
116
-        List<Boolean> a = new ArrayList<>();
167
+        if (tablename == null) {
168
+            tablename = Collections.emptyList();
169
+        }
117 170
         String controllername = controllerId + year + formattedMonth + "_fault";
118 171
 
119
-        for (int i = 0; i < tablename.size(); i++) {
120
-            a.add(tablename.get(i).equals(controllername));
172
+        boolean tableExists = false;
173
+        for (String name : tablename) {
174
+            if (controllername.equals(name)) {
175
+                tableExists = true;
176
+                break;
177
+            }
121 178
         }
122
-        if (!a.contains(true)) {
179
+        if (!tableExists) {
123 180
             sysFaultService.createmessage(controllername);
124 181
         }
125 182
 
183
+        SysDevice jingdu = sysControllerService.selectjingweidu(topics[0], "经度");
184
+        SysDevice weidu = sysControllerService.selectjingweidu(topics[0], "纬度");
185
+        if (jingdu == null || weidu == null) {
186
+            log.warn("未查询到控制器经纬度信息: {}", topics[0]);
187
+            return;
188
+        }
189
+        String jingduValue = jingdu.getV();
190
+        String weiduValue = weidu.getV();
191
+        String companyid = COMPANY_ID_PREFIX + numericIdGenerator.nextId();
192
+        LocalDateTime currentTime = LocalDateTime.now();
193
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);
194
+        String currentTimeStr = currentTime.format(formatter);
195
+
126 196
         if ("触发".equals(type)) {
127
-            SysDevice jingdu = sysControllerService.selectjingweidu(topics[0], "经度");
128
-            SysDevice weidu = sysControllerService.selectjingweidu(topics[0], "纬度");
129
-            String companyid = "GJ" + numericIdGenerator.nextId();
130
-            LocalDateTime currentTime = LocalDateTime.now();
131
-            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
132
-            String currentTimeStr = currentTime.format(formatter);
133
-            sysAlarmService.insertalarm(controllername, companyid, desc, "0", currentTimeStr, "0", controllerId, deviceId, jingdu.getV(), weidu.getV());
134
-            sysFaultService.insertfault(companyid, desc, "0", currentTimeStr, "0", controllerId, deviceId, jingdu.getV(), weidu.getV(), "");
197
+            sysAlarmService.insertalarm(controllername, companyid, desc, ALARM_STATUS_TRIGGER, currentTimeStr, ALARM_STATUS_TRIGGER, controllerId, deviceId, jingduValue, weiduValue);
198
+            sysFaultService.insertfault(companyid, desc, ALARM_STATUS_TRIGGER, currentTimeStr, ALARM_STATUS_TRIGGER, controllerId, deviceId, jingduValue, weiduValue, "");
135 199
         } else if ("恢复".equals(type)) {
136
-            SysDevice jingdu = sysControllerService.selectjingweidu(topics[0], "经度");
137
-            SysDevice weidu = sysControllerService.selectjingweidu(topics[0], "纬度");
138
-            String companyid = "GJ" + numericIdGenerator.nextId();
139
-            LocalDateTime currentTime = LocalDateTime.now();
140
-            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
141
-            String currentTimeStr = currentTime.format(formatter);
142
-            sysAlarmService.insertalarm(controllername, companyid, desc, "1", currentTimeStr, "0", controllerId, deviceId, jingdu.getV(), weidu.getV());
143
-            sysFaultService.updatefault("1", "0", jingdu.getV(), weidu.getV(), desc, controllerId, deviceId, currentTimeStr);
200
+            sysAlarmService.insertalarm(controllername, companyid, desc, ALARM_STATUS_RECOVERED, currentTimeStr, ALARM_STATUS_TRIGGER, controllerId, deviceId, jingduValue, weiduValue);
201
+            sysFaultService.updatefault(ALARM_STATUS_RECOVERED, ALARM_STATUS_TRIGGER, jingduValue, weiduValue, desc, controllerId, deviceId, currentTimeStr);
144 202
         }
145 203
 
146
-        String url = "https://esos-iot.com:9443/syscar/gaojing?controllerId=" + topics[0];
204
+        String url = iotProperties.getMqtt().getAlarmWebhookUrl() + "?controllerId=" + topics[0];
147 205
         restTemplate.postForObject(url, null, String.class);
148 206
     }
149 207
 }

+ 100
- 251
iot-platform/src/main/java/com/iot/platform/mqtt/MqttGenericConsumer.java Visa fil

@@ -1,296 +1,145 @@
1 1
 package com.iot.platform.mqtt;
2
-import com.alibaba.fastjson2.util.DateUtils;
2
+
3 3
 import com.fasterxml.jackson.databind.ObjectMapper;
4
+import com.iot.platform.config.IotProperties;
4 5
 import com.iot.platform.domain.ControllerData;
5 6
 import com.iot.platform.domain.topics;
6 7
 import com.iot.platform.service.SysControllerService;
7
-import com.iot.platform.service.TDengineService;
8
-import org.eclipse.paho.client.mqttv3.*;
9
-import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
10
-import com.iot.platform.config.IotProperties;
11 8
 import org.springframework.beans.factory.annotation.Autowired;
12
-import javax.annotation.PostConstruct;
13
-import javax.annotation.PreDestroy;
14
-import javax.net.ssl.*;
15
-import java.io.InputStream;
16
-import java.net.InetSocketAddress;
17
-import java.net.Socket;
18
-import java.security.KeyStore;
19
-import java.security.cert.CertificateFactory;
20
-import java.security.cert.X509Certificate;
21
-import java.util.*;
22
-import java.util.concurrent.ExecutorService;
23
-import java.util.concurrent.Executors;
24
-import org.springframework.core.io.ClassPathResource;
9
+import org.springframework.beans.factory.annotation.Qualifier;
25 10
 import org.springframework.data.redis.core.StringRedisTemplate;
26
-import org.springframework.scheduling.annotation.Async;
27 11
 import org.springframework.stereotype.Component;
28
-import org.slf4j.Logger;
29
-import org.slf4j.LoggerFactory;
12
+
13
+import java.text.SimpleDateFormat;
14
+import java.util.Collections;
15
+import java.util.Date;
16
+import java.util.List;
17
+import java.util.concurrent.ExecutorService;
30 18
 
31 19
 /**
32 20
  * 存储控制器数据
33 21
  */
34 22
 @Component
35
-public class MqttGenericConsumer {
23
+public class MqttGenericConsumer extends AbstractMqttConsumer {
36 24
 
37
-    private static final Logger log = LoggerFactory.getLogger(MqttGenericConsumer.class);
38
-    private static ExecutorService threadPool= Executors.newCachedThreadPool();
39
-    /**
40
-     *
41
-     * 添加mysql数据
42
-     *
43
-     */
44
-    @Autowired
45
-    private StringRedisTemplate stringRedisTemplate;
46
-    @Autowired
47
-    public SysControllerService sysControllerService;
48
-    @Autowired
49
-    public MqttDynamicConsumer messageListenerService2;
50
-
51
-    @Autowired
52
-    private IotProperties iotProperties;
53
-
54
-    // ========== 移除SSL,改用普通TCP连接 ==========
55
-    private String brokerUrl;
56
-    private String brokerHost;
57
-    private int brokerPort;
25
+    private final StringRedisTemplate stringRedisTemplate;
26
+    private final SysControllerService sysControllerService;
27
+    private final MqttDynamicConsumer messageListenerService2;
58 28
 
59
-    private static final int QOS = 1;
60
-    // 保留原有订阅主题
61
-    private static final String SUBSCRIBE_TOPIC = "+/generics";
62
-    private static final int CONNECT_TIMEOUT = 3000;
63
-    private static final int RECONNECT_INTERVAL = 5000;
29
+    private final ObjectMapper objectMapper = new ObjectMapper();
64 30
 
65
-    private String mqttUsername;
66
-    private String mqttPassword;
67
-
68
-    // 移除:SSL相关的CA_CERT_PATH常量(无需证书配置)
69
-    // 你的其他成员变量保持不变
70 31
     @Autowired
71
-    public TDengineService tDengineMapceshi2;
72
-    private MqttClient mqttClient;
73
-    private final ExecutorService executorService = Executors.newSingleThreadExecutor();
74
-    private boolean isMqttConnected = false;
75
-    private MqttConnectOptions connOpts;
76
-    private ObjectMapper objectMapper = new ObjectMapper();
77
-
78
-
79
-    // 移除:未定义的threadPool引用(避免编译错误)
80
-    @PostConstruct
81
-    public void connectAndSubscribe() {
82
-        this.brokerUrl = iotProperties.getMqtt().getBrokerUrl();
83
-        String brokerAddr = this.brokerUrl.replace("tcp://", "");
84
-        int colonIdx = brokerAddr.lastIndexOf(':');
85
-        this.brokerHost = brokerAddr.substring(0, colonIdx);
86
-        this.brokerPort = Integer.parseInt(brokerAddr.substring(colonIdx + 1));
87
-        this.mqttUsername = iotProperties.getMqtt().getUsername();
88
-        this.mqttPassword = iotProperties.getMqtt().getPassword();
89
-
90
-        try {
91
-            // 1. 检测普通MQTT服务器连通性(移除SSL,仅检测TCP端口)
92
-            checkServerAvailability();
93
-            // 2. 创建MQTT客户端(使用普通TCP的brokerUrl,保持逻辑不变)
94
-            String clientId = generateClientIdByOs();
95
-            mqttClient = new MqttClient(brokerUrl, clientId, new MemoryPersistence());
96
-            // 3. 初始化MQTT连接选项(移除SSL配置,新增账号密码)
97
-            initMqttConnectOptions();
98
-            // 4. 设置MQTT回调(保持核心逻辑,修复消息解析bug)
99
-            setMqttCallback();
100
-            // 5. 建立连接并订阅主题(保持逻辑不变,移除SSL日志标识)
101
-            connectAndSubscribeTopic();
102
-        } catch (MqttException | InterruptedException e) {
103
-            log.error("MQTT客户端初始化失败:", e);
104
-        }
32
+    public MqttGenericConsumer(@Qualifier("abstractConsumerExecutor") ExecutorService executorService,
33
+                               IotProperties iotProperties,
34
+                               StringRedisTemplate stringRedisTemplate,
35
+                               SysControllerService sysControllerService,
36
+                               MqttDynamicConsumer messageListenerService2) {
37
+        super(executorService, iotProperties);
38
+        this.stringRedisTemplate = stringRedisTemplate;
39
+        this.sysControllerService = sysControllerService;
40
+        this.messageListenerService2 = messageListenerService2;
105 41
     }
106 42
 
107
-    /**
108
-     * 检测普通MQTT服务器连通性(移除SSL,仅验证TCP端口可达性)
109
-     */
110
-    private void checkServerAvailability() throws InterruptedException {
111
-        boolean serverAvailable = false;
112
-        while (!serverAvailable) {
113
-            try (Socket socket = new Socket()) {
114
-                // 改用普通brokerHost和brokerPort
115
-                socket.connect(new InetSocketAddress(brokerHost, brokerPort), CONNECT_TIMEOUT);
116
-                serverAvailable = true;
117
-                log.info("普通MQTT服务器连通性检测通过");
118
-            } catch (Exception e) {
119
-                log.error("普通MQTT服务器不可达,5秒后重试...");
120
-                Thread.sleep(RECONNECT_INTERVAL);
121
-            }
122
-        }
123
-    }
124
-
125
-    /**
126
-     * 初始化MQTT连接选项(移除SSL配置,新增账号密码认证)
127
-     */
128
-    private void initMqttConnectOptions() {
129
-        connOpts = new MqttConnectOptions();
130
-        connOpts.setCleanSession(true);
131
-        connOpts.setAutomaticReconnect(true);
132
-        connOpts.setConnectionTimeout(10);
133
-
134
-        // ========== 新增:配置MQTT账号密码 ==========
135
-        connOpts.setUserName(mqttUsername);
136
-        connOpts.setPassword(mqttPassword.toCharArray()); // 密码要求传入char数组
137
-
138
-        // 移除:原有的SSL配置方法调用(configureSslAndStrictHostnameVerify())
43
+    @Override
44
+    protected String getSubscribeTopic() {
45
+        return "+/generics";
139 46
     }
140 47
 
141
-    /**
142
-     * 按操作系统生成唯一ClientId(保持原有逻辑不变)
143
-     */
144
-    private String generateClientIdByOs() {
48
+    @Override
49
+    protected String generateClientId() {
145 50
         String osName = System.getProperty("os.name").toLowerCase();
146
-        return osName.contains("windows") ? "mqttx_e216fbf1615" : "mqttx_e216fbf1616";
147
-    }
148
-
149
-    /**
150
-     * 设置MQTT回调函数(保留核心逻辑,修复2个关键bug)
151
-     */
152
-    private void setMqttCallback() {
153
-        mqttClient.setCallback(new MqttCallback() {
154
-            @Override
155
-            public void connectionLost(Throwable cause) {
156
-                log.error("MQTT连接断开,开始重连:" + cause.getMessage());
157
-                isMqttConnected = false;
158
-                reconnect();
159
-            }
160
-
161
-            @Override
162
-            public void messageArrived(String topic, MqttMessage message) throws Exception {
163
-                if (isMqttConnected) {
164
-                    executorService.submit(() -> {
165
-                        try {
166
-                            ObjectMapper objectMapper = new ObjectMapper();
167
-                            // 修复bug1:原代码先读取正确的messageContent,却误用mqtt(message.toString())解析
168
-                            // 正确读取消息负载(UTF-8编码,避免乱码)
169
-                            String messageContent = new String(message.getPayload(), "UTF-8");
170
-                            // 修复bug2:用正确的messageContent解析为ControllerData对象
171
-                            ControllerData controllerData = objectMapper.readValue(messageContent, ControllerData.class);
172
-                            // 业务处理
173
-                            triggermethod(controllerData);
174
-                        } catch (Exception e) {
175
-                            log.error("消息处理失败:", e);
176
-                        }
177
-                    });
178
-                }
179
-            }
180
-
181
-            @Override
182
-            public void deliveryComplete(IMqttDeliveryToken token) {
183
-                // 消息投递完成回调(无需处理)
184
-            }
185
-        });
51
+        String base = osName.contains("windows") ? "mqttx_e216fbf1615" : "mqttx_e216fbf1616";
52
+        return base + "_" + Long.toHexString(System.nanoTime()).substring(0, 6);
186 53
     }
187 54
 
188
-    /**
189
-     * 建立MQTT连接并订阅主题(保持逻辑不变,移除SSL日志标识)
190
-     */
191
-    private void connectAndSubscribeTopic() throws MqttException {
192
-        if (!mqttClient.isConnected()) {
193
-            IMqttToken connectToken = mqttClient.connectWithResult(connOpts);
194
-            if (connectToken.isComplete()) {
195
-                mqttClient.subscribe(SUBSCRIBE_TOPIC, QOS);
196
-                isMqttConnected = true;
197
-                log.info("MQTT连接成功,已订阅主题:" + SUBSCRIBE_TOPIC);
198
-            }
199
-        }
200
-    }
201
-
202
-    /**
203
-     * MQTT重连逻辑(移除SSL日志标识,保持功能不变)
204
-     */
205
-    public void reconnect() {
206
-        int maxReconnectAttempts = 3;
207
-        for (int attempt = 1; attempt <= maxReconnectAttempts; attempt++) {
208
-            try {
209
-                Thread.sleep(RECONNECT_INTERVAL);
210
-                if (mqttClient != null && !mqttClient.isConnected()) {
211
-                    mqttClient.connect(connOpts);
212
-                    mqttClient.subscribe(SUBSCRIBE_TOPIC, QOS);
213
-                    isMqttConnected = true;
214
-                    log.info("MQTT重连成功(第" + attempt + "次尝试)");
215
-                    break; // 重连成功后退出循环
216
-                }
217
-            } catch (MqttException | InterruptedException e) {
218
-                log.error("MQTT重连失败(第" + attempt + "次尝试):" + e.getMessage());
219
-                if (attempt == maxReconnectAttempts) {
220
-                    log.error("已达最大重连次数,停止重连");
221
-                }
222
-            }
223
-        }
224
-    }
225
-
226
-    /**
227
-     * 销毁时断开MQTT连接,关闭线程池(移除未定义的threadPool.shutdown())
228
-     */
229
-    @PreDestroy
230
-    public void disconnect() {
231
-        try {
232
-            if (mqttClient != null && mqttClient.isConnected()) {
233
-                mqttClient.disconnect();
234
-                mqttClient.close();
235
-                log.info("MQTT连接已断开");
236
-            }
237
-            executorService.shutdown();
238
-            // 移除:原代码中未定义的threadPool.shutdown();(避免编译错误)
239
-        } catch (MqttException e) {
240
-            log.error("MQTT断开连接失败:", e);
241
-        }
55
+    @Override
56
+    protected void handleMessage(String topic, String messageContent) throws Exception {
57
+        ControllerData controllerData = objectMapper.readValue(messageContent, ControllerData.class);
58
+        triggermethod(controllerData);
242 59
     }
243 60
 
244 61
     public void triggermethod(ControllerData weather) throws Exception {
245 62
         String timestamp = weather.getTimestamp();
246 63
         String fleetId = weather.getFleet_id();
247 64
         String controllerId = weather.getController_id();
65
+
66
+        if (controllerId == null || controllerId.isEmpty()) {
67
+            log.warn("ControllerData 缺少 controller_id,跳过处理");
68
+            return;
69
+        }
70
+        if (timestamp == null || timestamp.isEmpty()) {
71
+            log.warn("ControllerData 缺少 timestamp,跳过处理 controllerId={}", controllerId);
72
+            return;
73
+        }
74
+
248 75
         List<topics> topics = weather.getTopics();
76
+        if (topics == null) topics = Collections.emptyList();
249 77
         List<topics> cmdtopics = weather.getCmd_topics();
78
+        if (cmdtopics == null) cmdtopics = Collections.emptyList();
250 79
         topics faultprot = weather.getFault_prot();
251
-        //需要检索全部的数据是否存在,如果存在就进行修改,如果不存在就进行添加
252
-        Integer controllercountcount=0;
80
+
81
+        Integer controllercountcount = 0;
253 82
         for (topics topicsMap : topics) {
254
-            Integer count = sysControllerService.selectsyscontrollercount(topicsMap.getPath());
83
+            String path = topicsMap.getPath();
84
+            String name = topicsMap.getName();
85
+            if (path == null || path.isEmpty() || name == null) {
86
+                log.warn("Topic 数据不完整,跳过: path={}, name={}", path, name);
87
+                continue;
88
+            }
89
+            Integer count = sysControllerService.selectsyscontrollercount(path);
255 90
             if (count <= 0) {
256
-                //存储redis
257
-                stringRedisTemplate.persist(controllerId);
258
-                stringRedisTemplate.opsForHash().put(controllerId+":"+topicsMap.getName(), "path", topicsMap.getPath());
259
-                //将数据存储到mysql中
260
-                sysControllerService.insertsyscontroller(controllerId, timestamp, fleetId, topicsMap.getName(), topicsMap.getPath(),topicsMap.getPath().split("/")[1]);
91
+                stringRedisTemplate.opsForHash().put(controllerId + ":" + name, "path", path);
92
+                String deviceId = path.contains("/") ? path.split("/")[1] : "";
93
+                sysControllerService.insertsyscontroller(controllerId, timestamp, fleetId, name, path, deviceId);
261 94
                 controllercountcount++;
262
-            }else{
263
-                // 毫秒时间戳转换为秒级日期格式
264
-                long ts = Long.parseLong(timestamp);
265
-                String date = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
266
-                        .format(new java.util.Date(ts));
267
-                //修改数据库
268
-                sysControllerService.updatecontrollerAccept(controllerId, timestamp, fleetId, topicsMap.getName(), topicsMap.getPath(),topicsMap.getPath().split("/")[1],date);
95
+            } else {
96
+                long ts;
97
+                try {
98
+                    ts = Long.parseLong(timestamp);
99
+                } catch (NumberFormatException e) {
100
+                    log.warn("timestamp 格式错误: {},使用当前时间", timestamp);
101
+                    ts = System.currentTimeMillis();
102
+                }
103
+                String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(ts));
104
+                String deviceId = path.contains("/") ? path.split("/")[1] : "";
105
+                sysControllerService.updatecontrollerAccept(controllerId, timestamp, fleetId, name, path, deviceId, date);
269 106
             }
270 107
         }
271
-        Integer controllercountcmdcount=0;
108
+
272 109
         for (topics cmdtopicsMap : cmdtopics) {
273
-            //将数据存储到redis
274
-            Integer count = sysControllerService.selectsyscontrollercountcmd(cmdtopicsMap.getPath());
275
-            if (count<=0) {
276
-                stringRedisTemplate.opsForHash().put(controllerId+"_cmd:"+cmdtopicsMap.getName(), "path", cmdtopicsMap.getPath());
277
-                stringRedisTemplate.persist(controllerId);
278
-                sysControllerService.insertsyscontrollercmd(controllerId, timestamp, fleetId, cmdtopicsMap.getName(), cmdtopicsMap.getPath());
279
-                controllercountcmdcount++;
110
+            String path = cmdtopicsMap.getPath();
111
+            String name = cmdtopicsMap.getName();
112
+            if (path == null || path.isEmpty() || name == null) {
113
+                log.warn("CmdTopic 数据不完整,跳过: path={}, name={}", path, name);
114
+                continue;
115
+            }
116
+            Integer count = sysControllerService.selectsyscontrollercountcmd(path);
117
+            if (count <= 0) {
118
+                stringRedisTemplate.opsForHash().put(controllerId + "_cmd:" + name, "path", path);
119
+                sysControllerService.insertsyscontrollercmd(controllerId, timestamp, fleetId, name, path);
280 120
             }
281 121
         }
282
-        Integer count =sysControllerService.selectsyscontrollercountfault(faultprot.getPath());
283
-        if (count<=0){
284
-            stringRedisTemplate.opsForHash().put(controllerId+"_fault:"+faultprot.getName(), "path", faultprot.getPath());
285
-            sysControllerService.insertsyscontrollerfault(controllerId, timestamp, fleetId, faultprot.getName(), faultprot.getPath());
286
-            stringRedisTemplate.persist(controllerId);
122
+
123
+        if (faultprot != null) {
124
+            String path = faultprot.getPath();
125
+            String name = faultprot.getName();
126
+            if (path == null || path.isEmpty() || name == null) {
127
+                log.warn("FaultProt 数据不完整,跳过: path={}, name={}", path, name);
128
+            } else {
129
+                Integer count = sysControllerService.selectsyscontrollercountfault(path);
130
+                if (count <= 0) {
131
+                    stringRedisTemplate.opsForHash().put(controllerId + "_fault:" + name, "path", path);
132
+                    sysControllerService.insertsyscontrollerfault(controllerId, timestamp, fleetId, name, path);
133
+                }
134
+            }
287 135
         }
288
-        //如果控制器中的数据出现更新情况,就触发该方法进行重连
289
-        if (controllercountcount>0) {
290
-            //关闭连接线程
291
-            messageListenerService2.destroy();
292
-            //重新建立连接
293
-            messageListenerService2.initMqttConnection();
136
+
137
+        // 统一 persist controllerId,确保其不会被过期删除
138
+        stringRedisTemplate.persist(controllerId);
139
+
140
+        if (controllercountcount > 0) {
141
+            // TODO: 后续建议改为 Spring Event 解耦,避免直接调用其他 Bean 的方法
142
+            messageListenerService2.refreshMqttSubscription();
294 143
         }
295 144
     }
296 145
 }

+ 30
- 10
iot-platform/src/main/java/com/iot/platform/mqtt/MqttStatusConsumer.java Visa fil

@@ -1,15 +1,18 @@
1 1
 package com.iot.platform.mqtt;
2 2
 
3 3
 import com.fasterxml.jackson.databind.ObjectMapper;
4
+import com.iot.platform.config.IotProperties;
4 5
 import com.iot.platform.service.SysControllerService;
5 6
 import com.iot.platform.service.SysStatusService;
6 7
 import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.beans.factory.annotation.Qualifier;
7 9
 import org.springframework.data.redis.core.StringRedisTemplate;
8 10
 import org.springframework.stereotype.Component;
9 11
 
10 12
 import java.time.LocalDateTime;
11 13
 import java.time.format.DateTimeFormatter;
12 14
 import java.util.Map;
15
+import java.util.concurrent.ExecutorService;
13 16
 
14 17
 /**
15 18
  * 存储控制器状态数据
@@ -17,15 +20,24 @@ import java.util.Map;
17 20
 @Component
18 21
 public class MqttStatusConsumer extends AbstractMqttConsumer {
19 22
 
20
-    @Autowired
21
-    private StringRedisTemplate stringRedisTemplate;
22
-    @Autowired
23
-    public SysControllerService sysControllerService;
24
-    @Autowired
25
-    public SysStatusService sysStatusService;
23
+    private final StringRedisTemplate stringRedisTemplate;
24
+    private final SysControllerService sysControllerService;
25
+    private final SysStatusService sysStatusService;
26 26
 
27 27
     private final ObjectMapper objectMapper = new ObjectMapper();
28 28
 
29
+    @Autowired
30
+    public MqttStatusConsumer(@Qualifier("abstractConsumerExecutor") ExecutorService executorService,
31
+                              IotProperties iotProperties,
32
+                              StringRedisTemplate stringRedisTemplate,
33
+                              SysControllerService sysControllerService,
34
+                              SysStatusService sysStatusService) {
35
+        super(executorService, iotProperties);
36
+        this.stringRedisTemplate = stringRedisTemplate;
37
+        this.sysControllerService = sysControllerService;
38
+        this.sysStatusService = sysStatusService;
39
+    }
40
+
29 41
     @Override
30 42
     protected String getSubscribeTopic() {
31 43
         return "+/status";
@@ -34,7 +46,8 @@ public class MqttStatusConsumer extends AbstractMqttConsumer {
34 46
     @Override
35 47
     protected String generateClientId() {
36 48
         String osName = System.getProperty("os.name").toLowerCase();
37
-        return osName.contains("windows") ? "mqttx_e216fbf1613" : "mqttx_e216fbf1614";
49
+        String base = osName.contains("windows") ? "mqttx_e216fbf1613" : "mqttx_e216fbf1614";
50
+        return base + "_" + Long.toHexString(System.nanoTime()).substring(0, 6);
38 51
     }
39 52
 
40 53
     @Override
@@ -44,9 +57,16 @@ public class MqttStatusConsumer extends AbstractMqttConsumer {
44 57
     }
45 58
 
46 59
     public void triggermethod(Map<String, Object> weather) throws Exception {
47
-        String controllerId = weather.get("controller_id").toString();
48
-        String fleetId = weather.get("fleet_id").toString();
49
-        String status = weather.get("status").toString();
60
+        Object controllerIdObj = weather.get("controller_id");
61
+        Object fleetIdObj = weather.get("fleet_id");
62
+        Object statusObj = weather.get("status");
63
+        if (controllerIdObj == null || fleetIdObj == null || statusObj == null) {
64
+            log.warn("MQTT状态消息缺少必填字段,跳过处理");
65
+            return;
66
+        }
67
+        String controllerId = controllerIdObj.toString();
68
+        String fleetId = fleetIdObj.toString();
69
+        String status = statusObj.toString();
50 70
 
51 71
         LocalDateTime currentTime = LocalDateTime.now();
52 72
         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

+ 7
- 0
iot-platform/src/main/java/com/iot/platform/service/SysAlarmService.java Visa fil

@@ -5,14 +5,21 @@ import org.apache.ibatis.annotations.Param;
5 5
 import org.springframework.stereotype.Service;
6 6
 
7 7
 import javax.annotation.Resource;
8
+import java.util.regex.Pattern;
8 9
 
9 10
 @Service
10 11
 //@DataSource(value = DataSourceType.SLAVE)
11 12
 public class SysAlarmService {
13
+
14
+    private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$");
15
+
12 16
     @Resource
13 17
     public SysAlarmMapper sysAlarmMapper;
14 18
 
15 19
     public void insertalarm(String tableName,String faultId,String faultdescs,String faultstatus,String createtime,String messageType,String controllerId,String deviceId,String longitude,String latitude){
20
+        if (tableName == null || !TABLE_NAME_PATTERN.matcher(tableName).matches()) {
21
+            throw new IllegalArgumentException("非法表名: " + tableName);
22
+        }
16 23
         sysAlarmMapper.insertalarm(tableName,faultId, faultdescs, faultstatus, createtime, messageType,controllerId,deviceId,longitude,latitude);
17 24
     }
18 25
 

+ 13
- 6
iot-platform/src/main/java/com/iot/platform/service/SysControllerService.java Visa fil

@@ -8,9 +8,13 @@ import org.springframework.stereotype.Service;
8 8
 
9 9
 import javax.annotation.Resource;
10 10
 import java.util.List;
11
+import java.util.regex.Pattern;
11 12
 
12 13
 @Service
13 14
 public class SysControllerService {
15
+
16
+    private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$");
17
+
14 18
     @Resource
15 19
     public SysControllerMapper sysControllerMapper;
16 20
     public void insertsyscontroller(@Param("controllerId")String controllerId,
@@ -38,14 +42,14 @@ public class SysControllerService {
38 42
         sysControllerMapper.insertsyscontrollerfault(controllerId, timestamp, fleetId, name, path);
39 43
     }
40 44
 
41
-    public Integer selectsyscontrollercount(@Param("path")String paht){
42
-        return sysControllerMapper.selectsyscontrollercount(paht);
45
+    public Integer selectsyscontrollercount(@Param("path")String path){
46
+        return sysControllerMapper.selectsyscontrollercount(path);
43 47
     }
44
-    public Integer selectsyscontrollercountcmd(@Param("path")String paht){
45
-        return sysControllerMapper.selectsyscontrollercountcmd(paht);
48
+    public Integer selectsyscontrollercountcmd(@Param("path")String path){
49
+        return sysControllerMapper.selectsyscontrollercountcmd(path);
46 50
     }
47
-    public Integer selectsyscontrollercountfault(@Param("path")String paht){
48
-        return sysControllerMapper.selectsyscontrollercountfault(paht);
51
+    public Integer selectsyscontrollercountfault(@Param("path")String path){
52
+        return sysControllerMapper.selectsyscontrollercountfault(path);
49 53
     }
50 54
 
51 55
     public void updatecontrollerAccept(@Param("controllerId")String controllerId,
@@ -64,6 +68,9 @@ public class SysControllerService {
64 68
         return sysControllerMapper.selectall();
65 69
     }
66 70
     public SysDevice selectjingweidu(String tableName, String Name){
71
+        if (tableName == null || !TABLE_NAME_PATTERN.matcher(tableName).matches()) {
72
+            throw new IllegalArgumentException("非法表名: " + tableName);
73
+        }
67 74
         return sysControllerMapper.selectjingweidu(tableName, Name);
68 75
     }
69 76
 

+ 7
- 1
iot-platform/src/main/java/com/iot/platform/service/SysFaultService.java Visa fil

@@ -4,11 +4,14 @@ import com.iot.platform.mapper.SysFaultMapper;
4 4
 import org.apache.ibatis.annotations.Param;
5 5
 import org.springframework.stereotype.Service;
6 6
 
7
-
8 7
 import javax.annotation.Resource;
8
+import java.util.regex.Pattern;
9 9
 
10 10
 @Service
11 11
 public class SysFaultService {
12
+
13
+    private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$");
14
+
12 15
     @Resource
13 16
     public SysFaultMapper sysFaultMapper;
14 17
 
@@ -41,6 +44,9 @@ public class SysFaultService {
41 44
         return sysFaultMapper.selectfaultcount(fleetId);
42 45
     }
43 46
     public void createmessage(@Param("tableName")String tableName){
47
+        if (tableName == null || !TABLE_NAME_PATTERN.matcher(tableName).matches()) {
48
+            throw new IllegalArgumentException("非法表名: " + tableName);
49
+        }
44 50
         sysFaultMapper.createmessage(tableName);
45 51
     }
46 52
 

+ 5
- 1
iot-platform/src/main/java/com/iot/platform/service/SysIndicatorsService.java Visa fil

@@ -11,8 +11,12 @@ import org.springframework.stereotype.Service;
11 11
 @DataSource(value = DataSourceType.SLAVE)
12 12
 public class SysIndicatorsService {
13 13
 
14
+    private final SysIndicatorsMapper sysIndicatorsMapper;
15
+
14 16
     @Autowired
15
-    public SysIndicatorsMapper sysIndicatorsMapper;
17
+    public SysIndicatorsService(SysIndicatorsMapper sysIndicatorsMapper) {
18
+        this.sysIndicatorsMapper = sysIndicatorsMapper;
19
+    }
16 20
 
17 21
 
18 22
     public void insertindicators(Integer workordercount, Double profit, String carId,String createdata){

+ 6
- 1
iot-platform/src/main/java/com/iot/platform/service/SysWorkorderService.java Visa fil

@@ -9,8 +9,13 @@ import org.springframework.stereotype.Service;
9 9
 @Service
10 10
 @DataSource(value = DataSourceType.SLAVE)
11 11
 public class SysWorkorderService {
12
+
13
+    private final SysWorkorderMapper sysWorkorderMapper;
14
+
12 15
     @Autowired
13
-    public SysWorkorderMapper sysWorkorderMapper;
16
+    public SysWorkorderService(SysWorkorderMapper sysWorkorderMapper) {
17
+        this.sysWorkorderMapper = sysWorkorderMapper;
18
+    }
14 19
 
15 20
 
16 21
 

+ 50
- 50
iot-platform/src/main/java/com/iot/platform/service/TDegnineAlarm.java Visa fil

@@ -14,8 +14,11 @@ public class TDegnineAlarm {
14 14
 
15 15
     private static final Logger log = LoggerFactory.getLogger(TDegnineAlarm.class);
16 16
 
17
-    @Autowired
18
-    private TDengineService tdengineService;
17
+    private final TDengineService tdengineService;
18
+
19
+    public TDegnineAlarm(TDengineService tdengineService) {
20
+        this.tdengineService = tdengineService;
21
+    }
19 22
 
20 23
     private static final String VALID_FIELD_PATTERN = "^[a-zA-Z0-9_]+$";
21 24
 
@@ -37,6 +40,11 @@ public class TDegnineAlarm {
37 40
      * 根据标签获取表的名称,添加数据到TDengine时序数据中
38 41
      */
39 42
     public void shibaihou(Map<String, Object> shuzi, String supertablename, String table, String deviceid) throws SQLException {
43
+        // 白名单校验表名和超级表名(提前到获取连接前)
44
+        if (!isValidFieldName(supertablename) || !isValidFieldName(table)) {
45
+            throw new IllegalArgumentException("非法表名或超级表名: supertablename=" + supertablename + ", table=" + table);
46
+        }
47
+
40 48
         Connection conn = null;
41 49
         Statement stmt = null;
42 50
 
@@ -44,12 +52,6 @@ public class TDegnineAlarm {
44 52
             conn = tdengineService.getConnection();
45 53
             stmt = conn.createStatement();
46 54
 
47
-            // 白名单校验表名和超级表名
48
-            if (!isValidFieldName(supertablename) || !isValidFieldName(table)) {
49
-                log.warn("非法表名或超级表名: supertablename={}, table={}", supertablename, table);
50
-                return;
51
-            }
52
-
53 55
             // 解析数据value(安全转义)
54 56
             String value = shuzi.values().stream()
55 57
                     .map(obj -> "'" + escapeValue(obj == null ? "" : obj.toString()) + "'")
@@ -87,45 +89,45 @@ public class TDegnineAlarm {
87 89
             StringBuilder u = new StringBuilder();
88 90
 
89 91
             // ========== 查询超级表是否存在 ==========
90
-            boolean s = false;
92
+            boolean stableExists = false;
91 93
             List<String> tableNamesc = new ArrayList<>();
92 94
             try {
93 95
                 stmt.executeUpdate("use fault");
94
-                ResultSet resultSet = stmt.executeQuery("SHOW stables");
95
-                while (resultSet.next()) {
96
-                    tableNamesc.add(resultSet.getString(1));
96
+                try (ResultSet resultSet = stmt.executeQuery("SHOW stables")) {
97
+                    while (resultSet.next()) {
98
+                        tableNamesc.add(resultSet.getString(1));
99
+                    }
97 100
                 }
98
-                resultSet.close();
99 101
             } catch (SQLException e) {
100
-                log.warn("SHOW stables 失败: {}", e.getMessage());
102
+                throw new SQLException("SHOW stables 查询失败: " + e.getMessage(), e);
101 103
             }
102 104
             for (String ta : tableNamesc) {
103 105
                 if (supertablename.equalsIgnoreCase(ta)) {
104
-                    s = true;
106
+                    stableExists = true;
107
+                    break;
105 108
                 }
106 109
             }
107 110
 
108 111
             // ========== 超级表存在,查看子表是否存在 ==========
109
-            if (s) {
112
+            if (stableExists) {
110 113
                 String querySubTable = "select distinct tbname from " + wrapName(table)
111 114
                         + " where location='" + escapeValue(supertablename) + "'";
112
-                ResultSet rsc = stmt.executeQuery(querySubTable);
113
-                while (rsc.next()) {
114
-                    zibiaoname.add(rsc.getString(1));
115
+                try (ResultSet rsc = stmt.executeQuery(querySubTable)) {
116
+                    while (rsc.next()) {
117
+                        zibiaoname.add(rsc.getString(1));
118
+                    }
115 119
                 }
116
-                rsc.close();
117
-                boolean zibiaobu = zibiaoname.isEmpty();
120
+                boolean subTableMissing = zibiaoname.isEmpty();
118 121
 
119 122
                 // ========== 添加数据到数据库中 ==========
120
-                try {
121
-                    if (!zibiaobu) {
123
+                if (!subTableMissing) {
122 124
                         // 子表存在:查询列、新增差异列、插入数据
123
-                        ResultSet resultSet = stmt.executeQuery("describe " + wrapName(table));
124
-                        int columnNameIndex = resultSet.findColumn("field");
125
-                        while (resultSet.next()) {
126
-                            columnNames.add(resultSet.getString(columnNameIndex));
125
+                        try (ResultSet resultSet = stmt.executeQuery("describe " + wrapName(table))) {
126
+                            int columnNameIndex = resultSet.findColumn("field");
127
+                            while (resultSet.next()) {
128
+                                columnNames.add(resultSet.getString(columnNameIndex));
129
+                            }
127 130
                         }
128
-                        resultSet.close();
129 131
 
130 132
                         for (int z = 0; z < columnNames.size(); z++) {
131 133
                             u.append(columnNames.get(z));
@@ -148,8 +150,13 @@ public class TDegnineAlarm {
148 150
                                 try {
149 151
                                     stmt.executeUpdate("ALTER TABLE " + wrapName(supertablename)
150 152
                                             + " ADD COLUMN " + setCopys[ar]);
151
-                                } catch (Exception e) {
152
-                                    // 列可能已存在,忽略
153
+                                } catch (SQLException e) {
154
+                                    String msg = e.getMessage();
155
+                                    if (msg != null && (msg.contains("already exists") || msg.contains("Duplicate column") || msg.contains("TAG"))) {
156
+                                        log.debug("列已存在,忽略: {}", setCopys[ar]);
157
+                                    } else {
158
+                                        throw new SQLException("添加列失败: " + setCopys[ar] + " | " + msg, e);
159
+                                    }
153 160
                                 }
154 161
                             }
155 162
                         }
@@ -162,12 +169,12 @@ public class TDegnineAlarm {
162 169
                                 + " using " + wrapName(supertablename)
163 170
                                 + " tags('" + escapeValue(supertablename) + "')");
164 171
 
165
-                        ResultSet resultSet = stmt.executeQuery("describe " + wrapName(supertablename));
166
-                        int columnNameIndex = resultSet.findColumn("field");
167
-                        while (resultSet.next()) {
168
-                            columnNames.add(resultSet.getString(columnNameIndex));
172
+                        try (ResultSet resultSet = stmt.executeQuery("describe " + wrapName(supertablename))) {
173
+                            int columnNameIndex = resultSet.findColumn("field");
174
+                            while (resultSet.next()) {
175
+                                columnNames.add(resultSet.getString(columnNameIndex));
176
+                            }
169 177
                         }
170
-                        resultSet.close();
171 178
 
172 179
                         for (int z = 0; z < columnNames.size(); z++) {
173 180
                             if (z < columnNames.size() - 1) {
@@ -188,23 +195,16 @@ public class TDegnineAlarm {
188 195
                         stmt.executeUpdate("insert into " + wrapName(table)
189 196
                                 + "(ts," + inkey + ")values(now()," + value + ")");
190 197
                     }
191
-                } catch (SQLException e) {
192
-                    log.error("shibaihou 数据处理异常: {}", e.getMessage());
193
-                }
194 198
             } else {
195 199
                 // ========== 超级表不存在:创建超级表、子表、插入数据 ==========
196
-                try {
197
-                    stmt.executeUpdate("create stable " + wrapName(supertablename)
198
-                            + " (ts timestamp," + tia + " ) TAGS (location binary(64))");
199
-                    stmt.executeUpdate("create table " + wrapName(table)
200
-                            + " using " + wrapName(supertablename)
201
-                            + " tags('" + escapeValue(supertablename) + "')");
202
-                    log.debug("insert into {}(ts,{})values(now(),{})", table, inkey, value);
203
-                    stmt.executeUpdate("insert into " + wrapName(table)
204
-                            + "(ts," + inkey + ")values(now()," + value + ")");
205
-                } catch (SQLException e) {
206
-                    log.error("shibaihou 创建超级表异常: {}", e.getMessage());
207
-                }
200
+                stmt.executeUpdate("create stable " + wrapName(supertablename)
201
+                        + " (ts timestamp," + tia + " ) TAGS (location binary(64))");
202
+                stmt.executeUpdate("create table " + wrapName(table)
203
+                        + " using " + wrapName(supertablename)
204
+                        + " tags('" + escapeValue(supertablename) + "')");
205
+                log.debug("insert into {}(ts,{})values(now(),{})", table, inkey, value);
206
+                stmt.executeUpdate("insert into " + wrapName(table)
207
+                        + "(ts," + inkey + ")values(now()," + value + ")");
208 208
             }
209 209
         } finally {
210 210
             if (stmt != null) {

+ 41
- 51
iot-platform/src/main/java/com/iot/platform/service/TDengineService.java Visa fil

@@ -20,21 +20,21 @@ import com.fasterxml.jackson.databind.ObjectMapper;
20 20
 public class TDengineService {
21 21
     private static final Logger log = LoggerFactory.getLogger(TDengineService.class);
22 22
 
23
-    @Autowired
24
-    private IotProperties iotProperties;
25
-
23
+    private final IotProperties iotProperties;
26 24
     private final ExecutorService batchExecutor;
27 25
 
28 26
     @Autowired
29
-    public TDengineService(ExecutorService tdengineBatchExecutor) {
27
+    public TDengineService(ExecutorService tdengineBatchExecutor, IotProperties iotProperties) {
30 28
         this.batchExecutor = tdengineBatchExecutor;
29
+        this.iotProperties = iotProperties;
31 30
     }
32 31
 
33 32
     private HikariDataSource dataSource;
34
-    private boolean dataSourceInitialized = false;
33
+    private volatile boolean dataSourceInitialized = false;
35 34
 
36 35
     // === 新增:缓存超级表结构 (key = dbName.stableName) ===
37 36
     private final Map<String, Set<String>> stableColumnCache = new ConcurrentHashMap<>();
37
+    private static final int MAX_CACHE_SIZE = 1000;
38 38
 
39 39
     // JSON 列名,用于存储所有动态字段
40 40
     private static final String JSON_COLUMN_NAME = "ext_data";
@@ -61,10 +61,10 @@ public class TDengineService {
61 61
             config.setConnectionTestQuery("SELECT NOW()");
62 62
             config.setValidationTimeout(3000);
63 63
             this.dataSource = new HikariDataSource(config);
64
-            log.info("TDengine 连接池初始化完成");
64
+            log.info("TDengine 连接池初始化完成");
65 65
         } catch (Exception e) {
66
-            log.warn("⚠️ TDengine 连接池初始化失败: {}", e.getMessage());
67
-            this.dataSource = null;
66
+            log.error("TDengine 连接池初始化失败: {}", e.getMessage());
67
+            throw new IllegalStateException("TDengine 连接池初始化失败", e);
68 68
         }
69 69
         dataSourceInitialized = true;
70 70
     }
@@ -130,6 +130,10 @@ public class TDengineService {
130 130
         // 缓存未命中,查 DB
131 131
         Set<String> columns = loadStableColumnsFromDB(dbName, stableName);
132 132
         if (!columns.isEmpty()) {
133
+            if (stableColumnCache.size() >= MAX_CACHE_SIZE) {
134
+                stableColumnCache.clear();
135
+                log.warn("TDengine 超级表缓存已达上限({}),已清空", MAX_CACHE_SIZE);
136
+            }
133 137
             stableColumnCache.put(key, columns);
134 138
         }
135 139
         return columns;
@@ -165,7 +169,7 @@ public class TDengineService {
165 169
     // ==========================================
166 170
     // 初始化表结构
167 171
     // ==========================================
168
-    private boolean initTableStructure(String dbName, String supertablename, String table, Set<String> fieldNames) {
172
+    private void initTableStructure(String dbName, String supertablename, String table, Set<String> fieldNames) throws SQLException {
169 173
         Connection conn = null;
170 174
         Statement stmt = null;
171 175
         try {
@@ -186,6 +190,10 @@ public class TDengineService {
186 190
 
187 191
             // 更新缓存:固定列
188 192
             String key = getStableKey(dbName, supertablename);
193
+            if (stableColumnCache.size() >= MAX_CACHE_SIZE) {
194
+                stableColumnCache.clear();
195
+                log.warn("TDengine 超级表缓存已达上限({}),已清空", MAX_CACHE_SIZE);
196
+            }
189 197
             Set<String> fixedCols = new HashSet<>();
190 198
             fixedCols.add("ts");
191 199
             fixedCols.add("surfacename");
@@ -202,11 +210,6 @@ public class TDengineService {
202 210
             );
203 211
             stmt.executeUpdate(tableSql);
204 212
 
205
-            return true;
206
-
207
-        } catch (SQLException e) {
208
-            log.error("❌ 表结构初始化失败: {}.{} | {}", dbName, table, e.getMessage());
209
-            return false;
210 213
         } finally {
211 214
             if (stmt != null) try { stmt.close(); } catch (SQLException ignored) {}
212 215
             closeConnection(conn);
@@ -216,8 +219,8 @@ public class TDengineService {
216 219
     // ==========================================
217 220
     // 批量插入
218 221
     // ==========================================
219
-    public boolean insertBatch(String dbName, String table, List<Map<String, Object>> dataList) throws Exception {
220
-        if (dataList == null || dataList.isEmpty()) return true;
222
+    public void insertBatch(String dbName, String table, List<Map<String, Object>> dataList) throws SQLException {
223
+        if (dataList == null || dataList.isEmpty()) return;
221 224
 
222 225
         String supertablename = table.contains("_") ? table.substring(0, table.lastIndexOf('_')) : table;
223 226
 
@@ -233,19 +236,16 @@ public class TDengineService {
233 236
             int end = Math.min(start + batchSize, dataList.size());
234 237
             List<Map<String, Object>> batch = dataList.subList(start, end);
235 238
 
236
-            if (!insertBatchInternal(dbName, supertablename, table, batch)) {
237
-                return false;
238
-            }
239
+            insertBatchInternal(dbName, supertablename, table, batch);
239 240
         }
240 241
 
241
-        log.info("✅ 批量写入成功: {} | 条数: {}", table, dataList.size());
242
-        return true;
242
+        log.info("批量写入成功: {} | 条数: {}", table, dataList.size());
243 243
     }
244 244
 
245 245
     /**
246 246
      * 内部方法:插入一批数据
247 247
      */
248
-    private boolean insertBatchInternal(String dbName, String supertablename, String table, List<Map<String, Object>> dataList) {
248
+    private void insertBatchInternal(String dbName, String supertablename, String table, List<Map<String, Object>> dataList) throws SQLException {
249 249
         // 确保表存在(可能有竞态条件)
250 250
         ensureTableExists(dbName, supertablename, table);
251 251
 
@@ -266,7 +266,7 @@ public class TDengineService {
266 266
             hasData = true;
267 267
         }
268 268
 
269
-        if (!hasData) return true;
269
+        if (!hasData) return;
270 270
 
271 271
         sqlBuilder.setLength(sqlBuilder.length() - 1);
272 272
         String finalSql = sqlBuilder.toString();
@@ -278,16 +278,15 @@ public class TDengineService {
278 278
             stmt = conn.createStatement();
279 279
             stmt.setQueryTimeout(30);
280 280
             stmt.executeUpdate(finalSql);
281
-            return true;
282 281
         } catch (SQLException e) {
283 282
             // 表不存在时尝试重建表后重试
284 283
             if (e.getMessage().contains("Table does not exist")) {
285
-                log.warn("⚠️ 表不存在,重建表: {}", table);
284
+                log.warn("表不存在,重建表: {}", table);
286 285
                 initTableStructure(dbName, supertablename, table, Collections.emptySet());
287
-                return insertBatchRetry(dbName, supertablename, table, dataList);
286
+                insertBatchRetry(dbName, supertablename, table, dataList);
287
+                return;
288 288
             }
289
-            log.error("❌ 批量写入 SQL 失败: {} | 错误: {}", table, e.getMessage());
290
-            return false;
289
+            throw new SQLException("批量写入 SQL 失败: " + table + " | 错误: " + e.getMessage(), e);
291 290
         } finally {
292 291
             if (stmt != null) try { stmt.close(); } catch (SQLException ignored) {}
293 292
             closeConnection(conn);
@@ -297,7 +296,7 @@ public class TDengineService {
297 296
     /**
298 297
      * 重试插入(表重建后)
299 298
      */
300
-    private boolean insertBatchRetry(String dbName, String supertablename, String table, List<Map<String, Object>> dataList) {
299
+    private void insertBatchRetry(String dbName, String supertablename, String table, List<Map<String, Object>> dataList) throws SQLException {
301 300
         StringBuilder sqlBuilder = new StringBuilder();
302 301
         sqlBuilder.append("INSERT INTO ").append(wrapName(dbName)).append(".").append(wrapName(table))
303 302
                 .append(" (ts, surfacename, ext_data) VALUES ");
@@ -312,7 +311,7 @@ public class TDengineService {
312 311
             hasData = true;
313 312
         }
314 313
 
315
-        if (!hasData) return true;
314
+        if (!hasData) return;
316 315
 
317 316
         sqlBuilder.setLength(sqlBuilder.length() - 1);
318 317
         String finalSql = sqlBuilder.toString();
@@ -324,10 +323,8 @@ public class TDengineService {
324 323
             stmt = conn.createStatement();
325 324
             stmt.setQueryTimeout(30);
326 325
             stmt.executeUpdate(finalSql);
327
-            return true;
328 326
         } catch (SQLException e) {
329
-            log.error("❌ 重试插入失败: {} | 错误: {}", table, e.getMessage());
330
-            return false;
327
+            throw new SQLException("重试插入失败: " + table + " | 错误: " + e.getMessage(), e);
331 328
         } finally {
332 329
             if (stmt != null) try { stmt.close(); } catch (SQLException ignored) {}
333 330
             closeConnection(conn);
@@ -337,7 +334,7 @@ public class TDengineService {
337 334
     /**
338 335
      * 确保表存在(检查+创建)
339 336
      */
340
-    private void ensureTableExists(String dbName, String supertablename, String table) {
337
+    private void ensureTableExists(String dbName, String supertablename, String table) throws SQLException {
341 338
         Connection conn = null;
342 339
         Statement stmt = null;
343 340
         ResultSet rs = null;
@@ -381,10 +378,10 @@ public class TDengineService {
381 378
                         escapeValue(supertablename)
382 379
                 );
383 380
                 stmt.executeUpdate(tableSql);
384
-                log.info("子表创建成功: {}", table);
381
+                log.info("子表创建成功: {}", table);
385 382
             }
386 383
         } catch (SQLException e) {
387
-            log.warn("检查表存在性失败,继续尝试插入: {}", e.getMessage());
384
+            throw new SQLException("检查表存在性失败: " + dbName + "." + table + " | " + e.getMessage(), e);
388 385
         } finally {
389 386
             closeQuietly(rs, stmt);
390 387
             closeConnection(conn);
@@ -411,8 +408,7 @@ public class TDengineService {
411 408
         try {
412 409
             return objectMapper.writeValueAsString(dynamic);
413 410
         } catch (Exception e) {
414
-            log.warn("JSON 序列化失败,返回空对象: {}", e.getMessage());
415
-            return "{}";
411
+            throw new IllegalStateException("JSON 序列化失败,数据可能丢失: " + dynamic, e);
416 412
         }
417 413
     }
418 414
 
@@ -423,27 +419,21 @@ public class TDengineService {
423 419
         if (data == null || data.isEmpty()) return "";
424 420
         try {
425 421
             ByteArrayOutputStream bos = new ByteArrayOutputStream();
426
-            GZIPOutputStream gzip = new GZIPOutputStream(bos);
427
-            gzip.write(data.getBytes("UTF-8"));
428
-            gzip.close();
422
+            try (GZIPOutputStream gzip = new GZIPOutputStream(bos)) {
423
+                gzip.write(data.getBytes("UTF-8"));
424
+            }
429 425
             byte[] compressed = Base64.getEncoder().encode(bos.toByteArray());
430 426
             return new String(compressed);
431 427
         } catch (Exception e) {
432
-            log.warn("GZIP 压缩失败,使用原始数据: {}", e.getMessage());
433
-            return data;
428
+            throw new IllegalStateException("GZIP 压缩失败: " + e.getMessage(), e);
434 429
         }
435 430
     }
436 431
 
437 432
     @Deprecated
438
-    public boolean addToBatch(String dbName, String table, Map<String, Object> dataMap) {
433
+    public void addToBatch(String dbName, String table, Map<String, Object> dataMap) throws SQLException {
439 434
         List<Map<String, Object>> list = new ArrayList<>();
440 435
         list.add(dataMap);
441
-        try {
442
-            return insertBatch(dbName, table, list);
443
-        } catch (Exception e) {
444
-            log.error("单条转批量写入失败", e);
445
-            return false;
446
-        }
436
+        insertBatch(dbName, table, list);
447 437
     }
448 438
 
449 439
     // ==========================================
@@ -451,7 +441,7 @@ public class TDengineService {
451 441
     // ==========================================
452 442
     public void clearStableColumnCache() {
453 443
         stableColumnCache.clear();
454
-        log.info("🧹 清除了 TDengine 超级表结构缓存");
444
+        log.info("清除了 TDengine 超级表结构缓存");
455 445
     }
456 446
 
457 447
     public void close() {

+ 112
- 76
iot-platform/src/main/java/com/iot/platform/task/VehicleSyncTask.java Visa fil

@@ -4,6 +4,7 @@ import com.iot.platform.domain.SysCar;
4 4
 import com.iot.platform.domain.SysCompany;
5 5
 import com.iot.platform.domain.SysDevice;
6 6
 import com.iot.platform.domain.SysDeviceControl;
7
+import com.iot.platform.config.IotProperties;
7 8
 import com.iot.platform.service.*;
8 9
 import org.slf4j.Logger;
9 10
 import org.slf4j.LoggerFactory;
@@ -28,26 +29,42 @@ public class VehicleSyncTask {
28 29
 
29 30
     private static final Logger log = LoggerFactory.getLogger(VehicleSyncTask.class);
30 31
 
32
+    private final SysCarService sysCarService;
33
+    private final SysDeviceService sysDeviceService;
34
+    private final StringRedisTemplate stringRedisTemplate;
35
+    private final SysrealtimeService sysrealtimeService;
36
+    private final SysDeviceVoService sysDeviceVoService;
37
+    private final SysDeviceControlService sysDeviceControlService;
38
+    private final SysWorkorderService sysWorkorderService;
39
+    private final SysIndicatorsService sysIndicatorsService;
40
+    private final SysCompanyService sysCompanyService;
41
+    private final RestTemplate restTemplate;
42
+    private final IotProperties iotProperties;
43
+
31 44
     @Autowired
32
-    public SysCarService sysCarService;
33
-    @Autowired
34
-    public SysDeviceService sysDeviceService;
35
-    @Autowired
36
-    private StringRedisTemplate stringRedisTemplate;
37
-    @Autowired
38
-    private SysrealtimeService sysrealtimeService;
39
-    @Autowired
40
-    public SysDeviceVoService sysDeviceVoService;
41
-    @Autowired
42
-    public SysDeviceControlService sysDeviceControlService;
43
-    @Autowired
44
-    public SysWorkorderService sysWorkorderService;
45
-    @Autowired
46
-    public SysIndicatorsService sysIndicatorsService;
47
-    @Autowired
48
-    public SysCompanyService sysCompanyService;
49
-    @Autowired
50
-    private RestTemplate restTemplate;
45
+    public VehicleSyncTask(SysCarService sysCarService,
46
+                           SysDeviceService sysDeviceService,
47
+                           StringRedisTemplate stringRedisTemplate,
48
+                           SysrealtimeService sysrealtimeService,
49
+                           SysDeviceVoService sysDeviceVoService,
50
+                           SysDeviceControlService sysDeviceControlService,
51
+                           SysWorkorderService sysWorkorderService,
52
+                           SysIndicatorsService sysIndicatorsService,
53
+                           SysCompanyService sysCompanyService,
54
+                           RestTemplate restTemplate,
55
+                           IotProperties iotProperties) {
56
+        this.sysCarService = sysCarService;
57
+        this.sysDeviceService = sysDeviceService;
58
+        this.stringRedisTemplate = stringRedisTemplate;
59
+        this.sysrealtimeService = sysrealtimeService;
60
+        this.sysDeviceVoService = sysDeviceVoService;
61
+        this.sysDeviceControlService = sysDeviceControlService;
62
+        this.sysWorkorderService = sysWorkorderService;
63
+        this.sysIndicatorsService = sysIndicatorsService;
64
+        this.sysCompanyService = sysCompanyService;
65
+        this.restTemplate = restTemplate;
66
+        this.iotProperties = iotProperties;
67
+    }
51 68
 
52 69
     private boolean tryLock(String lockKey, long expireSeconds) {
53 70
         Boolean acquired = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", expireSeconds, TimeUnit.SECONDS);
@@ -55,7 +72,10 @@ public class VehicleSyncTask {
55 72
     }
56 73
 
57 74
     private void unlock(String lockKey) {
58
-        stringRedisTemplate.delete(lockKey);
75
+        Boolean deleted = stringRedisTemplate.delete(lockKey);
76
+        if (!Boolean.TRUE.equals(deleted)) {
77
+            log.warn("分布式锁释放失败: {}", lockKey);
78
+        }
59 79
     }
60 80
 
61 81
     /**
@@ -79,25 +99,33 @@ public class VehicleSyncTask {
79 99
     private void doUpdateSysCar() {
80 100
         List<SysCar> sysCarList = sysCarService.selectcontrollerId();
81 101
         for (SysCar sysCar : sysCarList) {
82
-            if (sysCar.getControllerId() == null || sysCar.getControllerId().isEmpty()) {
83
-                continue;
84
-            }
85
-            SysDevice latitude = sysDeviceService.selectsysdevice(sysCar.getControllerId(), "纬度");
86
-            SysDevice longitude = sysDeviceService.selectsysdevice(sysCar.getControllerId(), "经度");
102
+            try {
103
+                if (sysCar.getControllerId() == null || sysCar.getControllerId().isEmpty()) {
104
+                    continue;
105
+                }
106
+                SysDevice latitude = sysDeviceService.selectsysdevice(sysCar.getControllerId(), "纬度");
107
+                SysDevice longitude = sysDeviceService.selectsysdevice(sysCar.getControllerId(), "经度");
87 108
 
88
-            String redisKeyPattern = "workorder:coordinate:" + sysCar.getControllerId() + ":*";
89
-            Set<String> keys = scanKeys(redisKeyPattern);
109
+                String redisKeyPattern = "workorder:coordinate:" + sysCar.getControllerId() + ":*";
110
+                Set<String> keys = scanKeys(redisKeyPattern);
90 111
 
91
-            if (keys == null || keys.isEmpty()) {
92
-                updateCarPosition(sysCar, latitude, longitude);
93
-            } else {
94
-                for (String key : keys) {
95
-                    Map<Object, Object> coordinateMap = stringRedisTemplate.opsForHash().entries(key);
96
-                    if (coordinateMap.get("latitude").equals(latitude.getV()) && coordinateMap.get("longitude").equals(longitude.getV())) {
97
-                        continue;
98
-                    }
112
+                if (keys == null || keys.isEmpty()) {
99 113
                     updateCarPosition(sysCar, latitude, longitude);
114
+                } else {
115
+                    for (String key : keys) {
116
+                        Map<Object, Object> coordinateMap = stringRedisTemplate.opsForHash().entries(key);
117
+                        Object cachedLat = coordinateMap.get("latitude");
118
+                        Object cachedLon = coordinateMap.get("longitude");
119
+                        if (cachedLat != null && cachedLat.equals(latitude.getV()) && cachedLon != null && cachedLon.equals(longitude.getV())) {
120
+                            continue;
121
+                        }
122
+                        updateCarPosition(sysCar, latitude, longitude);
123
+                    }
100 124
                 }
125
+            } catch (DataAccessException e) {
126
+                log.error("更新车辆位置失败 carId={}: {}", sysCar.getCarId(), e.getMessage(), e);
127
+            } catch (Exception e) {
128
+                log.error("更新车辆位置异常 carId={}: {}", sysCar.getCarId(), e.getMessage(), e);
101 129
             }
102 130
         }
103 131
     }
@@ -110,7 +138,7 @@ public class VehicleSyncTask {
110 138
 
111 139
         String position = latitude.getV() + "," + longitude.getV();
112 140
         sysCarService.updatecarposition(position, sysCar.getCarId());
113
-        String url = "https://esos-iot.com:9443/syscar/trigger?carId=" + sysCar.getCarId();
141
+        String url = iotProperties.getMqtt().getVehicleTriggerUrl() + "?carId=" + sysCar.getCarId();
114 142
         try {
115 143
             restTemplate.postForObject(url, null, String.class);
116 144
         } catch (RestClientException e) {
@@ -130,8 +158,10 @@ public class VehicleSyncTask {
130 158
                 cursor.close();
131 159
                 return null;
132 160
             });
161
+        } catch (RedisConnectionFailureException e) {
162
+            log.warn("Redis SCAN 连接失败 pattern={}: {}", pattern, e.getMessage());
133 163
         } catch (Exception e) {
134
-            log.error("Redis SCAN失败 pattern={}: {}", pattern, e.getMessage());
164
+            log.error("Redis SCAN 失败 pattern={}: {}", pattern, e.getMessage(), e);
135 165
         }
136 166
         return keys;
137 167
     }
@@ -185,13 +215,16 @@ public class VehicleSyncTask {
185 215
                             String fieldvalue = entry.getValue().toString();
186 216
                             if (sysDeviceControlList.get(i).getControllerName().equals(fieldKey)) {
187 217
                                 keyvalue.append(fieldKey).append("=/'").append(fieldvalue).append("/'");
188
-                                if (i > sysDeviceControlList.size() - 1) {
218
+                                if (i < sysDeviceControlList.size() - 1) {
189 219
                                     keyvalue.append(",");
190 220
                                 }
191 221
                             }
192 222
                         }
193 223
                     }
194
-                    sysDeviceVoService.updatesysdevice(keyvalue.toString(), controllerId);
224
+                    boolean updated = sysDeviceVoService.updatesysdevice(keyvalue.toString(), controllerId);
225
+                    if (!updated) {
226
+                        log.warn("更新设备配置失败: controllerId={}", controllerId);
227
+                    }
195 228
                 } else {
196 229
                     StringBuilder key = new StringBuilder();
197 230
                     StringBuilder value = new StringBuilder();
@@ -202,14 +235,17 @@ public class VehicleSyncTask {
202 235
                             if (sysDeviceControlList.get(i).getControllerName().equals(fieldKey)) {
203 236
                                 key.append(fieldKey);
204 237
                                 value.append(fieldvalue);
205
-                                if (i > sysDeviceControlList.size() - 1) {
238
+                                if (i < sysDeviceControlList.size() - 1) {
206 239
                                     key.append(",");
207 240
                                     value.append(",");
208 241
                                 }
209 242
                             }
210 243
                         }
211 244
                     }
212
-                    sysDeviceVoService.insertdevice(key.toString(), value.toString());
245
+                    boolean inserted = sysDeviceVoService.insertdevice(key.toString(), value.toString());
246
+                    if (!inserted) {
247
+                        log.warn("插入设备配置失败: controllerId={}", controllerId);
248
+                    }
213 249
                 }
214 250
             }
215 251
         } catch (RedisConnectionFailureException e) {
@@ -307,40 +343,40 @@ public class VehicleSyncTask {
307 343
      * 更新指标信息
308 344
      * 根据公司去查询
309 345
      */
310
-    @Scheduled(fixedDelay = 30000)
311
-    public void insertIndicators() {
312
-        String lockKey = "lock:vehicle-sync:insertIndicators";
313
-        if (!tryLock(lockKey, 60)) {
314
-            log.debug("获取锁失败,跳过本次执行: {}", lockKey);
315
-            return;
316
-        }
317
-        try {
318
-            doInsertIndicators();
319
-        } finally {
320
-            unlock(lockKey);
321
-        }
322
-    }
346
+//    @Scheduled(fixedDelay = 30000)
347
+//    public void insertIndicators() {
348
+//        String lockKey = "lock:vehicle-sync:insertIndicators";
349
+//        if (!tryLock(lockKey, 60)) {
350
+//            log.debug("获取锁失败,跳过本次执行: {}", lockKey);
351
+//            return;
352
+//        }
353
+//        try {
354
+//            doInsertIndicators();
355
+//        } finally {
356
+//            unlock(lockKey);
357
+//        }
358
+//    }
323 359
 
324
-    private void doInsertIndicators() {
325
-        try {
326
-            LocalDate today = LocalDate.now();
327
-            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
328
-            String day = today.format(formatter);
329
-            List<SysCompany> sysCompanyList = sysCompanyService.selectcompany();
330
-            for (SysCompany sysCompany : sysCompanyList) {
331
-                Integer countworkorder = sysWorkorderService.selectworkordercount(sysCompany.getCompanyId(), day);
332
-                Double countprofit = sysWorkorderService.selectwokroderprofit(sysCompany.getCompanyId(), day);
333
-                Integer count = sysIndicatorsService.selectcarcount(sysCompany.getCompanyId(), day);
334
-                if (count <= 0) {
335
-                    sysIndicatorsService.insertindicators(countworkorder, countprofit, sysCompany.getCompanyId(), day);
336
-                } else {
337
-                    sysIndicatorsService.updateindicators(countworkorder, countprofit, sysCompany.getCompanyId(), day);
338
-                }
339
-            }
340
-        } catch (DataAccessException e) {
341
-            log.error("数据库操作失败: {}", e.getMessage(), e);
342
-        } catch (Exception e) {
343
-            log.error("更新指标信息失败: {}", e.getMessage(), e);
344
-        }
345
-    }
360
+//    private void doInsertIndicators() {
361
+//        try {
362
+//            LocalDate today = LocalDate.now();
363
+//            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
364
+//            String day = today.format(formatter);
365
+//            List<SysCompany> sysCompanyList = sysCompanyService.selectcompany();
366
+//            for (SysCompany sysCompany : sysCompanyList) {
367
+//                Integer countworkorder = sysWorkorderService.selectworkordercount(sysCompany.getCompanyId(), day);
368
+//                Double countprofit = sysWorkorderService.selectwokroderprofit(sysCompany.getCompanyId(), day);
369
+//                Integer count = sysIndicatorsService.selectcarcount(sysCompany.getCompanyId(), day);
370
+//                if (count <= 0) {
371
+//                    sysIndicatorsService.insertindicators(countworkorder, countprofit, sysCompany.getCompanyId(), day);
372
+//                } else {
373
+//                    sysIndicatorsService.updateindicators(countworkorder, countprofit, sysCompany.getCompanyId(), day);
374
+//                }
375
+//            }
376
+//        } catch (DataAccessException e) {
377
+//            log.error("数据库操作失败: {}", e.getMessage(), e);
378
+//        } catch (Exception e) {
379
+//            log.error("更新指标信息失败: {}", e.getMessage(), e);
380
+//        }
381
+//    }
346 382
 }

+ 3
- 3
iot-platform/src/main/resources/application-druid.yml Visa fil

@@ -8,14 +8,14 @@ spring:
8 8
             master:
9 9
                 url: jdbc:mysql://47.104.204.180:3306/data?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
10 10
                 username: ${MYSQL_USERNAME:root}
11
-                password: ${MYSQL_PASSWORD:Zhu059300()__}
11
+                password: ${MYSQL_PASSWORD}
12 12
             # 从库数据源
13 13
             slave:
14 14
                 # 从数据源开关/默认关闭
15 15
                 enabled: true
16 16
                 url: jdbc:mysql://47.104.204.180:3306/cnc?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
17 17
                 username: ${MYSQL_USERNAME:root}
18
-                password: ${MYSQL_PASSWORD:Zhu059300()__}
18
+                password: ${MYSQL_PASSWORD}
19 19
             # 初始连接数
20 20
             initialSize: 5
21 21
             # 最小连接池数量
@@ -48,7 +48,7 @@ spring:
48 48
                 url-pattern: /druid/*
49 49
                 # 控制台管理用户名和密码
50 50
                 login-username: ${DRUID_USERNAME:ruoyi}
51
-                login-password: ${DRUID_PASSWORD:123456}
51
+                login-password: ${DRUID_PASSWORD}
52 52
             filter:
53 53
                 stat:
54 54
                     enabled: true

+ 1
- 0
iot-platform/src/main/resources/application.yml Visa fil

@@ -68,6 +68,7 @@ iot:
68 68
     broker-url: tcp://47.104.204.180:1883
69 69
     username: ${MQTT_USERNAME:}
70 70
     password: ${MQTT_PASSWORD:}
71
+    charge-station-topic: ${MQTT_CHARGE_STATION_TOPIC:station/ChargeStation/device/+/post/json}
71 72
   tdengine:
72 73
     url: jdbc:TAOS://localhost:6030/
73 74
     username: ${TDENGINE_USERNAME:}

+ 1
- 1
iot-platform/src/main/resources/mapper/SysFaultMapper.xml Visa fil

@@ -30,7 +30,7 @@
30 30
     </insert>
31 31
 
32 32
     <select id="selectfaultcount" resultType="Integer">
33
-        select cout(*) from alert_data where  device_id=#{deviceId}
33
+        select count(*) from alert_data where device_id=#{deviceId}
34 34
     </select>
35 35
     <update id="createmessage">
36 36
         CREATE TABLE `${tableName}` (

+ 0
- 20
iot-platform/src/main/resources/mybatis-config.xml Visa fil

@@ -1,20 +0,0 @@
1
-<?xml version="1.0" encoding="UTF-8" ?>
2
-<!DOCTYPE configuration
3
-PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
4
-"http://mybatis.org/dtd/mybatis-3-config.dtd">
5
-<configuration>
6
-    <!-- 全局参数 -->
7
-    <settings>
8
-        <!-- 使全局的映射器启用或禁用缓存 -->
9
-        <setting name="cacheEnabled"             value="true"   />
10
-        <!-- 允许JDBC 支持自动生成主键 -->
11
-        <setting name="useGeneratedKeys"         value="true"   />
12
-        <!-- 配置默认的执行器.SIMPLE就是普通执行器;REUSE执行器会重用预处理语句(prepared statements);BATCH执行器将重用语句并执行批量更新 -->
13
-        <setting name="defaultExecutorType"      value="SIMPLE" />
14
-		<!-- 指定 MyBatis 所用日志的具体实现 -->
15
-        <setting name="logImpl"                  value="SLF4J"  />
16
-        <!-- 使用驼峰命名法转换字段 -->
17
-		<!-- <setting name="mapUnderscoreToCamelCase" value="true"/> -->
18
-	</settings>
19
-	
20
-</configuration>

+ 105
- 0
iot-platform/src/test/java/com/iot/platform/mqtt/AbstractDynamicMqttConsumerTest.java Visa fil

@@ -0,0 +1,105 @@
1
+package com.iot.platform.mqtt;
2
+
3
+import com.iot.platform.config.IotProperties;
4
+import org.junit.jupiter.api.BeforeEach;
5
+import org.junit.jupiter.api.DisplayName;
6
+import org.junit.jupiter.api.Test;
7
+import org.junit.jupiter.api.extension.ExtendWith;
8
+import org.mockito.Mock;
9
+import org.mockito.junit.jupiter.MockitoExtension;
10
+
11
+import java.util.*;
12
+import java.util.concurrent.ExecutorService;
13
+import java.util.concurrent.Executors;
14
+import java.util.concurrent.ScheduledExecutorService;
15
+
16
+import static org.assertj.core.api.Assertions.assertThat;
17
+
18
+@ExtendWith(MockitoExtension.class)
19
+class AbstractDynamicMqttConsumerTest {
20
+
21
+    @Mock
22
+    private IotProperties iotProperties;
23
+
24
+    private TestDynamicConsumer consumer;
25
+
26
+    @BeforeEach
27
+    void setUp() {
28
+        ScheduledExecutorService coreExecutor = Executors.newSingleThreadScheduledExecutor();
29
+        ExecutorService writeExecutor = Executors.newSingleThreadExecutor();
30
+        consumer = new TestDynamicConsumer(iotProperties, coreExecutor, writeExecutor);
31
+    }
32
+
33
+    @Test
34
+    @DisplayName("deepCopyMap: null 输入应返回空 Map")
35
+    void deepCopyMap_nullInput_returnsEmptyMap() {
36
+        Map<String, Object> result = consumer.deepCopyMap(null);
37
+        assertThat(result).isNotNull().isEmpty();
38
+    }
39
+
40
+    @Test
41
+    @DisplayName("deepCopyMap: 应返回深拷贝,修改副本不影响原图")
42
+    void deepCopyMap_returnsDeepCopy() {
43
+        Map<String, Object> original = new HashMap<>();
44
+        original.put("key1", "value1");
45
+        original.put("key2", 42);
46
+
47
+        Map<String, Object> copy = consumer.deepCopyMap(original);
48
+
49
+        assertThat(copy).containsExactlyInAnyOrderEntriesOf(original);
50
+        copy.put("key1", "modified");
51
+        assertThat(original.get("key1")).isEqualTo("value1");
52
+    }
53
+
54
+    @Test
55
+    @DisplayName("deepCopyMap: 应处理嵌套 Map")
56
+    void deepCopyMap_nestedMap_returnsDeepCopy() {
57
+        Map<String, Object> inner = new HashMap<>();
58
+        inner.put("innerKey", "innerValue");
59
+
60
+        Map<String, Object> original = new HashMap<>();
61
+        original.put("nested", inner);
62
+
63
+        Map<String, Object> copy = consumer.deepCopyMap(original);
64
+
65
+        assertThat(copy).containsKey("nested");
66
+        @SuppressWarnings("unchecked")
67
+        Map<String, Object> copiedInner = (Map<String, Object>) copy.get("nested");
68
+        copiedInner.put("innerKey", "modified");
69
+        assertThat(inner.get("innerKey")).isEqualTo("innerValue");
70
+    }
71
+
72
+    @Test
73
+    @DisplayName("deepCopyMap: 应处理包含 List 的 Map")
74
+    void deepCopyMap_withList_returnsDeepCopy() {
75
+        Map<String, Object> original = new HashMap<>();
76
+        original.put("list", Arrays.asList("a", "b", "c"));
77
+
78
+        Map<String, Object> copy = consumer.deepCopyMap(original);
79
+
80
+        assertThat(copy).containsKey("list");
81
+        @SuppressWarnings("unchecked")
82
+        List<String> copiedList = (List<String>) copy.get("list");
83
+        assertThat(copiedList).containsExactly("a", "b", "c");
84
+    }
85
+
86
+    /**
87
+     * 用于测试的匿名实现
88
+     */
89
+    static class TestDynamicConsumer extends AbstractDynamicMqttConsumer {
90
+        TestDynamicConsumer(IotProperties iotProperties,
91
+                            ScheduledExecutorService coreExecutor,
92
+                            ExecutorService writeExecutor) {
93
+            super(iotProperties, coreExecutor, writeExecutor);
94
+        }
95
+
96
+        @Override
97
+        protected List<String> fetchTopics() {
98
+            return Collections.emptyList();
99
+        }
100
+
101
+        @Override
102
+        protected void processMessage(String content, String topic) {
103
+        }
104
+    }
105
+}

+ 86
- 0
iot-platform/src/test/java/com/iot/platform/mqtt/MqttChargeStationConsumerTest.java Visa fil

@@ -0,0 +1,86 @@
1
+package com.iot.platform.mqtt;
2
+
3
+import com.iot.platform.config.IotProperties;
4
+import com.iot.platform.service.TDengineService;
5
+import org.junit.jupiter.api.BeforeEach;
6
+import org.junit.jupiter.api.DisplayName;
7
+import org.junit.jupiter.api.Test;
8
+import org.junit.jupiter.api.extension.ExtendWith;
9
+import org.mockito.Mock;
10
+import org.mockito.junit.jupiter.MockitoExtension;
11
+
12
+import java.util.Arrays;
13
+import java.util.Collections;
14
+import java.util.List;
15
+import java.util.concurrent.ExecutorService;
16
+import java.util.concurrent.Executors;
17
+import java.util.concurrent.ScheduledExecutorService;
18
+
19
+import static org.assertj.core.api.Assertions.assertThat;
20
+import static org.mockito.Mockito.*;
21
+
22
+@ExtendWith(MockitoExtension.class)
23
+class MqttChargeStationConsumerTest {
24
+
25
+    @Mock
26
+    private IotProperties iotProperties;
27
+
28
+    @Mock
29
+    private IotProperties.Mqtt mqtt;
30
+
31
+    @Mock
32
+    private TDengineService tdengineService;
33
+
34
+    private MqttChargeStationConsumer consumer;
35
+
36
+    @BeforeEach
37
+    void setUp() {
38
+        ScheduledExecutorService coreExecutor = Executors.newSingleThreadScheduledExecutor();
39
+        ExecutorService writeExecutor = Executors.newSingleThreadExecutor();
40
+        consumer = new MqttChargeStationConsumer(iotProperties, coreExecutor, writeExecutor, tdengineService);
41
+    }
42
+
43
+    @Test
44
+    @DisplayName("fetchTopics: 应返回配置的 chargeStationTopic")
45
+    void fetchTopics_withConfiguredTopic_returnsTopicList() {
46
+        when(iotProperties.getMqtt()).thenReturn(mqtt);
47
+        when(mqtt.getChargeStationTopic()).thenReturn("station/ChargeStation/device/+/post/json");
48
+
49
+        List<String> result = consumer.fetchTopics();
50
+
51
+        assertThat(result).containsExactly("station/ChargeStation/device/+/post/json");
52
+    }
53
+
54
+    @Test
55
+    @DisplayName("fetchTopics: topic 为 null 时应返回空列表")
56
+    void fetchTopics_nullTopic_returnsEmptyList() {
57
+        when(iotProperties.getMqtt()).thenReturn(mqtt);
58
+        when(mqtt.getChargeStationTopic()).thenReturn(null);
59
+
60
+        List<String> result = consumer.fetchTopics();
61
+
62
+        assertThat(result).isEmpty();
63
+    }
64
+
65
+    @Test
66
+    @DisplayName("fetchTopics: topic 为空字符串时应返回空列表")
67
+    void fetchTopics_emptyTopic_returnsEmptyList() {
68
+        when(iotProperties.getMqtt()).thenReturn(mqtt);
69
+        when(mqtt.getChargeStationTopic()).thenReturn("   ");
70
+
71
+        List<String> result = consumer.fetchTopics();
72
+
73
+        assertThat(result).isEmpty();
74
+    }
75
+
76
+    @Test
77
+    @DisplayName("fetchTopics: topic 应被 trim 处理")
78
+    void fetchTopics_trimmedTopic_returnsTrimmedList() {
79
+        when(iotProperties.getMqtt()).thenReturn(mqtt);
80
+        when(mqtt.getChargeStationTopic()).thenReturn("  station/test  ");
81
+
82
+        List<String> result = consumer.fetchTopics();
83
+
84
+        assertThat(result).containsExactly("station/test");
85
+    }
86
+}

+ 138
- 0
iot-platform/src/test/java/com/iot/platform/mqtt/MqttDynamicConsumerTest.java Visa fil

@@ -0,0 +1,138 @@
1
+package com.iot.platform.mqtt;
2
+
3
+import com.iot.platform.config.IotProperties;
4
+import com.iot.platform.domain.SysController;
5
+import com.iot.platform.service.SysControllerService;
6
+import com.iot.platform.service.TDengineService;
7
+import org.junit.jupiter.api.BeforeEach;
8
+import org.junit.jupiter.api.DisplayName;
9
+import org.junit.jupiter.api.Test;
10
+import org.junit.jupiter.api.extension.ExtendWith;
11
+import org.mockito.Mock;
12
+import org.mockito.junit.jupiter.MockitoExtension;
13
+import org.springframework.data.redis.core.HashOperations;
14
+import org.springframework.data.redis.core.SetOperations;
15
+import org.springframework.data.redis.core.StringRedisTemplate;
16
+
17
+import java.util.*;
18
+import java.util.concurrent.ExecutorService;
19
+import java.util.concurrent.Executors;
20
+import java.util.concurrent.ScheduledExecutorService;
21
+import java.util.concurrent.TimeUnit;
22
+
23
+import static org.assertj.core.api.Assertions.assertThat;
24
+import static org.mockito.ArgumentMatchers.*;
25
+import static org.mockito.Mockito.*;
26
+
27
+@ExtendWith(MockitoExtension.class)
28
+class MqttDynamicConsumerTest {
29
+
30
+    @Mock
31
+    private IotProperties iotProperties;
32
+
33
+    @Mock
34
+    private SysControllerService sysControllerService;
35
+
36
+    @Mock
37
+    private TDengineService tdengineService;
38
+
39
+    @Mock
40
+    private StringRedisTemplate stringRedisTemplate;
41
+
42
+    @Mock
43
+    private HashOperations<String, Object, Object> hashOperations;
44
+
45
+    @Mock
46
+    private SetOperations<String, String> setOperations;
47
+
48
+    private MqttDynamicConsumer consumer;
49
+
50
+    @BeforeEach
51
+    void setUp() {
52
+        ScheduledExecutorService coreExecutor = Executors.newSingleThreadScheduledExecutor();
53
+        ExecutorService writeExecutor = Executors.newSingleThreadExecutor();
54
+        consumer = new MqttDynamicConsumer(iotProperties, coreExecutor, writeExecutor,
55
+                sysControllerService, tdengineService, stringRedisTemplate);
56
+    }
57
+
58
+    @Test
59
+    @DisplayName("fetchTopics 应返回 sysControllerService.selectall() 的结果")
60
+    void fetchTopics_delegatesToService() {
61
+        List<String> expected = Arrays.asList("topic/1", "topic/2");
62
+        when(sysControllerService.selectall()).thenReturn(expected);
63
+
64
+        List<String> result = consumer.fetchTopics();
65
+
66
+        assertThat(result).isEqualTo(expected);
67
+        verify(sysControllerService).selectall();
68
+    }
69
+
70
+    @Test
71
+    @DisplayName("fetchTopics 应处理 null 返回值")
72
+    void fetchTopics_nullReturn_returnsNull() {
73
+        when(sysControllerService.selectall()).thenReturn(null);
74
+
75
+        List<String> result = consumer.fetchTopics();
76
+
77
+        assertThat(result).isNull();
78
+    }
79
+
80
+    @Test
81
+    @DisplayName("insertredis: 应写入 Redis hash 和 active devices set")
82
+    void insertredis_validData_writesToRedis() throws Exception {
83
+        when(stringRedisTemplate.opsForHash()).thenReturn(hashOperations);
84
+        when(stringRedisTemplate.opsForSet()).thenReturn(setOperations);
85
+
86
+        SysController controller = new SysController();
87
+        controller.setName("temperature");
88
+        when(sysControllerService.selectcontrollerpath("ctrl1/temperature")).thenReturn(controller);
89
+
90
+        Map<String, Object> weather = new HashMap<>();
91
+        weather.put("timestamp", "1234567890");
92
+        weather.put("device_id", "dev001");
93
+        Map<String, Object> metricData = new HashMap<>();
94
+        metricData.put("val", "25.5");
95
+        weather.put("temperature", metricData);
96
+
97
+        consumer.insertredis(weather, "ctrl1/temperature");
98
+
99
+        verify(hashOperations).putAll(eq("DSB:ctrl1:temperature"), anyMap());
100
+        verify(stringRedisTemplate).expire("DSB:ctrl1:temperature", 2, TimeUnit.HOURS);
101
+        verify(setOperations).add("DSB:active:devices", "DSB:ctrl1:temperature");
102
+        verify(stringRedisTemplate).expire("DSB:active:devices", 2, TimeUnit.HOURS);
103
+    }
104
+
105
+    @Test
106
+    @DisplayName("insertredis: topic 格式无效时应直接返回")
107
+    void insertredis_invalidTopic_returnsSilently() throws Exception {
108
+        consumer.insertredis(new HashMap<>(), "invalidtopic");
109
+
110
+        verifyNoInteractions(stringRedisTemplate);
111
+    }
112
+
113
+    @Test
114
+    @DisplayName("insertredis: controller 未找到时应直接返回")
115
+    void insertredis_controllerNotFound_returnsSilently() throws Exception {
116
+        when(sysControllerService.selectcontrollerpath("ctrl1/missing")).thenReturn(null);
117
+
118
+        Map<String, Object> weather = new HashMap<>();
119
+        consumer.insertredis(weather, "ctrl1/missing");
120
+
121
+        verifyNoInteractions(stringRedisTemplate);
122
+    }
123
+
124
+    @Test
125
+    @DisplayName("insertredis: metricData 为 null 时应直接返回")
126
+    void insertredis_nullMetricData_returnsSilently() throws Exception {
127
+        SysController controller = new SysController();
128
+        controller.setName("temperature");
129
+        when(sysControllerService.selectcontrollerpath("ctrl1/temperature")).thenReturn(controller);
130
+
131
+        Map<String, Object> weather = new HashMap<>();
132
+        weather.put("temperature", null);
133
+
134
+        consumer.insertredis(weather, "ctrl1/temperature");
135
+
136
+        verifyNoInteractions(stringRedisTemplate);
137
+    }
138
+}

+ 225
- 0
iot-platform/src/test/java/com/iot/platform/mqtt/MqttFaultConsumerTest.java Visa fil

@@ -0,0 +1,225 @@
1
+package com.iot.platform.mqtt;
2
+
3
+import com.fasterxml.jackson.databind.ObjectMapper;
4
+import com.iot.platform.common.utils.NumericIdGenerator;
5
+import com.iot.platform.config.IotProperties;
6
+import com.iot.platform.domain.SysDevice;
7
+import com.iot.platform.domain.SysFault;
8
+import com.iot.platform.service.*;
9
+import org.junit.jupiter.api.BeforeEach;
10
+import org.junit.jupiter.api.DisplayName;
11
+import org.junit.jupiter.api.Test;
12
+import org.junit.jupiter.api.extension.ExtendWith;
13
+import org.mockito.Mock;
14
+import org.mockito.junit.jupiter.MockitoExtension;
15
+import org.mockito.junit.jupiter.MockitoSettings;
16
+import org.mockito.quality.Strictness;
17
+import org.springframework.web.client.RestTemplate;
18
+
19
+import java.sql.SQLException;
20
+import java.time.LocalDate;
21
+import java.util.Collections;
22
+import java.util.HashMap;
23
+import java.util.Map;
24
+import java.util.concurrent.ExecutorService;
25
+
26
+import static org.assertj.core.api.Assertions.assertThat;
27
+import static org.mockito.ArgumentMatchers.*;
28
+import static org.mockito.Mockito.*;
29
+
30
+@ExtendWith(MockitoExtension.class)
31
+@MockitoSettings(strictness = Strictness.LENIENT)
32
+class MqttFaultConsumerTest {
33
+
34
+    @Mock
35
+    private SysControllerService sysControllerService;
36
+
37
+    @Mock
38
+    private SysFaultService sysFaultService;
39
+
40
+    @Mock
41
+    private SysrealtimeService sysrealtimeService;
42
+
43
+    @Mock
44
+    private SysWorkorderService sysWorkorderService;
45
+
46
+    @Mock
47
+    private SysAlarmService sysAlarmService;
48
+
49
+    @Mock
50
+    private NumericIdGenerator numericIdGenerator;
51
+
52
+    @Mock
53
+    private TDegnineAlarm tDegnineAlarm;
54
+
55
+    @Mock
56
+    private RestTemplate restTemplate;
57
+
58
+    @Mock
59
+    private IotProperties iotProperties;
60
+
61
+    @Mock(name = "mqttFaultExecutor")
62
+    private ExecutorService mqttFaultExecutor;
63
+
64
+    @Mock(name = "abstractConsumerExecutor")
65
+    private ExecutorService abstractConsumerExecutor;
66
+
67
+    private MqttFaultConsumer mqttFaultConsumer;
68
+
69
+    private final ObjectMapper objectMapper = new ObjectMapper();
70
+
71
+    @BeforeEach
72
+    void setUp() {
73
+        mqttFaultConsumer = new MqttFaultConsumer(
74
+                mqttFaultExecutor, abstractConsumerExecutor, iotProperties,
75
+                sysControllerService, sysFaultService, sysrealtimeService,
76
+                sysWorkorderService, sysAlarmService, numericIdGenerator,
77
+                tDegnineAlarm, restTemplate);
78
+
79
+        IotProperties.Mqtt mqttConfig = new IotProperties.Mqtt();
80
+        mqttConfig.setAlarmWebhookUrl("https://esos-iot.com:9443/syscar/gaojing");
81
+        when(iotProperties.getMqtt()).thenReturn(mqttConfig);
82
+
83
+        // executor mock runs submitted tasks synchronously
84
+        lenient().when(mqttFaultExecutor.submit(any(Runnable.class))).thenAnswer(inv -> {
85
+            Runnable r = inv.getArgument(0);
86
+            r.run();
87
+            return null;
88
+        });
89
+    }
90
+
91
+    @Test
92
+    @DisplayName("getSubscribeTopic returns +/fault_prot")
93
+    void getSubscribeTopic_returnsCorrectValue() {
94
+        assertThat(mqttFaultConsumer.getSubscribeTopic()).isEqualTo("+/fault_prot");
95
+    }
96
+
97
+    @Test
98
+    @DisplayName("generateClientId contains mqttx prefix and differs by OS")
99
+    void generateClientId_containsPrefixAndDiffersByOs() {
100
+        String clientId = mqttFaultConsumer.generateClientId();
101
+        assertThat(clientId).startsWith("mqttx_e216fbf162");
102
+
103
+        String osName = System.getProperty("os.name").toLowerCase();
104
+        String expectedBase = osName.contains("windows") ? "mqttx_e216fbf1620" : "mqttx_e216fbf1621";
105
+        assertThat(clientId).startsWith(expectedBase + "_");
106
+    }
107
+
108
+    @Test
109
+    @DisplayName("insertTDegine maps keys correctly via KEY_MAPPING")
110
+    void insertTDegine_mapsKeysCorrectly() throws SQLException {
111
+        Map<String, Object> weather = new HashMap<>();
112
+        weather.put("timestamp", "2024-01-01T00:00:00Z");
113
+        weather.put("type", "触发");
114
+        weather.put("desc", "overtemperature");
115
+        weather.put("other", "value");
116
+
117
+        String topic = "controller1/fault_prot";
118
+
119
+        mqttFaultConsumer.insertTDegine(weather, topic);
120
+
121
+        verify(tDegnineAlarm).shibaihou(argThat(map ->
122
+                "2024-01-01T00:00:00Z".equals(map.get("devicetimestamp"))
123
+                        && "触发".equals(map.get("devicetype"))
124
+                        && "overtemperature".equals(map.get("devicedesc"))
125
+                        && "value".equals(map.get("other"))
126
+                        && !map.containsKey("timestamp")
127
+                        && !map.containsKey("type")
128
+                        && !map.containsKey("desc")
129
+        ), anyString(), anyString(), anyString());
130
+    }
131
+
132
+    @Test
133
+    @DisplayName("insertTDegine splits topic correctly")
134
+    void insertTDegine_splitsTopicCorrectly() throws SQLException {
135
+        Map<String, Object> weather = new HashMap<>();
136
+        weather.put("timestamp", "2024-01-01T00:00:00Z");
137
+
138
+        String topic = "myController/fault_prot";
139
+        LocalDate now = LocalDate.now();
140
+        int year = now.getYear();
141
+        int month = now.getMonthValue();
142
+        String expectedTable = "myController_" + year + month;
143
+
144
+        mqttFaultConsumer.insertTDegine(weather, topic);
145
+
146
+        verify(tDegnineAlarm).shibaihou(anyMap(), eq("myController"), eq(expectedTable), eq("fault_prot"));
147
+    }
148
+
149
+    @Test
150
+    @DisplayName("handleMessage parses JSON and calls insertTDegine")
151
+    void handleMessage_parsesJsonAndCallsInsertTDegine() throws Exception {
152
+        String topic = "ctrl1/fault_prot";
153
+        String messageContent = "{\"device_id\":\"dev1\",\"controller_id\":\"ctrl1\",\"timestamp\":\"2024-01-01T00:00:00Z\",\"type\":\"触发\",\"code\":1,\"desc\":\"overtemperature\"}";
154
+
155
+        SysDevice jingdu = new SysDevice();
156
+        jingdu.setV("116.3974");
157
+        SysDevice weidu = new SysDevice();
158
+        weidu.setV("39.9093");
159
+
160
+        lenient().when(sysrealtimeService.selecttables()).thenReturn(Collections.singletonList("ctrl1" + LocalDate.now().getYear() + String.format("%02d", LocalDate.now().getMonthValue()) + "_fault"));
161
+        lenient().when(sysControllerService.selectjingweidu("ctrl1", "经度")).thenReturn(jingdu);
162
+        lenient().when(sysControllerService.selectjingweidu("ctrl1", "纬度")).thenReturn(weidu);
163
+        lenient().when(numericIdGenerator.nextId()).thenReturn("123456789");
164
+
165
+        mqttFaultConsumer.handleMessage(topic, messageContent);
166
+
167
+        verify(tDegnineAlarm).shibaihou(anyMap(), eq("ctrl1"), anyString(), eq("fault_prot"));
168
+    }
169
+
170
+    @Test
171
+    @DisplayName("triggermethod with 触发 type calls insertalarm and insertfault")
172
+    void triggermethod_triggerType_callsInsertAlarmAndInsertFault() {
173
+        SysFault weather = new SysFault();
174
+        weather.setDevice_id("dev1");
175
+        weather.setController_id("ctrl1");
176
+        weather.setTimestamp("2024-01-01T00:00:00Z");
177
+        weather.setType("触发");
178
+        weather.setDesc("overtemperature");
179
+
180
+        String topic = "ctrl1/fault_prot";
181
+
182
+        SysDevice jingdu = new SysDevice();
183
+        jingdu.setV("116.3974");
184
+        SysDevice weidu = new SysDevice();
185
+        weidu.setV("39.9093");
186
+
187
+        when(sysrealtimeService.selecttables()).thenReturn(Collections.singletonList("ctrl1" + LocalDate.now().getYear() + String.format("%02d", LocalDate.now().getMonthValue()) + "_fault"));
188
+        when(sysControllerService.selectjingweidu("ctrl1", "经度")).thenReturn(jingdu);
189
+        when(sysControllerService.selectjingweidu("ctrl1", "纬度")).thenReturn(weidu);
190
+        when(numericIdGenerator.nextId()).thenReturn("123456789");
191
+
192
+        mqttFaultConsumer.triggermethod(topic, weather);
193
+
194
+        verify(sysAlarmService).insertalarm(anyString(), eq("GJ123456789"), eq("overtemperature"), eq("0"), anyString(), eq("0"), eq("ctrl1"), eq("dev1"), eq("116.3974"), eq("39.9093"));
195
+        verify(sysFaultService).insertfault(eq("GJ123456789"), eq("overtemperature"), eq("0"), anyString(), eq("0"), eq("ctrl1"), eq("dev1"), eq("116.3974"), eq("39.9093"), eq(""));
196
+    }
197
+
198
+    @Test
199
+    @DisplayName("triggermethod with 恢复 type calls insertalarm and updatefault")
200
+    void triggermethod_recoverType_callsInsertAlarmAndUpdateFault() {
201
+        SysFault weather = new SysFault();
202
+        weather.setDevice_id("dev1");
203
+        weather.setController_id("ctrl1");
204
+        weather.setTimestamp("2024-01-01T00:00:00Z");
205
+        weather.setType("恢复");
206
+        weather.setDesc("overtemperature recovered");
207
+
208
+        String topic = "ctrl1/fault_prot";
209
+
210
+        SysDevice jingdu = new SysDevice();
211
+        jingdu.setV("116.3974");
212
+        SysDevice weidu = new SysDevice();
213
+        weidu.setV("39.9093");
214
+
215
+        when(sysrealtimeService.selecttables()).thenReturn(Collections.singletonList("ctrl1" + LocalDate.now().getYear() + String.format("%02d", LocalDate.now().getMonthValue()) + "_fault"));
216
+        when(sysControllerService.selectjingweidu("ctrl1", "经度")).thenReturn(jingdu);
217
+        when(sysControllerService.selectjingweidu("ctrl1", "纬度")).thenReturn(weidu);
218
+        when(numericIdGenerator.nextId()).thenReturn("987654321");
219
+
220
+        mqttFaultConsumer.triggermethod(topic, weather);
221
+
222
+        verify(sysAlarmService).insertalarm(anyString(), eq("GJ987654321"), eq("overtemperature recovered"), eq("1"), anyString(), eq("0"), eq("ctrl1"), eq("dev1"), eq("116.3974"), eq("39.9093"));
223
+        verify(sysFaultService).updatefault(eq("1"), eq("0"), eq("116.3974"), eq("39.9093"), eq("overtemperature recovered"), eq("ctrl1"), eq("dev1"), anyString());
224
+    }
225
+}

+ 19
- 2
iot-platform/src/test/java/com/iot/platform/mqtt/MqttGenericConsumerTest.java Visa fil

@@ -1,23 +1,40 @@
1 1
 package com.iot.platform.mqtt;
2 2
 
3
+import com.iot.platform.config.IotProperties;
4
+import com.iot.platform.service.SysControllerService;
3 5
 import org.junit.jupiter.api.DisplayName;
4 6
 import org.junit.jupiter.api.Test;
7
+import org.springframework.data.redis.core.StringRedisTemplate;
8
+
9
+import java.util.concurrent.ExecutorService;
10
+import java.util.concurrent.Executors;
5 11
 
6 12
 import static org.assertj.core.api.Assertions.assertThat;
13
+import static org.mockito.Mockito.mock;
7 14
 
8 15
 class MqttGenericConsumerTest {
9 16
 
17
+    private final ExecutorService executorService = Executors.newSingleThreadExecutor();
18
+    private final IotProperties iotProperties = mock(IotProperties.class);
19
+    private final StringRedisTemplate stringRedisTemplate = mock(StringRedisTemplate.class);
20
+    private final SysControllerService sysControllerService = mock(SysControllerService.class);
21
+    private final MqttDynamicConsumer mqttDynamicConsumer = mock(MqttDynamicConsumer.class);
22
+
23
+    private MqttGenericConsumer createConsumer() {
24
+        return new MqttGenericConsumer(executorService, iotProperties, stringRedisTemplate, sysControllerService, mqttDynamicConsumer);
25
+    }
26
+
10 27
     @Test
11 28
     @DisplayName("getSubscribeTopic 应返回 +/generics")
12 29
     void getSubscribeTopic_returnsCorrectValue() {
13
-        MqttGenericConsumer consumer = new MqttGenericConsumer();
30
+        MqttGenericConsumer consumer = createConsumer();
14 31
         assertThat(consumer.getSubscribeTopic()).isEqualTo("+/generics");
15 32
     }
16 33
 
17 34
     @Test
18 35
     @DisplayName("generateClientId 应包含 mqttx 前缀")
19 36
     void generateClientId_containsPrefix() {
20
-        MqttGenericConsumer consumer = new MqttGenericConsumer();
37
+        MqttGenericConsumer consumer = createConsumer();
21 38
         String clientId = consumer.generateClientId();
22 39
         assertThat(clientId).startsWith("mqttx_e216fbf16");
23 40
     }

+ 204
- 0
iot-platform/src/test/java/com/iot/platform/mqtt/MqttStatusConsumerTest.java Visa fil

@@ -0,0 +1,204 @@
1
+package com.iot.platform.mqtt;
2
+
3
+import com.iot.platform.config.IotProperties;
4
+import com.iot.platform.service.SysControllerService;
5
+import com.iot.platform.service.SysStatusService;
6
+import org.junit.jupiter.api.BeforeEach;
7
+import org.junit.jupiter.api.DisplayName;
8
+import org.junit.jupiter.api.Test;
9
+import org.junit.jupiter.api.extension.ExtendWith;
10
+import org.mockito.Mock;
11
+import org.mockito.junit.jupiter.MockitoExtension;
12
+import org.mockito.junit.jupiter.MockitoSettings;
13
+import org.mockito.quality.Strictness;
14
+import org.springframework.data.redis.core.HashOperations;
15
+import org.springframework.data.redis.core.StringRedisTemplate;
16
+
17
+import java.util.HashMap;
18
+import java.util.Map;
19
+import java.util.concurrent.ExecutorService;
20
+import java.util.concurrent.Executors;
21
+
22
+import static org.assertj.core.api.Assertions.assertThat;
23
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
24
+import static org.mockito.ArgumentMatchers.any;
25
+import static org.mockito.ArgumentMatchers.anyString;
26
+import static org.mockito.ArgumentMatchers.eq;
27
+import static org.mockito.Mockito.lenient;
28
+import static org.mockito.Mockito.never;
29
+import static org.mockito.Mockito.verify;
30
+import static org.mockito.Mockito.when;
31
+
32
+@ExtendWith(MockitoExtension.class)
33
+@MockitoSettings(strictness = Strictness.LENIENT)
34
+class MqttStatusConsumerTest {
35
+
36
+    @Mock
37
+    private StringRedisTemplate stringRedisTemplate;
38
+
39
+    @Mock
40
+    private SysControllerService sysControllerService;
41
+
42
+    @Mock
43
+    private SysStatusService sysStatusService;
44
+
45
+    @Mock
46
+    private HashOperations<String, Object, Object> hashOperations;
47
+
48
+    @Mock
49
+    private IotProperties iotProperties;
50
+
51
+    private MqttStatusConsumer mqttStatusConsumer;
52
+
53
+    private final ExecutorService executorService = Executors.newSingleThreadExecutor();
54
+
55
+    @BeforeEach
56
+    void setUp() {
57
+        mqttStatusConsumer = new MqttStatusConsumer(executorService, iotProperties,
58
+                stringRedisTemplate, sysControllerService, sysStatusService);
59
+        lenient().when(stringRedisTemplate.opsForHash()).thenReturn(hashOperations);
60
+    }
61
+
62
+    @Test
63
+    @DisplayName("getSubscribeTopic returns +/status")
64
+    void getSubscribeTopic_returnsStatusTopic() {
65
+        assertThat(mqttStatusConsumer.getSubscribeTopic()).isEqualTo("+/status");
66
+    }
67
+
68
+    @Test
69
+    @DisplayName("generateClientId contains mqttx prefix and differs by OS")
70
+    void generateClientId_containsPrefixAndDiffersByOs() {
71
+        String clientId = mqttStatusConsumer.generateClientId();
72
+
73
+        assertThat(clientId).startsWith("mqttx_e216fbf161");
74
+
75
+        String osName = System.getProperty("os.name").toLowerCase();
76
+        String expectedBase = osName.contains("windows") ? "mqttx_e216fbf1613" : "mqttx_e216fbf1614";
77
+        assertThat(clientId).startsWith(expectedBase + "_");
78
+    }
79
+
80
+    @Test
81
+    @DisplayName("triggermethod with count <= 0 calls insertsysstatus")
82
+    void triggermethod_countZeroOrLess_callsInsertsysstatus() throws Exception {
83
+        Map<String, Object> weather = new HashMap<>();
84
+        weather.put("controller_id", "CTRL_001");
85
+        weather.put("fleet_id", "FLEET_001");
86
+        weather.put("status", "online");
87
+
88
+        when(sysStatusService.selectstatuscount("CTRL_001")).thenReturn(0);
89
+
90
+        mqttStatusConsumer.triggermethod(weather);
91
+
92
+        verify(sysStatusService).selectstatuscount("CTRL_001");
93
+        verify(sysStatusService).insertsysstatus(
94
+            eq("CTRL_001"), eq("FLEET_001"), eq("online"), anyString()
95
+        );
96
+        verify(sysStatusService, never()).updatestatus(anyString(), anyString(), anyString(), anyString());
97
+
98
+        verify(hashOperations).put("CTRL_001status:", "fleet_id", "FLEET_001");
99
+        verify(hashOperations).put("CTRL_001status:", "status", "online");
100
+    }
101
+
102
+    @Test
103
+    @DisplayName("triggermethod with count > 0 calls updatestatus")
104
+    void triggermethod_countGreaterThanZero_callsUpdatestatus() throws Exception {
105
+        Map<String, Object> weather = new HashMap<>();
106
+        weather.put("controller_id", "CTRL_002");
107
+        weather.put("fleet_id", "FLEET_002");
108
+        weather.put("status", "offline");
109
+
110
+        when(sysStatusService.selectstatuscount("CTRL_002")).thenReturn(3);
111
+
112
+        mqttStatusConsumer.triggermethod(weather);
113
+
114
+        verify(sysStatusService).selectstatuscount("CTRL_002");
115
+        verify(sysStatusService).updatestatus(
116
+            eq("CTRL_002"), eq("FLEET_002"), eq("offline"), anyString()
117
+        );
118
+        verify(sysStatusService, never()).insertsysstatus(anyString(), anyString(), anyString(), anyString());
119
+
120
+        verify(hashOperations).put("CTRL_002status:", "fleet_id", "FLEET_002");
121
+        verify(hashOperations).put("CTRL_002status:", "status", "offline");
122
+    }
123
+
124
+    @Test
125
+    @DisplayName("handleMessage parses JSON and calls triggermethod")
126
+    void handleMessage_validJson_callsTriggermethod() throws Exception {
127
+        String json = "{\"controller_id\":\"CTRL_003\",\"fleet_id\":\"FLEET_003\",\"status\":\"running\"}";
128
+
129
+        when(sysStatusService.selectstatuscount("CTRL_003")).thenReturn(1);
130
+
131
+        mqttStatusConsumer.handleMessage("+/status", json);
132
+
133
+        verify(sysStatusService).selectstatuscount("CTRL_003");
134
+        verify(sysStatusService).updatestatus(
135
+            eq("CTRL_003"), eq("FLEET_003"), eq("running"), anyString()
136
+        );
137
+
138
+        verify(hashOperations).put("CTRL_003status:", "fleet_id", "FLEET_003");
139
+        verify(hashOperations).put("CTRL_003status:", "status", "running");
140
+    }
141
+
142
+    @Test
143
+    @DisplayName("handleMessage with invalid JSON throws exception")
144
+    void handleMessage_invalidJson_throwsException() {
145
+        String invalidJson = "not valid json";
146
+
147
+        assertThatThrownBy(() -> mqttStatusConsumer.handleMessage("+/status", invalidJson))
148
+            .isInstanceOf(Exception.class);
149
+    }
150
+
151
+    @Test
152
+    @DisplayName("triggermethod with null controller_id returns silently")
153
+    void triggermethod_nullControllerId_returnsSilently() throws Exception {
154
+        Map<String, Object> weather = new HashMap<>();
155
+        weather.put("controller_id", null);
156
+        weather.put("fleet_id", "FLEET_001");
157
+        weather.put("status", "online");
158
+
159
+        mqttStatusConsumer.triggermethod(weather);
160
+        verify(sysStatusService, never()).selectstatuscount(anyString());
161
+    }
162
+
163
+    @Test
164
+    @DisplayName("triggermethod with null fleet_id returns silently")
165
+    void triggermethod_nullFleetId_returnsSilently() throws Exception {
166
+        Map<String, Object> weather = new HashMap<>();
167
+        weather.put("controller_id", "CTRL_001");
168
+        weather.put("fleet_id", null);
169
+        weather.put("status", "online");
170
+
171
+        mqttStatusConsumer.triggermethod(weather);
172
+        verify(sysStatusService, never()).selectstatuscount(anyString());
173
+    }
174
+
175
+    @Test
176
+    @DisplayName("triggermethod with null status returns silently")
177
+    void triggermethod_nullStatus_returnsSilently() throws Exception {
178
+        Map<String, Object> weather = new HashMap<>();
179
+        weather.put("controller_id", "CTRL_001");
180
+        weather.put("fleet_id", "FLEET_001");
181
+        weather.put("status", null);
182
+
183
+        mqttStatusConsumer.triggermethod(weather);
184
+        verify(sysStatusService, never()).selectstatuscount(anyString());
185
+    }
186
+
187
+    @Test
188
+    @DisplayName("triggermethod with negative count calls insertsysstatus")
189
+    void triggermethod_negativeCount_callsInsertsysstatus() throws Exception {
190
+        Map<String, Object> weather = new HashMap<>();
191
+        weather.put("controller_id", "CTRL_004");
192
+        weather.put("fleet_id", "FLEET_004");
193
+        weather.put("status", "error");
194
+
195
+        when(sysStatusService.selectstatuscount("CTRL_004")).thenReturn(-1);
196
+
197
+        mqttStatusConsumer.triggermethod(weather);
198
+
199
+        verify(sysStatusService).insertsysstatus(
200
+            eq("CTRL_004"), eq("FLEET_004"), eq("error"), anyString()
201
+        );
202
+        verify(sysStatusService, never()).updatestatus(anyString(), anyString(), anyString(), anyString());
203
+    }
204
+}

+ 200
- 0
iot-platform/src/test/java/com/iot/platform/service/TDengineServiceTest.java Visa fil

@@ -0,0 +1,200 @@
1
+package com.iot.platform.service;
2
+
3
+import com.iot.platform.config.IotProperties;
4
+import org.junit.jupiter.api.BeforeEach;
5
+import org.junit.jupiter.api.DisplayName;
6
+import org.junit.jupiter.api.Test;
7
+import org.junit.jupiter.api.extension.ExtendWith;
8
+import org.mockito.InjectMocks;
9
+import org.mockito.Mock;
10
+import org.mockito.junit.jupiter.MockitoExtension;
11
+import org.mockito.junit.jupiter.MockitoSettings;
12
+import org.mockito.quality.Strictness;
13
+
14
+import java.lang.reflect.Method;
15
+import java.sql.Connection;
16
+import java.sql.SQLException;
17
+import java.sql.Statement;
18
+import java.util.*;
19
+import java.util.concurrent.ExecutorService;
20
+
21
+import static org.assertj.core.api.Assertions.assertThat;
22
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
23
+import static org.mockito.Mockito.*;
24
+
25
+@ExtendWith(MockitoExtension.class)
26
+@MockitoSettings(strictness = Strictness.LENIENT)
27
+class TDengineServiceTest {
28
+
29
+    @Mock
30
+    private IotProperties iotProperties;
31
+
32
+    @Mock
33
+    private ExecutorService executorService;
34
+
35
+    @Mock
36
+    private IotProperties.TDengine tdengineConfig;
37
+
38
+    @InjectMocks
39
+    private TDengineService tdengineService;
40
+
41
+    @BeforeEach
42
+    void setUp() {
43
+        when(iotProperties.getTdengine()).thenReturn(tdengineConfig);
44
+        when(tdengineConfig.getUrl()).thenReturn("jdbc:TAOS://localhost:6030/test");
45
+        when(tdengineConfig.getUsername()).thenReturn("root");
46
+        when(tdengineConfig.getPassword()).thenReturn("taosdata");
47
+    }
48
+
49
+    @Test
50
+    @DisplayName("escapeValue: 单引号应被转义")
51
+    void escapeValue_singleQuote_doublesIt() throws Exception {
52
+        Method method = TDengineService.class.getDeclaredMethod("escapeValue", String.class);
53
+        method.setAccessible(true);
54
+
55
+        assertThat(method.invoke(tdengineService, "it's")).isEqualTo("it''s");
56
+        assertThat(method.invoke(tdengineService, (String) null)).isEqualTo("");
57
+        assertThat(method.invoke(tdengineService, "normal")).isEqualTo("normal");
58
+    }
59
+
60
+    @Test
61
+    @DisplayName("wrapName: 应包裹反引号并去除原有反引号")
62
+    void wrapName_wrapsInBackticks() throws Exception {
63
+        Method method = TDengineService.class.getDeclaredMethod("wrapName", String.class);
64
+        method.setAccessible(true);
65
+
66
+        assertThat(method.invoke(tdengineService, "mytable")).isEqualTo("`mytable`");
67
+        assertThat(method.invoke(tdengineService, "`injected`")).isEqualTo("`injected`");
68
+        assertThat(method.invoke(tdengineService, (String) null)).isEqualTo("`unknown`");
69
+        assertThat(method.invoke(tdengineService, "")).isEqualTo("`unknown`");
70
+    }
71
+
72
+    @Test
73
+    @DisplayName("isValidFieldName: 只允许字母数字下划线")
74
+    void isValidFieldName_validatesPattern() throws Exception {
75
+        Method method = TDengineService.class.getDeclaredMethod("isValidFieldName", String.class);
76
+        method.setAccessible(true);
77
+
78
+        assertThat(method.invoke(tdengineService, "valid_name")).isEqualTo(true);
79
+        assertThat(method.invoke(tdengineService, "Valid123")).isEqualTo(true);
80
+        assertThat(method.invoke(tdengineService, "invalid-name")).isEqualTo(false);
81
+        assertThat(method.invoke(tdengineService, "invalid.name")).isEqualTo(false);
82
+        assertThat(method.invoke(tdengineService, (String) null)).isEqualTo(false);
83
+    }
84
+
85
+    @Test
86
+    @DisplayName("buildDynamicJson: 应排除 topic/ts/surfacename")
87
+    void buildDynamicJson_excludesReservedKeys() throws Exception {
88
+        Method method = TDengineService.class.getDeclaredMethod("buildDynamicJson", Map.class);
89
+        method.setAccessible(true);
90
+
91
+        Map<String, Object> data = new LinkedHashMap<>();
92
+        data.put("topic", "test/topic");
93
+        data.put("ts", "1234567890");
94
+        data.put("surfacename", "s1");
95
+        data.put("temperature", "25.5");
96
+        data.put("humidity", "60");
97
+
98
+        String result = (String) method.invoke(tdengineService, data);
99
+        assertThat(result).contains("temperature", "humidity");
100
+        assertThat(result).doesNotContain("topic", "ts", "surfacename");
101
+    }
102
+
103
+    @Test
104
+    @DisplayName("buildDynamicJson: 空值和空字符串应被过滤")
105
+    void buildDynamicJson_filtersEmptyValues() throws Exception {
106
+        Method method = TDengineService.class.getDeclaredMethod("buildDynamicJson", Map.class);
107
+        method.setAccessible(true);
108
+
109
+        Map<String, Object> data = new LinkedHashMap<>();
110
+        data.put("valid", "value");
111
+        data.put("empty", "");
112
+        data.put("nullVal", null);
113
+
114
+        String result = (String) method.invoke(tdengineService, data);
115
+        assertThat(result).contains("valid");
116
+        assertThat(result).doesNotContain("empty", "nullVal");
117
+    }
118
+
119
+    @Test
120
+    @DisplayName("buildDynamicJson: null 输入应返回空 JSON")
121
+    void buildDynamicJson_nullInput_returnsEmptyJson() throws Exception {
122
+        Method method = TDengineService.class.getDeclaredMethod("buildDynamicJson", Map.class);
123
+        method.setAccessible(true);
124
+
125
+        assertThat(method.invoke(tdengineService, (Map<String, Object>) null)).isEqualTo("{}");
126
+    }
127
+
128
+    @Test
129
+    @DisplayName("compressToBase64: 应压缩并返回非空字符串")
130
+    void compressToBase64_compressesData() throws Exception {
131
+        Method method = TDengineService.class.getDeclaredMethod("compressToBase64", String.class);
132
+        method.setAccessible(true);
133
+
134
+        String input = "{\"temperature\":25.5,\"humidity\":60}";
135
+        String result = (String) method.invoke(tdengineService, input);
136
+
137
+        assertThat(result).isNotEmpty();
138
+        assertThat(result).isNotEqualTo(input); // should be compressed
139
+    }
140
+
141
+    @Test
142
+    @DisplayName("compressToBase64: null 或空字符串应返回空")
143
+    void compressToBase64_nullOrEmpty_returnsEmpty() throws Exception {
144
+        Method method = TDengineService.class.getDeclaredMethod("compressToBase64", String.class);
145
+        method.setAccessible(true);
146
+
147
+        assertThat(method.invoke(tdengineService, (String) null)).isEqualTo("");
148
+        assertThat(method.invoke(tdengineService, "")).isEqualTo("");
149
+    }
150
+
151
+    @Test
152
+    @DisplayName("insertBatch: 空列表应直接返回不抛异常")
153
+    void insertBatch_emptyList_returnsWithoutError() throws Exception {
154
+        tdengineService.insertBatch("db", "table", Collections.emptyList());
155
+        tdengineService.insertBatch("db", "table", null);
156
+    }
157
+
158
+    @Test
159
+    @DisplayName("addToBatch: 应包装为单元素列表调用 insertBatch")
160
+    void addToBatch_wrapsSingleItem() {
161
+        Map<String, Object> data = new HashMap<>();
162
+        data.put("key", "value");
163
+
164
+        // addToBatch 现在声明 throws SQLException,尝试连接 TDengine 会失败
165
+        // 本地环境缺少 TDengine 驱动时会抛出各种异常(UnsatisfiedLinkError / NoClassDefFoundError / IllegalStateException)
166
+        assertThatThrownBy(() -> tdengineService.addToBatch("db", "table", data))
167
+                .isInstanceOf(Throwable.class);
168
+    }
169
+
170
+    @Test
171
+    @DisplayName("clearStableColumnCache: 应清空缓存不抛异常")
172
+    void clearStableColumnCache_clearsWithoutError() {
173
+        tdengineService.clearStableColumnCache();
174
+        // Should not throw
175
+    }
176
+
177
+    @Test
178
+    @DisplayName("close: 应关闭数据源不抛异常")
179
+    void close_closesDataSource() {
180
+        tdengineService.close();
181
+        // Should not throw even if dataSource is null
182
+    }
183
+
184
+    @Test
185
+    @DisplayName("closeConnection: null 连接应安全处理")
186
+    void closeConnection_nullConnection_safe() {
187
+        tdengineService.closeConnection(null);
188
+        // Should not throw
189
+    }
190
+
191
+    @Test
192
+    @DisplayName("getConnection: 数据源初始化失败时应抛异常")
193
+    void getConnection_initFails_throwsException() {
194
+        when(tdengineConfig.getUrl()).thenReturn("jdbc:TAOS://invalid:6030/test");
195
+
196
+        // 本地环境缺少 TDengine 驱动时会抛出 NoClassDefFoundError / UnsatisfiedLinkError
197
+        assertThatThrownBy(() -> tdengineService.getConnection())
198
+                .isInstanceOf(Throwable.class);
199
+    }
200
+}

+ 221
- 0
iot-platform/src/test/java/com/iot/platform/task/VehicleSyncTaskTest.java Visa fil

@@ -0,0 +1,221 @@
1
+package com.iot.platform.task;
2
+
3
+import com.iot.platform.config.IotProperties;
4
+import com.iot.platform.domain.SysCar;
5
+import com.iot.platform.domain.SysDevice;
6
+import com.iot.platform.service.*;
7
+import org.junit.jupiter.api.BeforeEach;
8
+import org.junit.jupiter.api.DisplayName;
9
+import org.junit.jupiter.api.Test;
10
+import org.junit.jupiter.api.extension.ExtendWith;
11
+import org.mockito.InjectMocks;
12
+import org.mockito.Mock;
13
+import org.mockito.junit.jupiter.MockitoExtension;
14
+import org.mockito.junit.jupiter.MockitoSettings;
15
+import org.mockito.quality.Strictness;
16
+import org.springframework.dao.DataAccessException;
17
+import org.springframework.data.redis.RedisConnectionFailureException;
18
+import org.springframework.data.redis.core.StringRedisTemplate;
19
+import org.springframework.data.redis.core.ValueOperations;
20
+import org.springframework.web.client.RestClientException;
21
+import org.springframework.web.client.RestTemplate;
22
+
23
+import java.util.*;
24
+
25
+import static org.assertj.core.api.Assertions.assertThat;
26
+import static org.mockito.ArgumentMatchers.*;
27
+import static org.mockito.Mockito.*;
28
+
29
+@ExtendWith(MockitoExtension.class)
30
+@MockitoSettings(strictness = Strictness.LENIENT)
31
+class VehicleSyncTaskTest {
32
+
33
+    @Mock
34
+    private SysCarService sysCarService;
35
+    @Mock
36
+    private SysDeviceService sysDeviceService;
37
+    @Mock
38
+    private StringRedisTemplate stringRedisTemplate;
39
+    @Mock
40
+    private SysrealtimeService sysrealtimeService;
41
+    @Mock
42
+    private SysDeviceVoService sysDeviceVoService;
43
+    @Mock
44
+    private SysDeviceControlService sysDeviceControlService;
45
+    @Mock
46
+    private SysWorkorderService sysWorkorderService;
47
+    @Mock
48
+    private SysIndicatorsService sysIndicatorsService;
49
+    @Mock
50
+    private SysCompanyService sysCompanyService;
51
+    @Mock
52
+    private RestTemplate restTemplate;
53
+    @Mock
54
+    private IotProperties iotProperties;
55
+    @Mock
56
+    private IotProperties.Mqtt mqttConfig;
57
+
58
+    @InjectMocks
59
+    private VehicleSyncTask task;
60
+
61
+    @Mock
62
+    private ValueOperations<String, String> valueOps;
63
+
64
+    @BeforeEach
65
+    void setUp() {
66
+        when(stringRedisTemplate.opsForValue()).thenReturn(valueOps);
67
+        when(iotProperties.getMqtt()).thenReturn(mqttConfig);
68
+        when(mqttConfig.getVehicleTriggerUrl()).thenReturn("https://esos-iot.com:9443/syscar/trigger");
69
+    }
70
+
71
+    @Test
72
+    @DisplayName("updateSysCar: 获取锁失败时应跳过执行")
73
+    void updateSysCar_lockFail_skipsExecution() {
74
+        when(valueOps.setIfAbsent(anyString(), eq("1"), anyLong(), any())).thenReturn(false);
75
+
76
+        task.updateSysCar();
77
+
78
+        verify(sysCarService, never()).selectcontrollerId();
79
+    }
80
+
81
+    @Test
82
+    @DisplayName("updateSysCar: 获取锁成功时应执行车辆位置更新")
83
+    void updateSysCar_lockSuccess_executesUpdate() {
84
+        when(valueOps.setIfAbsent(anyString(), eq("1"), anyLong(), any())).thenReturn(true);
85
+
86
+        SysCar car = new SysCar();
87
+        car.setCarId("1");
88
+        car.setControllerId("CTRL001");
89
+        when(sysCarService.selectcontrollerId()).thenReturn(Collections.singletonList(car));
90
+
91
+        SysDevice lat = new SysDevice();
92
+        lat.setV("31.2304");
93
+        SysDevice lon = new SysDevice();
94
+        lon.setV("121.4737");
95
+        when(sysDeviceService.selectsysdevice("CTRL001", "纬度")).thenReturn(lat);
96
+        when(sysDeviceService.selectsysdevice("CTRL001", "经度")).thenReturn(lon);
97
+
98
+        when(stringRedisTemplate.opsForHash()).thenReturn(mock(org.springframework.data.redis.core.HashOperations.class));
99
+        when(stringRedisTemplate.delete(anyString())).thenReturn(true);
100
+
101
+        task.updateSysCar();
102
+
103
+        verify(sysCarService).selectcontrollerId();
104
+    }
105
+
106
+    @Test
107
+    @DisplayName("updateSysCar: 单条记录异常时不应中断整个批次")
108
+    void updateSysCar_singleRecordException_continuesBatch() {
109
+        when(valueOps.setIfAbsent(anyString(), eq("1"), anyLong(), any())).thenReturn(true);
110
+
111
+        SysCar car1 = new SysCar();
112
+        car1.setCarId("1");
113
+        car1.setControllerId("CTRL001");
114
+        SysCar car2 = new SysCar();
115
+        car2.setCarId("2");
116
+        car2.setControllerId("CTRL002");
117
+        when(sysCarService.selectcontrollerId()).thenReturn(Arrays.asList(car1, car2));
118
+
119
+        // car1 正常
120
+        SysDevice lat1 = new SysDevice();
121
+        lat1.setV("31.2304");
122
+        SysDevice lon1 = new SysDevice();
123
+        lon1.setV("121.4737");
124
+        when(sysDeviceService.selectsysdevice("CTRL001", "纬度")).thenReturn(lat1);
125
+        when(sysDeviceService.selectsysdevice("CTRL001", "经度")).thenReturn(lon1);
126
+
127
+        // car2 抛异常
128
+        when(sysDeviceService.selectsysdevice("CTRL002", "纬度"))
129
+                .thenThrow(new DataAccessException("DB error") {});
130
+
131
+        when(stringRedisTemplate.opsForHash()).thenReturn(mock(org.springframework.data.redis.core.HashOperations.class));
132
+        when(stringRedisTemplate.delete(anyString())).thenReturn(true);
133
+
134
+        task.updateSysCar();
135
+
136
+        // car1 正常执行,car2 异常被捕获,两者都应该尝试
137
+        verify(sysDeviceService).selectsysdevice("CTRL001", "纬度");
138
+        verify(sysDeviceService).selectsysdevice("CTRL002", "纬度");
139
+    }
140
+
141
+    @Test
142
+    @DisplayName("insertDevice: Redis 连接失败时应跳过执行")
143
+    void insertDevice_redisConnectionFailure_skipsGracefully() {
144
+        when(valueOps.setIfAbsent(anyString(), eq("1"), anyLong(), any())).thenReturn(true);
145
+        when(stringRedisTemplate.opsForSet()).thenThrow(
146
+                new RedisConnectionFailureException("Redis down"));
147
+        when(stringRedisTemplate.delete(anyString())).thenReturn(true);
148
+
149
+        task.insertDevice();
150
+
151
+        verify(sysDeviceControlService, never()).selectdevice(anyString());
152
+    }
153
+
154
+    @Test
155
+    @DisplayName("insertDevice: 空数据时应直接返回")
156
+    void insertDevice_emptyData_returnsEarly() {
157
+        when(valueOps.setIfAbsent(anyString(), eq("1"), anyLong(), any())).thenReturn(true);
158
+
159
+        org.springframework.data.redis.core.SetOperations setOps = mock(org.springframework.data.redis.core.SetOperations.class);
160
+        when(stringRedisTemplate.opsForSet()).thenReturn(setOps);
161
+        when(setOps.members("DSB:active:devices")).thenReturn(Collections.emptySet());
162
+        when(stringRedisTemplate.delete(anyString())).thenReturn(true);
163
+
164
+        task.insertDevice();
165
+
166
+        verify(sysDeviceControlService, never()).selectdevice(anyString());
167
+    }
168
+
169
+    @Test
170
+    @DisplayName("syncRedisToMySQL: 空活跃 key 时应直接返回")
171
+    void syncRedisToMySQL_emptyKeys_returnsEarly() {
172
+        when(valueOps.setIfAbsent(anyString(), eq("1"), anyLong(), any())).thenReturn(true);
173
+
174
+        org.springframework.data.redis.core.SetOperations setOps = mock(org.springframework.data.redis.core.SetOperations.class);
175
+        when(stringRedisTemplate.opsForSet()).thenReturn(setOps);
176
+        when(setOps.members("DSB:active:devices")).thenReturn(null);
177
+        when(stringRedisTemplate.delete(anyString())).thenReturn(true);
178
+
179
+        task.syncRedisToMySQL();
180
+
181
+        verify(sysrealtimeService, never()).createrealtime(anyString());
182
+    }
183
+
184
+//    @Test
185
+//    @DisplayName("insertIndicators: 公司列表为空时不应抛异常")
186
+//    void insertIndicators_emptyCompanyList_noException() {
187
+//        when(valueOps.setIfAbsent(anyString(), eq("1"), anyLong(), any())).thenReturn(true);
188
+//        when(sysCompanyService.selectcompany()).thenReturn(Collections.emptyList());
189
+//        when(stringRedisTemplate.delete(anyString())).thenReturn(true);
190
+//
191
+//        task.insertIndicators();
192
+//
193
+//        verify(sysWorkorderService, never()).selectworkordercount(anyString(), anyString());
194
+//    }
195
+
196
+    @Test
197
+    @DisplayName("webhook 调用失败时不应中断主流程")
198
+    void updateCarPosition_webhookFailure_continues() {
199
+        when(valueOps.setIfAbsent(anyString(), eq("1"), anyLong(), any())).thenReturn(true);
200
+
201
+        SysCar car = new SysCar();
202
+        car.setCarId("1");
203
+        car.setControllerId("CTRL001");
204
+        when(sysCarService.selectcontrollerId()).thenReturn(Collections.singletonList(car));
205
+
206
+        SysDevice lat = new SysDevice();
207
+        lat.setV("31.2304");
208
+        SysDevice lon = new SysDevice();
209
+        lon.setV("121.4737");
210
+        when(sysDeviceService.selectsysdevice("CTRL001", "纬度")).thenReturn(lat);
211
+        when(sysDeviceService.selectsysdevice("CTRL001", "经度")).thenReturn(lon);
212
+
213
+        when(stringRedisTemplate.opsForHash()).thenReturn(mock(org.springframework.data.redis.core.HashOperations.class));
214
+        when(stringRedisTemplate.delete(anyString())).thenReturn(true);
215
+        when(restTemplate.postForObject(anyString(), isNull(), eq(String.class)))
216
+                .thenThrow(new RestClientException("Connection refused"));
217
+
218
+        // 不应抛异常
219
+        task.updateSysCar();
220
+    }
221
+}

+ 0
- 70
pom.xml Visa fil

@@ -20,16 +20,9 @@
20 20
         <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
21 21
         <spring-boot.version>2.5.15</spring-boot.version>
22 22
         <druid.version>1.2.23</druid.version>
23
-        <bitwalker.version>1.21</bitwalker.version>
24
-        <swagger.version>3.0.0</swagger.version>
25
-        <kaptcha.version>2.3.3</kaptcha.version>
26 23
         <pagehelper.boot.version>1.4.7</pagehelper.boot.version>
27 24
         <fastjson.version>2.0.57</fastjson.version>
28
-        <oshi.version>6.8.2</oshi.version>
29 25
         <commons.io.version>2.19.0</commons.io.version>
30
-        <poi.version>4.1.2</poi.version>
31
-        <velocity.version>2.3</velocity.version>
32
-        <jwt.version>0.9.1</jwt.version>
33 26
         <!-- override dependency version -->
34 27
         <tomcat.version>9.0.106</tomcat.version>
35 28
         <logback.version>1.2.13</logback.version>
@@ -107,13 +100,6 @@
107 100
                 <version>${druid.version}</version>
108 101
             </dependency>
109 102
 
110
-            <!-- 解析客户端操作系统、浏览器等 -->
111
-            <dependency>
112
-                <groupId>eu.bitwalker</groupId>
113
-                <artifactId>UserAgentUtils</artifactId>
114
-                <version>${bitwalker.version}</version>
115
-            </dependency>
116
-
117 103
             <!-- pagehelper 分页插件 -->
118 104
             <dependency>
119 105
                 <groupId>com.github.pagehelper</groupId>
@@ -121,26 +107,6 @@
121 107
                 <version>${pagehelper.boot.version}</version>
122 108
             </dependency>
123 109
 
124
-            <!-- 获取系统信息 -->
125
-            <dependency>
126
-                <groupId>com.github.oshi</groupId>
127
-                <artifactId>oshi-core</artifactId>
128
-                <version>${oshi.version}</version>
129
-            </dependency>
130
-
131
-            <!-- Swagger3依赖 -->
132
-            <dependency>
133
-                <groupId>io.springfox</groupId>
134
-                <artifactId>springfox-boot-starter</artifactId>
135
-                <version>${swagger.version}</version>
136
-                <exclusions>
137
-                    <exclusion>
138
-                        <groupId>io.swagger</groupId>
139
-                        <artifactId>swagger-models</artifactId>
140
-                    </exclusion>
141
-                </exclusions>
142
-            </dependency>
143
-
144 110
             <!-- io常用工具类 -->
145 111
             <dependency>
146 112
                 <groupId>commons-io</groupId>
@@ -148,42 +114,6 @@
148 114
                 <version>${commons.io.version}</version>
149 115
             </dependency>
150 116
 
151
-            <!-- excel工具 -->
152
-            <dependency>
153
-                <groupId>org.apache.poi</groupId>
154
-                <artifactId>poi-ooxml</artifactId>
155
-                <version>${poi.version}</version>
156
-            </dependency>
157
-
158
-            <!-- velocity代码生成使用模板 -->
159
-            <dependency>
160
-                <groupId>org.apache.velocity</groupId>
161
-                <artifactId>velocity-engine-core</artifactId>
162
-                <version>${velocity.version}</version>
163
-            </dependency>
164
-
165
-            <!-- 阿里JSON解析器 -->
166
-            <dependency>
167
-                <groupId>com.alibaba.fastjson2</groupId>
168
-                <artifactId>fastjson2</artifactId>
169
-                <version>${fastjson.version}</version>
170
-            </dependency>
171
-
172
-            <!-- Token生成与解析-->
173
-            <dependency>
174
-                <groupId>io.jsonwebtoken</groupId>
175
-                <artifactId>jjwt</artifactId>
176
-                <version>${jwt.version}</version>
177
-            </dependency>
178
-
179
-            <!-- 验证码 -->
180
-            <dependency>
181
-                <groupId>pro.fessional</groupId>
182
-                <artifactId>kaptcha</artifactId>
183
-                <version>${kaptcha.version}</version>
184
-            </dependency>
185
-
186
-
187 117
         </dependencies>
188 118
     </dependencyManagement>
189 119
 

+ 0
- 67
ry.bat Visa fil

@@ -1,67 +0,0 @@
1
-@echo off
2
-
3
-rem jar平级目录
4
-set AppName=ruoyi-admin.jar
5
-
6
-rem JVM参数
7
-set JVM_OPTS="-Dname=%AppName%  -Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDateStamps  -XX:+PrintGCDetails -XX:NewRatio=1 -XX:SurvivorRatio=30 -XX:+UseParallelGC -XX:+UseParallelOldGC"
8
-
9
-
10
-ECHO.
11
-	ECHO.  [1] 启动%AppName%
12
-	ECHO.  [2] 关闭%AppName%
13
-	ECHO.  [3] 重启%AppName%
14
-	ECHO.  [4] 启动状态 %AppName%
15
-	ECHO.  [5] 退 出
16
-ECHO.
17
-
18
-ECHO.请输入选择项目的序号:
19
-set /p ID=
20
-	IF "%id%"=="1" GOTO start
21
-	IF "%id%"=="2" GOTO stop
22
-	IF "%id%"=="3" GOTO restart
23
-	IF "%id%"=="4" GOTO status
24
-	IF "%id%"=="5" EXIT
25
-PAUSE
26
-:start
27
-    for /f "usebackq tokens=1-2" %%a in (`jps -l ^| findstr %AppName%`) do (
28
-		set pid=%%a
29
-		set image_name=%%b
30
-	)
31
-	if  defined pid (
32
-		echo %%is running
33
-		PAUSE
34
-	)
35
-
36
-start javaw %JVM_OPTS% -jar %AppName%
37
-
38
-echo  starting……
39
-echo  Start %AppName% success...
40
-goto:eof
41
-
42
-rem 函数stop通过jps命令查找pid并结束进程
43
-:stop
44
-	for /f "usebackq tokens=1-2" %%a in (`jps -l ^| findstr %AppName%`) do (
45
-		set pid=%%a
46
-		set image_name=%%b
47
-	)
48
-	if not defined pid (echo process %AppName% does not exists) else (
49
-		echo prepare to kill %image_name%
50
-		echo start kill %pid% ...
51
-		rem 根据进程ID,kill进程
52
-		taskkill /f /pid %pid%
53
-	)
54
-goto:eof
55
-:restart
56
-	call :stop
57
-    call :start
58
-goto:eof
59
-:status
60
-	for /f "usebackq tokens=1-2" %%a in (`jps -l ^| findstr %AppName%`) do (
61
-		set pid=%%a
62
-		set image_name=%%b
63
-	)
64
-	if not defined pid (echo process %AppName% is dead ) else (
65
-		echo %image_name% is running
66
-	)
67
-goto:eof

+ 0
- 86
ry.sh Visa fil

@@ -1,86 +0,0 @@
1
-#!/bin/sh
2
-# ./ry.sh start 启动 stop 停止 restart 重启 status 状态
3
-AppName=ruoyi-admin.jar
4
-
5
-# JVM参数
6
-JVM_OPTS="-Dname=$AppName  -Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDateStamps  -XX:+PrintGCDetails -XX:NewRatio=1 -XX:SurvivorRatio=30 -XX:+UseParallelGC -XX:+UseParallelOldGC"
7
-APP_HOME=`pwd`
8
-LOG_PATH=$APP_HOME/logs/$AppName.log
9
-
10
-if [ "$1" = "" ];
11
-then
12
-    echo -e "\033[0;31m 未输入操作名 \033[0m  \033[0;34m {start|stop|restart|status} \033[0m"
13
-    exit 1
14
-fi
15
-
16
-if [ "$AppName" = "" ];
17
-then
18
-    echo -e "\033[0;31m 未输入应用名 \033[0m"
19
-    exit 1
20
-fi
21
-
22
-function start()
23
-{
24
-    PID=`ps -ef |grep java|grep $AppName|grep -v grep|awk '{print $2}'`
25
-
26
-	if [ x"$PID" != x"" ]; then
27
-	    echo "$AppName is running..."
28
-	else
29
-		nohup java $JVM_OPTS -jar $AppName > /dev/null 2>&1 &
30
-		echo "Start $AppName success..."
31
-	fi
32
-}
33
-
34
-function stop()
35
-{
36
-    echo "Stop $AppName"
37
-
38
-	PID=""
39
-	query(){
40
-		PID=`ps -ef |grep java|grep $AppName|grep -v grep|awk '{print $2}'`
41
-	}
42
-
43
-	query
44
-	if [ x"$PID" != x"" ]; then
45
-		kill -TERM $PID
46
-		echo "$AppName (pid:$PID) exiting..."
47
-		while [ x"$PID" != x"" ]
48
-		do
49
-			sleep 1
50
-			query
51
-		done
52
-		echo "$AppName exited."
53
-	else
54
-		echo "$AppName already stopped."
55
-	fi
56
-}
57
-
58
-function restart()
59
-{
60
-    stop
61
-    sleep 2
62
-    start
63
-}
64
-
65
-function status()
66
-{
67
-    PID=`ps -ef |grep java|grep $AppName|grep -v grep|wc -l`
68
-    if [ $PID != 0 ];then
69
-        echo "$AppName is running..."
70
-    else
71
-        echo "$AppName is not running..."
72
-    fi
73
-}
74
-
75
-case $1 in
76
-    start)
77
-    start;;
78
-    stop)
79
-    stop;;
80
-    restart)
81
-    restart;;
82
-    status)
83
-    status;;
84
-    *)
85
-
86
-esac

Laddar…
Avbryt
Spara