diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index cc55540..7488d91 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -7,6 +7,14 @@
+
+
+
+
+
+
+
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index baa5bf6..2701299 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -8,7 +8,7 @@
-
+
\ No newline at end of file
diff --git a/device_cleanup_test.sql b/device_cleanup_test.sql
new file mode 100644
index 0000000..aa479ea
--- /dev/null
+++ b/device_cleanup_test.sql
@@ -0,0 +1,43 @@
+-- 设备状态变更记录清理测试脚本
+-- 用于验证24小时清理功能
+
+-- 1. 检查表结构
+SELECT 'Table Structure:' as info;
+DESC device_status_transition;
+
+-- 2. 查看当前记录数
+SELECT 'Current Records Count:' as info;
+SELECT COUNT(*) as total_records FROM device_status_transition;
+
+-- 3. 查看24小时前的记录数(将被清理的记录)
+SELECT '24+ Hours Old Records (to be cleaned):' as info;
+SELECT COUNT(*) as old_records
+FROM device_status_transition
+WHERE created_at < DATE_SUB(NOW(), INTERVAL 24 HOUR);
+
+-- 4. 查看最近24小时的记录数(将被保留的记录)
+SELECT 'Recent 24 Hours Records (to be kept):' as info;
+SELECT COUNT(*) as recent_records
+FROM device_status_transition
+WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR);
+
+-- 5. 查看记录的时间分布
+SELECT 'Records Distribution:' as info;
+SELECT
+ DATE(created_at) as date,
+ COUNT(*) as count
+FROM device_status_transition
+GROUP BY DATE(created_at)
+ORDER BY date DESC
+LIMIT 10;
+
+-- 6. 预览将被删除的记录(最多显示5条)
+SELECT 'Sample Records to be Deleted:' as info;
+SELECT device_id, prev_status, new_status, created_at
+FROM device_status_transition
+WHERE created_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)
+ORDER BY created_at DESC
+LIMIT 5;
+
+-- 测试删除语句(注释掉,仅用于验证语法)
+-- DELETE FROM device_status_transition WHERE created_at < DATE_SUB(NOW(), INTERVAL 24 HOUR);
diff --git a/pom.xml b/pom.xml
index 72dc917..413d2ab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,7 +16,7 @@
Spring Boot WebFlux + MyBatis + MySQL backend
- 17
+ 21
3.5.8
@@ -135,6 +135,13 @@
-parameters
+
+
+ org.projectlombok
+ lombok
+ 1.18.32
+
+
diff --git a/src/main/java/com/gameplatform/server/device/Detection.java b/src/main/java/com/gameplatform/server/device/Detection.java
new file mode 100644
index 0000000..36cbcca
--- /dev/null
+++ b/src/main/java/com/gameplatform/server/device/Detection.java
@@ -0,0 +1,209 @@
+package com.gameplatform.server.device;
+
+import com.gameplatform.server.model.dto.device.DeviceStatusResponse;
+import com.gameplatform.server.service.external.ScriptClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 检测模块:封装与脚本端交互的设备状态读取能力。
+ * - 提供全量快照与单设备状态两种接口
+ * - 同时提供异步(Mono)与阻塞(block)调用
+ * - 内置短TTL缓存,降低高并发下对脚本端的压力
+ */
+@Service
+public class Detection {
+ private static final Logger log = LoggerFactory.getLogger(Detection.class);
+
+ private final ScriptClient scriptClient;
+ private final DeviceStats deviceStats;
+
+ // 缓存配置(可通过 application.yml 覆盖)
+ private final long listAllCacheTtlMs;
+ private final long readOneCacheTtlMs;
+
+ // 全量快照缓存
+ private volatile DeviceStatusResponse lastSnapshot;
+ private volatile long lastSnapshotAtMs = 0L;
+
+ // 单设备缓存
+ private final ConcurrentHashMap deviceCache = new ConcurrentHashMap<>();
+
+ public Detection(
+ ScriptClient scriptClient,
+ DeviceStats deviceStats,
+ @Value("${detection.listAll.cacheTtlMs:500}") long listAllCacheTtlMs,
+ @Value("${detection.readOne.cacheTtlMs:200}") long readOneCacheTtlMs
+ ) {
+ this.scriptClient = scriptClient;
+ this.deviceStats = deviceStats;
+ this.listAllCacheTtlMs = listAllCacheTtlMs;
+ this.readOneCacheTtlMs = readOneCacheTtlMs;
+ }
+
+ /**
+ * 异步获取全量设备快照(带TTL缓存)。
+ */
+ public Mono listAllDevicesAsync() {
+ DeviceStatusResponse cached = getFreshSnapshot();
+ if (cached != null) {
+ if (log.isDebugEnabled()) {
+ log.debug("listAllDevicesAsync use cache: availableCount={}, total={}",
+ cached.getAvailableCount(), cached.getTotalDevices());
+ }
+ return Mono.just(cached);
+ }
+ return scriptClient.checkAvailableDeviceStatus()
+ .doOnSuccess(resp -> {
+ updateSnapshotCache(resp);
+ if (resp != null && log.isDebugEnabled()) {
+ log.debug("listAllDevicesAsync fetched fresh: availableCount={}, total={}",
+ resp.getAvailableCount(), resp.getTotalDevices());
+ }
+ });
+ }
+
+ /**
+ * 阻塞获取全量设备快照(带TTL缓存)。
+ */
+ public DeviceStatusResponse listAllDevices() {
+ long t0 = System.currentTimeMillis();
+ DeviceStatusResponse cached = getFreshSnapshot();
+ if (cached != null) {
+ long elapsed = System.currentTimeMillis() - t0;
+ if (log.isDebugEnabled()) {
+ log.debug("listAllDevices use cache in {} ms: availableCount={}, total={}",
+ elapsed, cached.getAvailableCount(), cached.getTotalDevices());
+ }
+ return cached;
+ }
+
+ long tFetchStart = System.currentTimeMillis();
+ DeviceStatusResponse resp = scriptClient.checkAvailableDeviceStatus().block();
+ long tFetchEnd = System.currentTimeMillis();
+
+ long tCacheStart = tFetchEnd;
+ updateSnapshotCache(resp);
+ long tCacheEnd = System.currentTimeMillis();
+
+ if (resp != null && log.isInfoEnabled()) {
+ long total = tCacheEnd - t0;
+ long fetch = tFetchEnd - tFetchStart;
+ long cacheWrite = tCacheEnd - tCacheStart;
+ log.info("listAllDevices fetched fresh in {} ms (http+parse={} ms, cacheWrite={} ms): availableCount={}, total={}",
+ total, fetch, cacheWrite, resp.getAvailableCount(), resp.getTotalDevices());
+ }
+ return resp;
+ }
+
+ /**
+ * 定时任务:全量拉取并交由 DeviceStats 更新内存分类与审计。
+ * 默认每 30 秒执行一次,可通过配置覆盖:detection.poll.cron 或 detection.poll.fixedDelayMs
+ */
+ @Scheduled(fixedRate = 30000)
+ public void pollAndUpdateDeviceStats() {
+ try {
+ log.info("定时拉取设备快照并更新统计");
+ DeviceStatusResponse snapshot = listAllDevices();
+ if (snapshot != null) {
+ deviceStats.updateWithSnapshot(snapshot);
+ log.info("设备快照更新统计完成");
+ }
+ } catch (Exception e) {
+ log.error("定时拉取设备快照并更新统计失败", e);
+ }
+ }
+
+ /**
+ * 异步读取单个设备状态(包含 f0/f1 等信息,带短TTL缓存)。
+ */
+ public Mono