ソースを参照

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 5日前
コミット
4e57215f6f

+ 21
- 0
iot-platform/pom.xml ファイルの表示

153
                     </execution>
153
                     </execution>
154
                 </executions>
154
                 </executions>
155
             </plugin>
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
         </plugins>
177
         </plugins>
157
         <finalName>${project.artifactId}</finalName>
178
         <finalName>${project.artifactId}</finalName>
158
     </build>
179
     </build>

+ 42
- 0
iot-platform/src/main/java/com/iot/platform/config/ExecutorConfig.java ファイルの表示

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 ファイルの表示

3
 import org.springframework.boot.context.properties.ConfigurationProperties;
3
 import org.springframework.boot.context.properties.ConfigurationProperties;
4
 import org.springframework.stereotype.Component;
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
  * IoT平台配置属性
11
  * IoT平台配置属性
8
  */
12
  */
13
     private Mqtt mqtt = new Mqtt();
17
     private Mqtt mqtt = new Mqtt();
14
     private TDengine tdengine = new TDengine();
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
     public Mqtt getMqtt() {
36
     public Mqtt getMqtt() {
17
         return mqtt;
37
         return mqtt;
18
     }
38
     }
34
      */
54
      */
35
     public static class Mqtt {
55
     public static class Mqtt {
36
         private String brokerUrl = "tcp://47.104.204.180:1883";
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
         public String getBrokerUrl() {
60
         public String getBrokerUrl() {
41
             return brokerUrl;
61
             return brokerUrl;
67
      */
87
      */
68
     public static class TDengine {
88
     public static class TDengine {
69
         private String url = "jdbc:TAOS://localhost:6030/";
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
         public String getUrl() {
93
         public String getUrl() {
74
             return url;
94
             return url;

+ 21
- 0
iot-platform/src/main/java/com/iot/platform/config/RestTemplateConfig.java ファイルの表示

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 ファイルの表示

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

+ 20
- 4
iot-platform/src/main/java/com/iot/platform/mqtt/MqttDynamicConsumer.java ファイルの表示

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

+ 8
- 4
iot-platform/src/main/java/com/iot/platform/mqtt/MqttFaultConsumer.java ファイルの表示

15
 import java.time.format.DateTimeFormatter;
15
 import java.time.format.DateTimeFormatter;
16
 import java.util.*;
16
 import java.util.*;
17
 import java.util.concurrent.ExecutorService;
17
 import java.util.concurrent.ExecutorService;
18
-import java.util.concurrent.Executors;
19
 
18
 
20
 /**
19
 /**
21
  * 添加告警信息
20
  * 添加告警信息
41
     private RestTemplate restTemplate;
40
     private RestTemplate restTemplate;
42
 
41
 
43
     private final ObjectMapper objectMapper = new ObjectMapper();
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
     private static final Map<String, String> KEY_MAPPING = new HashMap<>();
50
     private static final Map<String, String> KEY_MAPPING = new HashMap<>();
47
     static {
51
     static {
66
         Map<String, Object> messageMap = objectMapper.readValue(messageContent, Map.class);
70
         Map<String, Object> messageMap = objectMapper.readValue(messageContent, Map.class);
67
         insertTDegine(messageMap, topic);
71
         insertTDegine(messageMap, topic);
68
         SysFault sysFault = objectMapper.readValue(messageContent, SysFault.class);
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
     @Override
76
     @Override
73
     protected void onDestroy() {
77
     protected void onDestroy() {
74
-        mysqlWritePool.shutdown();
78
+        // 线程池由 Spring 管理生命周期,无需手动 shutdown
75
     }
79
     }
76
 
80
 
77
     public void insertTDegine(Map<String, Object> weather, String topic) throws SQLException {
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 ファイルの表示

23
     @Autowired
23
     @Autowired
24
     private IotProperties iotProperties;
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
     private HikariDataSource dataSource;
33
     private HikariDataSource dataSource;
39
     private boolean dataSourceInitialized = false;
34
     private boolean dataSourceInitialized = false;
47
     // ObjectMapper 线程安全,可复用
42
     // ObjectMapper 线程安全,可复用
48
     private static final ObjectMapper objectMapper = new ObjectMapper();
43
     private static final ObjectMapper objectMapper = new ObjectMapper();
49
 
44
 
50
-    public TDengineService() {
51
-        // 延迟初始化:不在构造器中创建连接池,避免 TDengine 本地库缺失时阻断启动
52
-    }
53
-
54
     private synchronized void initDataSource() {
45
     private synchronized void initDataSource() {
55
         if (dataSourceInitialized) {
46
         if (dataSourceInitialized) {
56
             return;
47
             return;
349
     private void ensureTableExists(String dbName, String supertablename, String table) {
340
     private void ensureTableExists(String dbName, String supertablename, String table) {
350
         Connection conn = null;
341
         Connection conn = null;
351
         Statement stmt = null;
342
         Statement stmt = null;
343
+        ResultSet rs = null;
352
         try {
344
         try {
353
             conn = getConnection();
345
             conn = getConnection();
354
             stmt = conn.createStatement();
346
             stmt = conn.createStatement();
358
             String checkStableSql = String.format(
350
             String checkStableSql = String.format(
359
                     "SELECT * FROM information_schema.ins_tables WHERE table_name = '%s' AND db_name = '%s'",
351
                     "SELECT * FROM information_schema.ins_tables WHERE table_name = '%s' AND db_name = '%s'",
360
                     escapeValue(supertablename), escapeValue(dbName));
352
                     escapeValue(supertablename), escapeValue(dbName));
361
-            ResultSet rs = stmt.executeQuery(checkStableSql);
353
+            rs = stmt.executeQuery(checkStableSql);
362
             boolean stableExists = rs.next();
354
             boolean stableExists = rs.next();
363
-            rs.close();
364
 
355
 
365
             if (!stableExists) {
356
             if (!stableExists) {
357
+                closeQuietly(rs);
358
+                rs = null;
366
                 log.info("超级表不存在,创建: {}.{}", dbName, supertablename);
359
                 log.info("超级表不存在,创建: {}.{}", dbName, supertablename);
367
                 initTableStructure(dbName, supertablename, table, Collections.emptySet());
360
                 initTableStructure(dbName, supertablename, table, Collections.emptySet());
368
                 return;
361
                 return;
369
             }
362
             }
370
 
363
 
364
+            closeQuietly(rs);
365
+            rs = null;
366
+
371
             // 检查子表是否存在
367
             // 检查子表是否存在
372
             String checkTableSql = String.format(
368
             String checkTableSql = String.format(
373
                     "SELECT * FROM information_schema.ins_tables WHERE table_name = '%s' AND db_name = '%s' AND table_type = 'CHILD_TABLE'",
369
                     "SELECT * FROM information_schema.ins_tables WHERE table_name = '%s' AND db_name = '%s' AND table_type = 'CHILD_TABLE'",
374
                     escapeValue(table), escapeValue(dbName));
370
                     escapeValue(table), escapeValue(dbName));
375
             rs = stmt.executeQuery(checkTableSql);
371
             rs = stmt.executeQuery(checkTableSql);
376
             boolean tableExists = rs.next();
372
             boolean tableExists = rs.next();
377
-            rs.close();
378
 
373
 
379
             if (!tableExists) {
374
             if (!tableExists) {
380
                 String tableSql = String.format(
375
                 String tableSql = String.format(
391
         } catch (SQLException e) {
386
         } catch (SQLException e) {
392
             log.warn("检查表存在性失败,继续尝试插入: {}", e.getMessage());
387
             log.warn("检查表存在性失败,继续尝试插入: {}", e.getMessage());
393
         } finally {
388
         } finally {
394
-            if (stmt != null) try { stmt.close(); } catch (SQLException ignored) {}
389
+            closeQuietly(rs, stmt);
395
             closeConnection(conn);
390
             closeConnection(conn);
396
         }
391
         }
397
     }
392
     }
461
 
456
 
462
     public void close() {
457
     public void close() {
463
         log.info("关闭 TDengine 服务...");
458
         log.info("关闭 TDengine 服务...");
464
-        batchExecutor.shutdown();
459
+        // batchExecutor 由 Spring 管理生命周期,不在这里 shutdown
465
         if (dataSource != null) {
460
         if (dataSource != null) {
466
             dataSource.close();
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 ファイルの表示

8
 import org.slf4j.Logger;
8
 import org.slf4j.Logger;
9
 import org.slf4j.LoggerFactory;
9
 import org.slf4j.LoggerFactory;
10
 import org.springframework.beans.factory.annotation.Autowired;
10
 import org.springframework.beans.factory.annotation.Autowired;
11
+import org.springframework.dao.DataAccessException;
12
+import org.springframework.data.redis.RedisConnectionFailureException;
11
 import org.springframework.data.redis.core.RedisCallback;
13
 import org.springframework.data.redis.core.RedisCallback;
12
 import org.springframework.data.redis.core.ScanOptions;
14
 import org.springframework.data.redis.core.ScanOptions;
13
 import org.springframework.data.redis.core.StringRedisTemplate;
15
 import org.springframework.data.redis.core.StringRedisTemplate;
14
-import org.springframework.http.client.SimpleClientHttpRequestFactory;
15
 import org.springframework.scheduling.annotation.Scheduled;
16
 import org.springframework.scheduling.annotation.Scheduled;
16
 import org.springframework.stereotype.Component;
17
 import org.springframework.stereotype.Component;
18
+import org.springframework.web.client.RestClientException;
17
 import org.springframework.web.client.RestTemplate;
19
 import org.springframework.web.client.RestTemplate;
18
 
20
 
19
 import java.time.LocalDate;
21
 import java.time.LocalDate;
20
 import java.time.format.DateTimeFormatter;
22
 import java.time.format.DateTimeFormatter;
21
 import java.util.*;
23
 import java.util.*;
24
+import java.util.concurrent.TimeUnit;
22
 
25
 
23
 @Component
26
 @Component
24
 public class VehicleSyncTask {
27
 public class VehicleSyncTask {
43
     public SysIndicatorsService sysIndicatorsService;
46
     public SysIndicatorsService sysIndicatorsService;
44
     @Autowired
47
     @Autowired
45
     public SysCompanyService sysCompanyService;
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
      */
64
      */
60
     @Scheduled(fixedDelay = 30000)
65
     @Scheduled(fixedDelay = 30000)
61
     public void updateSysCar() {
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
         List<SysCar> sysCarList = sysCarService.selectcontrollerId();
80
         List<SysCar> sysCarList = sysCarService.selectcontrollerId();
63
         for (SysCar sysCar : sysCarList) {
81
         for (SysCar sysCar : sysCarList) {
64
             if (sysCar.getControllerId() == null || sysCar.getControllerId().isEmpty()) {
82
             if (sysCar.getControllerId() == null || sysCar.getControllerId().isEmpty()) {
95
         String url = "https://esos-iot.com:9443/syscar/trigger?carId=" + sysCar.getCarId();
113
         String url = "https://esos-iot.com:9443/syscar/trigger?carId=" + sysCar.getCarId();
96
         try {
114
         try {
97
             restTemplate.postForObject(url, null, String.class);
115
             restTemplate.postForObject(url, null, String.class);
98
-        } catch (Exception e) {
116
+        } catch (RestClientException e) {
99
             log.warn("触发webhook失败 carId={}: {}", sysCar.getCarId(), e.getMessage());
117
             log.warn("触发webhook失败 carId={}: {}", sysCar.getCarId(), e.getMessage());
100
         }
118
         }
101
     }
119
     }
120
 
138
 
121
     @Scheduled(fixedDelay = 30000)
139
     @Scheduled(fixedDelay = 30000)
122
     public void insertDevice() {
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
         Set<String> activeKeys;
154
         Set<String> activeKeys;
124
         try {
155
         try {
125
             activeKeys = stringRedisTemplate.opsForSet().members("DSB:active:devices");
156
             activeKeys = stringRedisTemplate.opsForSet().members("DSB:active:devices");
181
                     sysDeviceVoService.insertdevice(key.toString(), value.toString());
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
         } catch (Exception e) {
219
         } catch (Exception e) {
185
-            log.error("同步设备配置失败: {}", e.getMessage());
220
+            log.error("同步设备配置失败: {}", e.getMessage(), e);
186
         }
221
         }
187
     }
222
     }
188
 
223
 
191
      */
226
      */
192
     @Scheduled(fixedDelay = 30000)
227
     @Scheduled(fixedDelay = 30000)
193
     public void syncRedisToMySQL() {
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
         Set<String> activeKeys = stringRedisTemplate.opsForSet().members("DSB:active:devices");
242
         Set<String> activeKeys = stringRedisTemplate.opsForSet().members("DSB:active:devices");
195
         if (activeKeys == null || activeKeys.isEmpty()) return;
243
         if (activeKeys == null || activeKeys.isEmpty()) return;
196
 
244
 
240
                         sysrealtimeService.inserttables(controllerId, createTime, deviceId, timestamp, fieldKey, fieldValue);
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
             } catch (Exception e) {
295
             } catch (Exception e) {
244
                 log.error("同步设备失败: {} | {}", redisKey, e.getMessage(), e);
296
                 log.error("同步设备失败: {} | {}", redisKey, e.getMessage(), e);
245
             }
297
             }
257
      */
309
      */
258
     @Scheduled(fixedDelay = 30000)
310
     @Scheduled(fixedDelay = 30000)
259
     public void insertIndicators() {
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
         try {
325
         try {
261
             LocalDate today = LocalDate.now();
326
             LocalDate today = LocalDate.now();
262
             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
327
             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
272
                     sysIndicatorsService.updateindicators(countworkorder, countprofit, sysCompany.getCompanyId(), day);
337
                     sysIndicatorsService.updateindicators(countworkorder, countprofit, sysCompany.getCompanyId(), day);
273
                 }
338
                 }
274
             }
339
             }
340
+        } catch (DataAccessException e) {
341
+            log.error("数据库操作失败: {}", e.getMessage(), e);
275
         } catch (Exception e) {
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 ファイルの表示

1
-# 开发环境配置
1
+# 生产环境配置
2
 server:
2
 server:
3
   port: 8887
3
   port: 8887
4
   servlet:
4
   servlet:
5
     context-path: /
5
     context-path: /
6
   tomcat:
6
   tomcat:
7
     uri-encoding: UTF-8
7
     uri-encoding: UTF-8
8
-    accept-count: 1000
8
+    accept-count: 100
9
     threads:
9
     threads:
10
-      max: 800
11
-      min-spare: 100
10
+      max: 200
11
+      min-spare: 20
12
 
12
 
13
 # 日志配置
13
 # 日志配置
14
 logging:
14
 logging:
15
   level:
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
     org.springframework: warn
18
     org.springframework: warn
19
 
19
 
20
 # Spring配置
20
 # Spring配置
27
       max-request-size: 20MB
27
       max-request-size: 20MB
28
   devtools:
28
   devtools:
29
     restart:
29
     restart:
30
-      enabled: true
30
+      enabled: false
31
   redis:
31
   redis:
32
     host: localhost
32
     host: localhost
33
     port: 6379
33
     port: 6379
36
     timeout: 10s
36
     timeout: 10s
37
     lettuce:
37
     lettuce:
38
       pool:
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
 # MyBatis配置
44
 # MyBatis配置
45
 mybatis:
45
 mybatis:
57
   endpoints:
57
   endpoints:
58
     web:
58
     web:
59
       exposure:
59
       exposure:
60
-        include: health,info
60
+        include: health
61
   endpoint:
61
   endpoint:
62
     health:
62
     health:
63
-      show-details: always
63
+      show-details: never
64
 
64
 
65
 # IoT平台配置
65
 # IoT平台配置
66
 iot:
66
 iot:
67
   mqtt:
67
   mqtt:
68
     broker-url: tcp://47.104.204.180:1883
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
   tdengine:
71
   tdengine:
72
     url: jdbc:TAOS://localhost:6030/
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 ファイルの表示

42
     </appender>
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
         <appender-ref ref="CONSOLE"/>
46
         <appender-ref ref="CONSOLE"/>
47
         <appender-ref ref="FILE"/>
47
         <appender-ref ref="FILE"/>
48
     </logger>
48
     </logger>

+ 34
- 0
iot-platform/src/test/java/com/iot/platform/config/ExecutorConfigTest.java ファイルの表示

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 ファイルの表示

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 ファイルの表示

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
+}

読み込み中…
キャンセル
保存