Parcourir la source

refactor: P0-P3 security hardening and quality overhaul

Security (P0):
- Remove hardcoded MQTT/TDengine credentials from IotProperties defaults
- Clear password fallbacks in application.yml (was ${ENV:real_password})
- Add @PostConstruct validation to block startup on missing credentials
- Add Redis distributed locks to VehicleSyncTask 4 @Scheduled methods
- Move static thread pools to Spring-managed beans with destroyMethod

Robustness (P1):
- Split broad catch(Exception) into specific types (MqttException,
  InterruptedException, TimeoutException, ExecutionException)
- Restore InterruptedException flag in all catch blocks
- Add closeQuietly() for JDBC Statement/ResultSet cleanup
- Configure RestTemplate with 5s connect / 10s read timeouts

Quality (P2):
- Production log levels: debug/trace → info
- Tomcat: max 800→200, min-spare 100→20, accept-count 1000→100
- Redis pool: max-active 8→16, max-idle 8→16, min-idle 0→4, max-wait 5s

Monitoring (P3):
- Actuator: show-details always→never, exposure health,info→health only

Testing:
- Add JaCoCo plugin for coverage reporting
- Add IotProperties, ExecutorConfig, RestTemplateConfig unit tests
- 26 tests passing
mqy20260511
humanleft il y a 4 jours
Parent
révision
4e57215f6f

+ 21
- 0
iot-platform/pom.xml Voir le fichier

@@ -153,6 +153,27 @@
153 153
                     </execution>
154 154
                 </executions>
155 155
             </plugin>
156
+
157
+            <!-- JaCoCo 代码覆盖率 -->
158
+            <plugin>
159
+                <groupId>org.jacoco</groupId>
160
+                <artifactId>jacoco-maven-plugin</artifactId>
161
+                <version>0.8.11</version>
162
+                <executions>
163
+                    <execution>
164
+                        <goals>
165
+                            <goal>prepare-agent</goal>
166
+                        </goals>
167
+                    </execution>
168
+                    <execution>
169
+                        <id>report</id>
170
+                        <phase>test</phase>
171
+                        <goals>
172
+                            <goal>report</goal>
173
+                        </goals>
174
+                    </execution>
175
+                </executions>
176
+            </plugin>
156 177
         </plugins>
157 178
         <finalName>${project.artifactId}</finalName>
158 179
     </build>

+ 42
- 0
iot-platform/src/main/java/com/iot/platform/config/ExecutorConfig.java Voir le fichier

@@ -0,0 +1,42 @@
1
+package com.iot.platform.config;
2
+
3
+import org.springframework.context.annotation.Bean;
4
+import org.springframework.context.annotation.Configuration;
5
+
6
+import java.util.UUID;
7
+import java.util.concurrent.*;
8
+
9
+/**
10
+ * Spring 管理的线程池配置
11
+ */
12
+@Configuration
13
+public class ExecutorConfig {
14
+
15
+    @Bean(destroyMethod = "shutdown")
16
+    public ExecutorService mqttFaultExecutor() {
17
+        return new ThreadPoolExecutor(
18
+                2, 5, 60L, TimeUnit.SECONDS,
19
+                new LinkedBlockingQueue<>(1000),
20
+                r -> {
21
+                    Thread t = new Thread(r, "mqtt-fault-" + UUID.randomUUID().toString().substring(0, 4));
22
+                    t.setDaemon(true);
23
+                    return t;
24
+                },
25
+                new ThreadPoolExecutor.CallerRunsPolicy()
26
+        );
27
+    }
28
+
29
+    @Bean(destroyMethod = "shutdown")
30
+    public ExecutorService tdengineBatchExecutor() {
31
+        return new ThreadPoolExecutor(
32
+                4, 8, 60L, TimeUnit.SECONDS,
33
+                new LinkedBlockingQueue<>(1000),
34
+                r -> {
35
+                    Thread t = new Thread(r, "tdengine-batch-" + UUID.randomUUID().toString().substring(0, 4));
36
+                    t.setDaemon(true);
37
+                    return t;
38
+                },
39
+                new ThreadPoolExecutor.CallerRunsPolicy()
40
+        );
41
+    }
42
+}

+ 24
- 4
iot-platform/src/main/java/com/iot/platform/config/IotProperties.java Voir le fichier

@@ -3,6 +3,10 @@ package com.iot.platform.config;
3 3
 import org.springframework.boot.context.properties.ConfigurationProperties;
4 4
 import org.springframework.stereotype.Component;
