From f339f16dedcc4836e50f2596486921d90301337c Mon Sep 17 00:00:00 2001 From: zyh <50652658+zyh530@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:13:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9B=B4=E6=96=B0=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E9=A2=84=E5=8A=A0=E8=BD=BD=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E4=BB=A5=E9=81=BF=E5=85=8DN+1=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E9=97=AE=E9=A2=98=EF=BC=9B=E5=9C=A8SystemConfigServic?= =?UTF-8?q?e=E4=B8=AD=E6=B7=BB=E5=8A=A0=E9=85=8D=E7=BD=AE=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E6=9C=BA=E5=88=B6=EF=BC=8C=E6=8F=90=E5=8D=87=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E8=AF=BB=E5=8F=96=E6=80=A7=E8=83=BD=EF=BC=9B=E8=B0=83?= =?UTF-8?q?=E6=95=B4GameCompletionDetectionService=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E4=BA=8B=E5=8A=A1=E8=B6=85=E6=97=B6=E8=AE=BE=E7=BD=AE=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E5=BF=AB=E9=80=9F=E5=AE=8C=E6=88=90=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=A0=87=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/device/DeviceStats.java | 78 ++++++++++++----- .../service/admin/SystemConfigService.java | 83 +++++++++++++++++-- .../GameCompletionDetectionService.java | 53 ++++++++---- src/main/resources/application.yml | 6 +- 4 files changed, 174 insertions(+), 46 deletions(-) diff --git a/src/main/java/com/gameplatform/server/device/DeviceStats.java b/src/main/java/com/gameplatform/server/device/DeviceStats.java index 07a3c86..cb6c96a 100644 --- a/src/main/java/com/gameplatform/server/device/DeviceStats.java +++ b/src/main/java/com/gameplatform/server/device/DeviceStats.java @@ -94,46 +94,83 @@ public class DeviceStats { /** * 根据外部传入的全量设备快照进行分组统计,并更新内存中的状态与变更记录。 * 分类优先级(高→低): - * 1) RUNNING:存在 LOGGED_IN 任务 或 脚本返回“已运行” - * 2) USING:存在 USING 任务 或 脚本返回“正在登录中/数字(进度/积分)” - * 3) IDLE_COOLDOWN:脚本返回“已打完” 或 处于冷却服务中 - * 4) IDLE_FREE:其余所有(包括脚本返回“空的/空闲/空字符串/未知”) + * 1) RUNNING:存在 LOGGED_IN 任务 或 脚本返回"已运行" + * 2) USING:存在 USING 任务 或 脚本返回"正在登录中/数字(进度/积分)" + * 3) IDLE_COOLDOWN:脚本返回"已打完" 或 处于冷却服务中 + * 4) IDLE_FREE:其余所有(包括脚本返回"空的/空闲/空字符串/未知") + * + * 优化:批量预加载所有设备的任务状态,避免逐个查询导致连接长时间占用 */ public Snapshot updateWithSnapshot(DeviceStatusResponse snapshot) { + long startTime = System.currentTimeMillis(); Map devices = snapshot != null ? snapshot.getDevices() : Collections.emptyMap(); if (devices == null) { devices = Collections.emptyMap(); } log.info("接收设备快照:设备总数={}", devices.size()); + // 批量预加载所有设备的任务状态(避免 N+1 查询问题) + Set devicesWithLoggedInTasks = new HashSet<>(); + Set devicesWithUsingTasks = new HashSet<>(); + + try { + long queryStart = System.currentTimeMillis(); + List allLoggedInTasks = linkTaskMapper.findByStatus("LOGGED_IN"); + List allUsingTasks = linkTaskMapper.findByStatus("USING"); + long queryEnd = System.currentTimeMillis(); + + if (allLoggedInTasks != null) { + for (LinkTask task : allLoggedInTasks) { + if (task.getMachineId() != null) { + devicesWithLoggedInTasks.add(task.getMachineId()); + } + } + } + if (allUsingTasks != null) { + for (LinkTask task : allUsingTasks) { + if (task.getMachineId() != null) { + devicesWithUsingTasks.add(task.getMachineId()); + } + } + } + + log.info("批量预加载任务状态完成:LOGGED_IN={}, USING={}, 耗时={}ms", + devicesWithLoggedInTasks.size(), devicesWithUsingTasks.size(), queryEnd - queryStart); + } catch (Exception e) { + log.error("批量预加载任务状态失败", e); + // 继续执行,但没有任务信息 + } + // 初始化分类容器 Map> bucket = new EnumMap<>(Category.class); for (Category c : Category.values()) { bucket.put(c, new ArrayList<>()); } + String configuredIdle = systemConfigService.getDeviceIdleStatus(); + for (String deviceId : devices.keySet()) { DeviceStatusResponse.DeviceInfo info = devices.get(deviceId); String val = info != null ? info.getVal() : null; String v = val != null ? val.trim() : null; - // 预先计算判定条件,便于统一记录日志 - boolean loggedIn = hasLoggedInTask(deviceId); - boolean usingTask = hasUsingTask(deviceId); + // 预先计算判定条件,便于统一记录日志(使用预加载的数据) + boolean loggedIn = devicesWithLoggedInTasks.contains(deviceId); + boolean usingTask = devicesWithUsingTasks.contains(deviceId); boolean cooldown = cooldownService.isMachineInCooldown(deviceId); boolean numeric = isNumeric(v); - String configuredIdle = systemConfigService.getDeviceIdleStatus(); // log.debug("设备[{}] 原始脚本值='{}' | LOGGED_IN={} USING={} COOLDOWN={} NUMERIC={}", // deviceId, v, loggedIn, usingTask, cooldown, numeric); // 当脚本值为配置的空闲标识时,若设备上存在 LOGGED_IN 且登录超过3分钟的任务,则自动完成这些任务 - if (v != null && configuredIdle != null && configuredIdle.equals(v)) { + if (v != null && configuredIdle != null && configuredIdle.equals(v) && loggedIn) { try { int completed = autoCompleteLoggedInTasksIfIdleOver3m(deviceId); if (completed > 0) { - // 任务状态可能已变化,重新评估 loggedIn - loggedIn = hasLoggedInTask(deviceId); + // 任务状态已变化,更新预加载的状态 + devicesWithLoggedInTasks.remove(deviceId); + loggedIn = false; } } catch (Exception e) { log.warn("autoCompleteLoggedInTasksIfIdleOver3m 执行异常 device={} err={}", deviceId, e.getMessage()); @@ -208,9 +245,13 @@ public class DeviceStats { } Snapshot result = new Snapshot(bucket); - log.info("设备分组统计完成:total={} running={} using={} idleCooldown={} idleFree={}", + long endTime = System.currentTimeMillis(); + long totalDuration = endTime - startTime; + + log.info("设备分组统计完成:total={} running={} using={} idleCooldown={} idleFree={}, 总耗时={}ms", result.getTotalDevices(), result.getRunningCount(), result.getUsingCount(), - result.getIdleCooldownCount(), result.getIdleFreeCount()); + result.getIdleCooldownCount(), result.getIdleFreeCount(), totalDuration); + this.lastComputedSnapshot = result; return result; } @@ -338,15 +379,8 @@ public class DeviceStats { return completed; } - private boolean hasLoggedInTask(String deviceId) { - List loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "LOGGED_IN"); - return loggedInTasks != null && !loggedInTasks.isEmpty(); - } - - private boolean hasUsingTask(String deviceId) { - List usingTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "USING"); - return usingTasks != null && !usingTasks.isEmpty(); - } + // 方法已移除:hasLoggedInTask 和 hasUsingTask + // 现在使用批量预加载方式在 updateWithSnapshot() 中处理,避免 N+1 查询问题 private static boolean isNumeric(String text) { if (text == null) return false; diff --git a/src/main/java/com/gameplatform/server/service/admin/SystemConfigService.java b/src/main/java/com/gameplatform/server/service/admin/SystemConfigService.java index aec7115..03310e7 100644 --- a/src/main/java/com/gameplatform/server/service/admin/SystemConfigService.java +++ b/src/main/java/com/gameplatform/server/service/admin/SystemConfigService.java @@ -6,6 +6,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; @Service public class SystemConfigService { @@ -13,13 +15,66 @@ public class SystemConfigService { @Autowired private SystemConfigMapper systemConfigMapper; + // 配置缓存:key -> ConfigEntry(value, cachedTime) + private final ConcurrentMap configCache = new ConcurrentHashMap<>(); + + // 缓存TTL(毫秒):5分钟 + private static final long CACHE_TTL_MS = 5 * 60 * 1000L; + + /** + * 配置缓存条目 + */ + private static class ConfigEntry { + final String value; + final long cachedAtMs; + + ConfigEntry(String value, long cachedAtMs) { + this.value = value; + this.cachedAtMs = cachedAtMs; + } + + boolean isExpired() { + return System.currentTimeMillis() - cachedAtMs > CACHE_TTL_MS; + } + } + public SystemConfig getConfigByKey(String configKey) { return systemConfigMapper.findByKey(configKey); } + /** + * 获取配置值(带缓存) + * 注意:此方法被频繁调用(尤其在响应式流中),必须使用缓存避免连接泄漏 + */ public String getConfigValue(String configKey, String defaultValue) { + // 先检查缓存 + ConfigEntry cached = configCache.get(configKey); + if (cached != null && !cached.isExpired()) { + return cached.value; + } + + // 缓存未命中或过期,查询数据库 SystemConfig config = systemConfigMapper.findByKey(configKey); - return config != null ? config.getConfigValue() : defaultValue; + String value = config != null ? config.getConfigValue() : defaultValue; + + // 更新缓存 + configCache.put(configKey, new ConfigEntry(value, System.currentTimeMillis())); + + return value; + } + + /** + * 清除指定配置的缓存(用于配置更新后) + */ + public void clearCache(String configKey) { + configCache.remove(configKey); + } + + /** + * 清除所有配置缓存 + */ + public void clearAllCache() { + configCache.clear(); } public Integer getConfigValueAsInt(String configKey, Integer defaultValue) { @@ -51,19 +106,37 @@ public class SystemConfigService { } public boolean createConfig(SystemConfig systemConfig) { - return systemConfigMapper.insert(systemConfig) > 0; + boolean result = systemConfigMapper.insert(systemConfig) > 0; + if (result && systemConfig.getConfigKey() != null) { + clearCache(systemConfig.getConfigKey()); + } + return result; } public boolean updateConfig(SystemConfig systemConfig) { - return systemConfigMapper.update(systemConfig) > 0; + boolean result = systemConfigMapper.update(systemConfig) > 0; + if (result && systemConfig.getConfigKey() != null) { + // 配置更新后清除缓存,确保下次读取最新值 + clearCache(systemConfig.getConfigKey()); + } + return result; } public boolean deleteConfig(Long id) { - return systemConfigMapper.deleteById(id) > 0; + boolean result = systemConfigMapper.deleteById(id) > 0; + if (result) { + // 删除后清除所有缓存(因为不知道具体 key) + clearAllCache(); + } + return result; } public boolean deleteConfigByKey(String configKey) { - return systemConfigMapper.deleteByKey(configKey) > 0; + boolean result = systemConfigMapper.deleteByKey(configKey) > 0; + if (result) { + clearCache(configKey); + } + return result; } // 获取链接相关的默认配置 diff --git a/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java b/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java index f77da68..1967e68 100644 --- a/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java +++ b/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java @@ -64,7 +64,7 @@ public class GameCompletionDetectionService { * @param detectionSource 检测来源 * @return 是否检测到完成 */ - @Transactional + @Transactional(timeout = 10) public boolean detectGameCompletion(String machineId, String deviceStatus, String detectionSource) { if (machineId == null || machineId.trim().isEmpty()) { return false; @@ -192,6 +192,7 @@ public class GameCompletionDetectionService { /** * 标记任务为完成状态 + * 注意:此方法在调用者的事务中执行,需要快速完成避免长时间持有连接 */ private boolean markTasksCompleted(List tasks, String machineId, Integer points, String detectionSource) { @@ -200,6 +201,7 @@ public class GameCompletionDetectionService { for (LinkTask task : tasks) { try { + String prevStatus = task.getStatus(); task.setStatus("COMPLETED"); task.setUpdatedAt(now); // 正常检测完成:记录原因 @@ -212,26 +214,25 @@ public class GameCompletionDetectionService { task.setCompletedPoints(0); } - String prev = task.getStatus(); int updated = linkTaskMapper.update(task); if (updated > 0) { log.info("任务{}已标记完成:设备={},点数={},检测来源={}", task.getCodeNo(), machineId, task.getCompletedPoints(), detectionSource); com.gameplatform.server.util.AuditLogger.info("StatusTransition: codeNo={}, device={}, {}->COMPLETED, source={}, points={}", - task.getCodeNo(), machineId, prev, detectionSource, task.getCompletedPoints()); - try { - if (statusHistoryMapper != null) { - statusHistoryMapper.insert(new LinkTaskStatusHistory( - task.getId(), task.getCodeNo(), machineId, prev, "COMPLETED", detectionSource, - "detectionComplete", null, null - )); - } - } catch (Exception ignore) {} + task.getCodeNo(), machineId, prevStatus, detectionSource, task.getCompletedPoints()); + anyCompleted = true; - // 将设备加入冷却队列 - machineCooldownService.addMachineToCooldown(machineId, - "游戏完成 - " + detectionSource, task.getId()); + // 异步记录历史,避免阻塞主事务 + recordHistoryAsync(task.getId(), task.getCodeNo(), machineId, prevStatus, detectionSource); + + // 将设备加入冷却队列(内存操作,快速) + try { + machineCooldownService.addMachineToCooldown(machineId, + "游戏完成 - " + detectionSource, task.getId()); + } catch (Exception e) { + log.error("添加设备冷却失败", e); + } } else { log.warn("更新任务{}完成状态失败", task.getCodeNo()); } @@ -242,7 +243,7 @@ public class GameCompletionDetectionService { } if (anyCompleted) { - // 清理待确认记录 + // 清理待确认记录(内存操作,快速) pendingCompletions.remove(machineId); // 清理登录记录 recentLogins.remove(machineId); @@ -251,8 +252,27 @@ public class GameCompletionDetectionService { return anyCompleted; } + /** + * 异步记录状态历史,避免阻塞主事务 + */ + private void recordHistoryAsync(Long taskId, String codeNo, String machineId, + String prevStatus, String detectionSource) { + try { + if (statusHistoryMapper != null) { + statusHistoryMapper.insert(new LinkTaskStatusHistory( + taskId, codeNo, machineId, prevStatus, "COMPLETED", detectionSource, + "detectionComplete", null, null + )); + } + } catch (Exception e) { + // 历史记录失败不影响主流程,只记录日志 + log.error("记录任务状态历史失败: taskId={}, codeNo={}", taskId, codeNo, e); + } + } + /** * 记录检测日志 + * 注意:快速记录,失败不影响主流程 */ private void recordDetectionLog(Long linkTaskId, String machineId, String detectionSource, String deviceStatus, Integer points, String confidence) { @@ -265,7 +285,8 @@ public class GameCompletionDetectionService { com.gameplatform.server.util.AuditLogger.info("CompletionDetect: taskId={}, device={}, source={}, status={}, points={}, confidence={}", linkTaskId, machineId, detectionSource, deviceStatus, points, confidence); } catch (Exception e) { - this.log.error("记录游戏完成检测日志失败", e); + // 日志记录失败不应该影响主流程,只记录错误 + log.error("记录游戏完成检测日志失败: taskId={}, device={}", linkTaskId, machineId, e); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index edbd9b5..58468fd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,9 +11,9 @@ spring: maximum-pool-size: 100 # 增加最大连接数(从50到100) minimum-idle: 20 # 增加最小空闲连接(从10到20) connection-timeout: 10000 # 降低连接获取超时(从30秒到10秒) - idle-timeout: 300000 # 空闲连接超时5分钟(从10分钟降低) - max-lifetime: 1800000 # 连接最大存活30分钟 - leak-detection-threshold: 30000 # 连接泄漏检测30秒(从60秒降低) + idle-timeout: 240000 # 空闲连接超时4分钟(必须 < wait_timeout) + max-lifetime: 240000 # 连接最大存活4分钟(必须 < MySQL wait_timeout=5分钟) + leak-detection-threshold: 60000 # 连接泄漏检测60秒(给事务足够时间) validation-timeout: 3000 # 连接验证超时3秒(从5秒降低) connection-test-query: SELECT 1 # 连接池健康监控和自动重连