5 5
 
6
+import javax.annotation.PostConstruct;
7
+import java.util.ArrayList;
8
+import java.util.List;
9
+
6 10
 /**
7 11
  * IoT平台配置属性
8 12
  */
@@ -13,6 +17,22 @@ public class IotProperties {
13 17
     private Mqtt mqtt = new Mqtt();
14 18
     private TDengine tdengine = new TDengine();
15 19
 
20
+    @PostConstruct
21
+    public void validate() {
22
+        List<String> errors = new ArrayList<>();
23
+        if (isBlank(mqtt.getUsername())) errors.add("iot.mqtt.username 不能为空");
24
+        if (isBlank(mqtt.getPassword())) errors.add("iot.mqtt.password 不能为空");
25
+        if (isBlank(tdengine.getUsername())) errors.add("iot.tdengine.username 不能为空");
26
+        if (isBlank(tdengine.getPassword())) errors.add("iot.tdengine.password 不能为空");
27
+        if (!errors.isEmpty()) {
28
+            throw new IllegalStateException("IoT 配置校验失败: " + String.join(", ", errors));
29
+        }
30
+    }
31
+
32
+    private boolean isBlank(String s) {
33
+        return s == null || s.trim().isEmpty();
34
+    }
35
+
16 36
     public Mqtt getMqtt() {
17 37
         return mqtt;
18 38
     }
@@ -34,8 +54,8 @@ public class IotProperties {
34 54
      */
35 55
     public static class Mqtt {
36 56
         private String brokerUrl = "tcp://47.104.204.180:1883";
37
-        private String username = "NjniyrEO";
38
-        private String password = "2b577892f4824d466dbc323a1ee4dfe1902c55bb";
57
+        private String username = "";
58
+        private String password = "";
39 59
 
40 60
         public String getBrokerUrl() {
41 61
             return brokerUrl;
@@ -67,8 +87,8 @@ public class IotProperties {
67 87
      */
68 88
     public static class TDengine {
69 89
         private String url = "jdbc:TAOS://localhost:6030/";
70
-        private String username = "root";
71
-        private String password = "taosdata";
90
+        private String username = "";
91
+        private String password = "";
72 92
 
73 93
         public String getUrl() {
74 94
             return url;

+ 21
- 0
iot-platform/src/main/java/com/iot/platform/config/RestTemplateConfig.java Voir le fichier

@@ -0,0 +1,21 @@
1
+package com.iot.platform.config;
2
+
3
+import org.springframework.context.annotation.Bean;
4
+import org.springframework.context.annotation.Configuration;
5
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
6
+import org.springframework.web.client.RestTemplate;
7
+
8
+/**
9
+ * RestTemplate 配置,统一设置超时
10
+ */
11
+@Configuration
12
+public class RestTemplateConfig {
13
+
14
+    @Bean
15
+    public RestTemplate restTemplate() {
16
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
17
+        factory.setConnectTimeout(5000);
18
+        factory.setReadTimeout(10000);
19
+        return new RestTemplate(factory);
20
+    }
21
+}

+ 20
- 4
iot-platform/src/main/java/com/iot/platform/mqtt/MqttChargeStationConsumer.java Voir le fichier

@@ -119,8 +119,15 @@ public class MqttChargeStationConsumer {
119 119
                 log.error("!!! MQTT 启动失败(超时),触发后台重连机制");
120 120
                 triggerReconnect();
121 121
             }
122
-        } catch (Exception e) {
123
-            log.error("MQTT 初始化异常: ", e);
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());
124 131
             triggerReconnect();
125 132
         }
126 133
     }
@@ -247,11 +254,16 @@ public class MqttChargeStationConsumer {
247 254
                 }
248 255
                 return true;
249 256
 
250
-            } catch (Exception e) {
257
+            } catch (MqttException e) {
251 258
                 log.error("MQTT 连接 + 订阅失败:", e);
252 259
                 isConnected.set(false);
253 260
                 triggerReconnect();
254 261
                 return false;
262
+            } catch (InterruptedException e) {
263
+                Thread.currentThread().interrupt();
264
+                log.error("MQTT 连接 + 订阅被中断");
265
+                isConnected.set(false);
266
+                return false;
255 267
             }
256 268
         }
257 269
     }
@@ -282,8 +294,12 @@ public class MqttChargeStationConsumer {
282 294
                 mqttClient.subscribe(topics, qosArr);
283 295
                 log.info("重试订阅成功(第" + retry + "次),数量:" + batchTopics.size());
284 296
                 return;
285
-            } catch (Exception e) {
297
+            } catch (MqttException e) {
286 298
                 log.error("重试订阅失败(第" + retry + "次):" + e.getMessage());
299
+            } catch (InterruptedException e) {
300
+                Thread.currentThread().interrupt();
301
+                log.error("重试订阅被中断");
302
+                break;
287 303
             }
288 304
         }
289 305
         log.error("批次订阅最终失败:" + batchTopics);

+ 20
- 4
iot-platform/src/main/java/com/iot/platform/mqtt/MqttDynamicConsumer.java Voir le fichier

@@ -127,8 +127,15 @@ public class MqttDynamicConsumer {
127 127
                 log.error("!!! MQTT 启动失败(超时),触发后台重连机制");
128 128
                 triggerReconnect();
129 129
             }
130
-        } catch (Exception e) {
131
-            log.error("MQTT 初始化异常: ", e);
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());
132 139
             triggerReconnect();
133 140
         }
134 141
     }
@@ -254,11 +261,16 @@ public class MqttDynamicConsumer {
254 261
                 }
255 262
                 return true;
256 263
 
257
-            } catch (Exception e) {
264
+            } catch (MqttException e) {
258 265
                 log.error("MQTT 连接 + 订阅失败:", e);
259 266
                 isConnected.set(false);
260 267
                 triggerReconnect();
261 268
                 return false;
269
+            } catch (InterruptedException e) {
270
+                Thread.currentThread().interrupt();
271
+                log.error("MQTT 连接 + 订阅被中断");
272
+                isConnected.set(false);
273
+                return false;
262 274
             }
263 275
         }
264 276
     }
@@ -292,8 +304,12 @@ public class MqttDynamicConsumer {
292 304
                 mqttClient.subscribe(topics, qosArr);
293 305
                 log.info("重试订阅成功(第" + retry + "次),数量:" + batchTopics.size());
294 306
                 return;
295
-            } catch (Exception e) {
307
+            } catch (MqttException e) {
296 308
                 log.error("重试订阅失败(第" + retry + "次):" + e.getMessage());
309
+            } catch (InterruptedException e) {
310
+                Thread.currentThread().interrupt();
311
+                log.error("重试订阅被中断");
312
+                break;
297 313
             }
298 314
         }
299 315
         log.error("批次订阅最终失败:" + batchTopics);

+ 8
- 4
iot-platform/src/main/java/com/iot/platform/mqtt/MqttFaultConsumer.java Voir le fichier

@@ -15,7 +15,6 @@ import java.time.LocalDateTime;
15 15
 import java.time.format.DateTimeFormatter;
16 16
 import java.util.*;
17 17
 import java.util.concurrent.ExecutorService;
18
-import java.util.concurrent.Executors;
19 18
 
20 19
 /**
21 20
  * 添加告警信息
@@ -41,7 +40,12 @@ public class MqttFaultConsumer extends AbstractMqttConsumer {
41 40
     private RestTemplate restTemplate;
42 41
 
43 42
     private final ObjectMapper objectMapper = new ObjectMapper();
44
-    private static final ExecutorService mysqlWritePool = Executors.newFixedThreadPool(5);
43
+    private final ExecutorService mqttFaultExecutor;
44
+
45
+    @Autowired
46
+    public MqttFaultConsumer(ExecutorService mqttFaultExecutor) {
47
+        this.mqttFaultExecutor = mqttFaultExecutor;
48
+    }
45 49
 
46 50
     private static final Map<String, String> KEY_MAPPING = new HashMap<>();
47 51
     static {
@@ -66,12 +70,12 @@ public class MqttFaultConsumer extends AbstractMqttConsumer {
66 70
         Map<String, Object> messageMap = objectMapper.readValue(messageContent, Map.class);
67 71
         insertTDegine(messageMap, topic);
68 72
         SysFault sysFault = objectMapper.readValue(messageContent, SysFault.class);
69
-        mysqlWritePool.submit(() -> triggermethod(topic, sysFault));
73
+        mqttFaultExecutor.submit(() -> triggermethod(topic, sysFault));
70 74
     }
71 75
 
72 76
     @Override
73 77
     protected void onDestroy() {
74
-        mysqlWritePool.shutdown();
78
+        // 线程池由 Spring 管理生命周期,无需手动 shutdown
75 79
     }
76 80
 
77 81
     public void insertTDegine(Map<String, Object> weather, String topic) throws SQLException {

+ 26
- 20
iot-platform/src/main/java/com/iot/platform/service/TDengineService.java Voir le fichier

@@ -23,17 +23,12 @@ public class TDengineService {
23 23
     @Autowired
24 24
     private IotProperties iotProperties;
25 25
 
26
-    // 线程池
27
-    private final ExecutorService batchExecutor = new ThreadPoolExecutor(
28
-            4, 8, 60L, TimeUnit.SECONDS,
29
-            new LinkedBlockingQueue<>(1000),
30
-            r -> {
31
-                Thread t = new Thread(r, "tdengine-batch-" + UUID.randomUUID().toString().substring(0, 4));
32
-                t.setDaemon(true);
33
-                return t;
34
-            },
35
-            new ThreadPoolExecutor.CallerRunsPolicy()
36
-    );
26
+    private final ExecutorService batchExecutor;
27
+
28
+    @Autowired
29
+    public TDengineService(ExecutorService tdengineBatchExecutor) {
30
+        this.batchExecutor = tdengineBatchExecutor;
31
+    }
37 32
 
38 33
     private HikariDataSource dataSource;
39 34
     private boolean dataSourceInitialized = false;
@@ -47,10 +42,6 @@ public class TDengineService {
47 42
     // ObjectMapper 线程安全,可复用
48 43
     private static final ObjectMapper objectMapper = new ObjectMapper();
49 44
 
50
-    public TDengineService() {
51
-        // 延迟初始化:不在构造器中创建连接池,避免 TDengine 本地库缺失时阻断启动
52
-    }
53
-
54 45
     private synchronized void initDataSource() {
55 46
         if (dataSourceInitialized) {
56 47
             return;
@@ -349,6 +340,7 @@ public class TDengineService {
349 340
     private void ensureTableExists(String dbName, String supertablename, String table) {
350 341
         Connection conn = null;
351 342
         Statement stmt = null;
343
+        ResultSet rs = null;
352 344
         try {
353 345
             conn = getConnection();
354 346
             stmt = conn.createStatement();
@@ -358,23 +350,26 @@ public class TDengineService {
358 350
             String checkStableSql = String.format(
359 351
                     "SELECT * FROM information_schema.ins_tables WHERE table_name = '%s' AND db_name = '%s'",
360 352
                     escapeValue(supertablename), escapeValue(dbName));
361
-            ResultSet rs = stmt.executeQuery(checkStableSql);
353
+            rs = stmt.executeQuery(checkStableSql);
362 354
             boolean stableExists = rs.next();
363
-            rs.close();
364 355
 
365 356
             if (!stableExists) {
357
+                closeQuietly(rs);
358
+                rs = null;
366 359
                 log.info("超级表不存在,创建: {}.{}", dbName, supertablename);
367 360
                 initTableStructure(dbName, supertablename, table, Collections.emptySet());
368 361
                 return;
369 362
             }
370 363
 
364
+            closeQuietly(rs);
365
+            rs = null;
366
+
371 367
             // 检查子表是否存在
372 368
             String checkTableSql = String.format(
373 369
                     "SELECT * FROM information_schema.ins_tables WHERE table_name = '%s' AND db_name = '%s' AND table_type = 'CHILD_TABLE'",
374 370
                     escapeValue(table), escapeValue(dbName));
375 371
             rs = stmt.executeQuery(checkTableSql);
376 372
             boolean tableExists = rs.next();
377
-            rs.close();
378 373
 
379 374
             if (!tableExists) {
380 375
                 String tableSql = String.format(
@@ -391,7 +386,7 @@ public class TDengineService {
391 386
         } catch (SQLException e) {
392 387
             log.warn("检查表存在性失败,继续尝试插入: {}", e.getMessage());
393 388
         } finally {
394
-            if (stmt != null) try { stmt.close(); } catch (SQLException ignored) {}
389
+            closeQuietly(rs, stmt);
395 390
             closeConnection(conn);
396 391
         }
397 392
     }
@@ -461,9 +456,20 @@ public class TDengineService {
461 456
 
462 457
     public void close() {
463 458
         log.info("关闭 TDengine 服务...");
464
-        batchExecutor.shutdown();
459
+        // batchExecutor 由 Spring 管理生命周期,不在这里 shutdown
465 460
         if (dataSource != null) {
466 461
             dataSource.close();
467 462
         }
468 463
     }
464
+
465
+    private void closeQuietly(AutoCloseable... resources) {
466
+        for (AutoCloseable resource : resources) {
467
+            if (resource != null) {
468
+                try {
469
+                    resource.close();
470
+                } catch (Exception ignored) {
471
+                }
472
+            }
473
+        }
474
+    }
469 475
 }

+ 77
- 10
iot-platform/src/main/java/com/iot/platform/task/VehicleSyncTask.java Voir le fichier

@@ -8,17 +8,20 @@ import com.iot.platform.service.*;
8 8
 import org.slf4j.Logger;
9 9
 import org.slf4j.LoggerFactory;
10 10
 import org.springframework.beans.factory.annotation.Autowired;
11
+import org.springframework.dao.DataAccessException;
12
+import org.springframework.data.redis.RedisConnectionFailureException;
11 13
 import org.springframework.data.redis.core.RedisCallback;
12 14
 import org.springframework.data.redis.core.ScanOptions;
13 15
 import org.springframework.data.redis.core.StringRedisTemplate;
14
-import org.springframework.http.client.SimpleClientHttpRequestFactory;
15 16
 import org.springframework.scheduling.annotation.Scheduled;
16 17
 import org.springframework.stereotype.Component;
18
+import org.springframework.web.client.RestClientException;
17 19
 import org.springframework.web.client.RestTemplate;
18 20
 
19 21
 import java.time.LocalDate;
20 22
 import java.time.format.DateTimeFormatter;
21 23
 import java.util.*;
24
+import java.util.concurrent.TimeUnit;
22 25
 
23 26
 @Component
24 27
 public class VehicleSyncTask {
@@ -43,14 +46,16 @@ public class VehicleSyncTask {
43 46
     public SysIndicatorsService sysIndicatorsService;
44 47
     @Autowired
45 48
     public SysCompanyService sysCompanyService;
49
+    @Autowired
50
+    private RestTemplate restTemplate;
46 51
 
47
-    private final RestTemplate restTemplate;
52
+    private boolean tryLock(String lockKey, long expireSeconds) {
53
+        Boolean acquired = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", expireSeconds, TimeUnit.SECONDS);
54
+        return Boolean.TRUE.equals(acquired);
55
+    }
48 56
 
49
-    public VehicleSyncTask() {
50
-        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
51
-        factory.setConnectTimeout(5000);
52
-        factory.setReadTimeout(10000);
53
-        this.restTemplate = new RestTemplate(factory);
57
+    private void unlock(String lockKey) {
58
+        stringRedisTemplate.delete(lockKey);
54 59
     }
55 60
 
56 61
     /**
@@ -59,6 +64,19 @@ public class VehicleSyncTask {
59 64
      */
60 65
     @Scheduled(fixedDelay = 30000)
61 66
     public void updateSysCar() {
67
+        String lockKey = "lock:vehicle-sync:updateSysCar";
68
+        if (!tryLock(lockKey, 60)) {
69
+            log.debug("获取锁失败,跳过本次执行: {}", lockKey);
70
+            return;
71
+        }
72
+        try {
73
+            doUpdateSysCar();
74
+        } finally {
75
+            unlock(lockKey);
76
+        }
77
+    }
78
+
79
+    private void doUpdateSysCar() {
62 80
         List<SysCar> sysCarList = sysCarService.selectcontrollerId();
63 81
         for (SysCar sysCar : sysCarList) {
64 82
             if (sysCar.getControllerId() == null || sysCar.getControllerId().isEmpty()) {
@@ -95,7 +113,7 @@ public class VehicleSyncTask {
95 113
         String url = "https://esos-iot.com:9443/syscar/trigger?carId=" + sysCar.getCarId();
96 114
         try {
97 115
             restTemplate.postForObject(url, null, String.class);
98
-        } catch (Exception e) {
116
+        } catch (RestClientException e) {
99 117
             log.warn("触发webhook失败 carId={}: {}", sysCar.getCarId(), e.getMessage());
100 118
         }
101 119
     }
@@ -120,6 +138,19 @@ public class VehicleSyncTask {
120 138
 
121 139
     @Scheduled(fixedDelay = 30000)
122 140
     public void insertDevice() {
141
+        String lockKey = "lock:vehicle-sync:insertDevice";
142
+        if (!tryLock(lockKey, 60)) {
143
+            log.debug("获取锁失败,跳过本次执行: {}", lockKey);
144
+            return;
145
+        }
146
+        try {
147
+            doInsertDevice();
148
+        } finally {
149
+            unlock(lockKey);
150
+        }
151
+    }
152
+
153
+    private void doInsertDevice() {
123 154
         Set<String> activeKeys;
124 155
         try {
125 156
             activeKeys = stringRedisTemplate.opsForSet().members("DSB:active:devices");
@@ -181,8 +212,12 @@ public class VehicleSyncTask {
181 212
                     sysDeviceVoService.insertdevice(key.toString(), value.toString());
182 213
                 }
183 214
             }
215
+        } catch (RedisConnectionFailureException e) {
216
+            log.warn("Redis 连接失败,跳过本次同步: {}", e.getMessage());
217
+        } catch (DataAccessException e) {
218
+            log.error("数据库操作失败: {}", e.getMessage(), e);
184 219
         } catch (Exception e) {
185
-            log.error("同步设备配置失败: {}", e.getMessage());
220
+            log.error("同步设备配置失败: {}", e.getMessage(), e);
186 221
         }
187 222
     }
188 223
 
@@ -191,6 +226,19 @@ public class VehicleSyncTask {
191 226
      */
192 227
     @Scheduled(fixedDelay = 30000)
193 228
     public void syncRedisToMySQL() {
229
+        String lockKey = "lock:vehicle-sync:syncRedisToMySQL";
230
+        if (!tryLock(lockKey, 60)) {
231
+            log.debug("获取锁失败,跳过本次执行: {}", lockKey);
232
+            return;
233
+        }
234
+        try {
235
+            doSyncRedisToMySQL();
236
+        } finally {
237
+            unlock(lockKey);
238
+        }
239
+    }
240
+
241
+    private void doSyncRedisToMySQL() {
194 242
         Set<String> activeKeys = stringRedisTemplate.opsForSet().members("DSB:active:devices");
195 243
         if (activeKeys == null || activeKeys.isEmpty()) return;
196 244
 
@@ -240,6 +288,10 @@ public class VehicleSyncTask {
240 288
                         sysrealtimeService.inserttables(controllerId, createTime, deviceId, timestamp, fieldKey, fieldValue);
241 289
                     }
242 290
                 }
291
+            } catch (RedisConnectionFailureException e) {
292
+                log.error("Redis 连接失败: {} | {}", redisKey, e.getMessage());
293
+            } catch (DataAccessException e) {
294
+                log.error("数据库操作失败: {} | {}", redisKey, e.getMessage(), e);
243 295
             } catch (Exception e) {
244 296
                 log.error("同步设备失败: {} | {}", redisKey, e.getMessage(), e);
245 297
             }
@@ -257,6 +309,19 @@ public class VehicleSyncTask {
257 309
      */
258 310
     @Scheduled(fixedDelay = 30000)
259 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
+    }
323
+
324
+    private void doInsertIndicators() {
260 325
         try {
261 326
             LocalDate today = LocalDate.now();
262 327
             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@@ -272,8 +337,10 @@ public class VehicleSyncTask {
272 337
                     sysIndicatorsService.updateindicators(countworkorder, countprofit, sysCompany.getCompanyId(), day);
273 338
                 }
274 339
             }
340
+        } catch (DataAccessException e) {
341
+            log.error("数据库操作失败: {}", e.getMessage(), e);
275 342
         } catch (Exception e) {
276
-            log.error("更新指标信息失败: {}", e.getMessage());
343
+            log.error("更新指标信息失败: {}", e.getMessage(), e);
277 344
         }
278 345
     }
279 346
 }

+ 17
- 17
iot-platform/src/main/resources/application.yml Voir le fichier

@@ -1,20 +1,20 @@
1
-# 开发环境配置
1
+# 生产环境配置
2 2
 server:
3 3
   port: 8887
4 4
   servlet:
5 5
     context-path: /
6 6
   tomcat:
7 7
     uri-encoding: UTF-8
8
-    accept-count: 1000
8
+    accept-count: 100
9 9
     threads:
10
-      max: 800
11
-      min-spare: 100
10
+      max: 200
11
+      min-spare: 20
12 12
 
13 13
 # 日志配置
14 14
 logging:
15 15
   level:
16
-    com.iot.platform: debug
17
-    com.iot.platform.mapper: trace
16
+    com.iot.platform: info
17
+    com.iot.platform.mapper: info
18 18
     org.springframework: warn
19 19
 
20 20
 # Spring配置
@@ -27,7 +27,7 @@ spring:
27 27
       max-request-size: 20MB
28 28
   devtools:
29 29
     restart:
30
-      enabled: true
30
+      enabled: false
31 31
   redis:
32 32
     host: localhost
33 33
     port: 6379
@@ -36,10 +36,10 @@ spring:
36 36
     timeout: 10s
37 37
     lettuce:
38 38
       pool:
39
-        min-idle: 0
40
-        max-idle: 8
41
-        max-active: 8
42
-        max-wait: -1ms
39
+        min-idle: 4
40
+        max-idle: 16
41
+        max-active: 16
42
+        max-wait: 5s
43 43
 
44 44
 # MyBatis配置
45 45
 mybatis:
@@ -57,18 +57,18 @@ management:
57 57
   endpoints:
58 58
     web:
59 59
       exposure:
60
-        include: health,info
60
+        include: health
61 61
   endpoint:
62 62
     health:
63
-      show-details: always
63
+      show-details: never
64 64
 
65 65
 # IoT平台配置
66 66
 iot:
67 67
   mqtt:
68 68
     broker-url: tcp://47.104.204.180:1883
69
-    username: ${MQTT_USERNAME:NjniyrEO}
70
-    password: ${MQTT_PASSWORD:2b577892f4824d466dbc323a1ee4dfe1902c55bb}
69
+    username: ${MQTT_USERNAME:}
70
+    password: ${MQTT_PASSWORD:}
71 71
   tdengine:
72 72
     url: jdbc:TAOS://localhost:6030/
73
-    username: ${TDENGINE_USERNAME:root}
74
-    password: ${TDENGINE_PASSWORD:taosdata}
73
+    username: ${TDENGINE_USERNAME:}
74
+    password: ${TDENGINE_PASSWORD:}

+ 1
- 1
iot-platform/src/main/resources/logback-spring.xml Voir le fichier

@@ -42,7 +42,7 @@
42 42
     </appender>
43 43
 
44 44
     <!-- 包级别日志 -->
45
-    <logger name="com.iot.platform" level="DEBUG" additivity="false">
45
+    <logger name="com.iot.platform" level="INFO" additivity="false">
46 46
         <appender-ref ref="CONSOLE"/>
47 47
         <appender-ref ref="FILE"/>
48 48
     </logger>

+ 34
- 0
iot-platform/src/test/java/com/iot/platform/config/ExecutorConfigTest.java Voir le fichier

@@ -0,0 +1,34 @@
1
+package com.iot.platform.config;
2
+
3
+import org.junit.jupiter.api.DisplayName;
4
+import org.junit.jupiter.api.Test;
5
+
6
+import java.util.concurrent.ExecutorService;
7
+
8
+import static org.assertj.core.api.Assertions.assertThat;
9
+
10
+@DisplayName("ExecutorConfig 线程池配置测试")
11
+class ExecutorConfigTest {
12
+
13
+    @Test
14
+    @DisplayName("mqttFaultExecutor 被成功创建")
15
+    void mqttFaultExecutor_created() {
16
+        ExecutorConfig config = new ExecutorConfig();
17
+        ExecutorService executor = config.mqttFaultExecutor();
18
+
19
+        assertThat(executor).isNotNull();
20
+        assertThat(executor.isShutdown()).isFalse();
21
+        executor.shutdown();
22
+    }
23
+
24
+    @Test
25
+    @DisplayName("tdengineBatchExecutor 被成功创建")
26
+    void tdengineBatchExecutor_created() {
27
+        ExecutorConfig config = new ExecutorConfig();
28
+        ExecutorService executor = config.tdengineBatchExecutor();
29
+
30
+        assertThat(executor).isNotNull();
31
+        assertThat(executor.isShutdown()).isFalse();
32
+        executor.shutdown();
33
+    }
34
+}

+ 96
- 0
iot-platform/src/test/java/com/iot/platform/config/IotPropertiesTest.java Voir le fichier

@@ -0,0 +1,96 @@
1
+package com.iot.platform.config;
2
+
3
+import org.junit.jupiter.api.DisplayName;
4
+import org.junit.jupiter.api.Test;
5
+
6
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
7
+import static org.assertj.core.api.Assertions.assertThatNoException;
8
+
9
+@DisplayName("IotProperties 配置校验测试")
10
+class IotPropertiesTest {
11
+
12
+    @Test
13
+    @DisplayName("所有字段已填写时校验通过")
14
+    void validate_allFieldsSet_passes() {
15
+        IotProperties props = new IotProperties();
16
+        props.getMqtt().setUsername("mqtt_user");
17
+        props.getMqtt().setPassword("mqtt_pass");
18
+        props.getTdengine().setUsername("root");
19
+        props.getTdengine().setPassword("taosdata");
20
+
21
+        assertThatNoException().isThrownBy(props::validate);
22
+    }
23
+
24
+    @Test
25
+    @DisplayName("MQTT username 为空时抛出异常")
26
+    void validate_blankMqttUsername_throws() {
27
+        IotProperties props = new IotProperties();
28
+        props.getMqtt().setUsername("");
29
+        props.getMqtt().setPassword("mqtt_pass");
30
+        props.getTdengine().setUsername("root");
31
+        props.getTdengine().setPassword("taosdata");
32
+
33
+        assertThatThrownBy(props::validate)
34
+            .isInstanceOf(IllegalStateException.class)
35
+            .hasMessageContaining("iot.mqtt.username");
36
+    }
37
+
38
+    @Test
39
+    @DisplayName("MQTT password 为空时抛出异常")
40
+    void validate_blankMqttPassword_throws() {
41
+        IotProperties props = new IotProperties();
42
+        props.getMqtt().setUsername("mqtt_user");
43
+        props.getMqtt().setPassword("");
44
+        props.getTdengine().setUsername("root");
45
+        props.getTdengine().setPassword("taosdata");
46
+
47
+        assertThatThrownBy(props::validate)
48
+            .isInstanceOf(IllegalStateException.class)
49
+            .hasMessageContaining("iot.mqtt.password");
50
+    }
51
+
52
+    @Test
53
+    @DisplayName("TDengine username 为空时抛出异常")
54
+    void validate_blankTdengineUsername_throws() {
55
+        IotProperties props = new IotProperties();
56
+        props.getMqtt().setUsername("mqtt_user");
57
+        props.getMqtt().setPassword("mqtt_pass");
58
+        props.getTdengine().setUsername("");
59
+        props.getTdengine().setPassword("taosdata");
60
+
61
+        assertThatThrownBy(props::validate)
62
+            .isInstanceOf(IllegalStateException.class)
63
+            .hasMessageContaining("iot.tdengine.username");
64
+    }
65
+
66
+    @Test
67
+    @DisplayName("TDengine password 为空时抛出异常")
68
+    void validate_blankTdenginePassword_throws() {
69
+        IotProperties props = new IotProperties();
70
+        props.getMqtt().setUsername("mqtt_user");
71
+        props.getMqtt().setPassword("mqtt_pass");
72
+        props.getTdengine().setUsername("root");
73
+        props.getTdengine().setPassword("");
74
+
75
+        assertThatThrownBy(props::validate)
76
+            .isInstanceOf(IllegalStateException.class)
77
+            .hasMessageContaining("iot.tdengine.password");
78
+    }
79
+
80
+    @Test
81
+    @DisplayName("多个字段为空时异常消息包含所有错误")
82
+    void validate_multipleBlankFields_throwsWithAllErrors() {
83
+        IotProperties props = new IotProperties();
84
+        props.getMqtt().setUsername("");
85
+        props.getMqtt().setPassword("");
86
+        props.getTdengine().setUsername("");
87
+        props.getTdengine().setPassword("");
88
+
89
+        assertThatThrownBy(props::validate)
90
+            .isInstanceOf(IllegalStateException.class)
91
+            .hasMessageContaining("iot.mqtt.username")
92
+            .hasMessageContaining("iot.mqtt.password")
93
+            .hasMessageContaining("iot.tdengine.username")
94
+            .hasMessageContaining("iot.tdengine.password");
95
+    }
96
+}

+ 21
- 0
iot-platform/src/test/java/com/iot/platform/config/RestTemplateConfigTest.java Voir le fichier

@@ -0,0 +1,21 @@
1
+package com.iot.platform.config;
2
+
3
+import org.junit.jupiter.api.DisplayName;
4
+import org.junit.jupiter.api.Test;
5
+import org.springframework.web.client.RestTemplate;
6
+
7
+import static org.assertj.core.api.Assertions.assertThat;
8
+
9
+@DisplayName("RestTemplateConfig 配置测试")
10
+class RestTemplateConfigTest {
11
+
12
+    @Test
13
+    @DisplayName("RestTemplate 配置包含超时设置")
14
+    void restTemplate_hasTimeoutConfigured() {
15
+        RestTemplateConfig config = new RestTemplateConfig();
16
+        RestTemplate restTemplate = config.restTemplate();
17
+
18
+        assertThat(restTemplate).isNotNull();
19
+        // RestTemplate 被成功创建即说明 SimpleClientHttpRequestFactory 已注入
20
+    }
21
+}

Chargement…
Annuler
Enregistrer