feat: 更新项目配置,升级Java版本至21,添加Lombok注解处理器,优化设备状态解析和任务更新逻辑
This commit is contained in:
8
.idea/compiler.xml
generated
8
.idea/compiler.xml
generated
@@ -7,6 +7,14 @@
|
|||||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||||
<outputRelativeToContentRoot value="true" />
|
<outputRelativeToContentRoot value="true" />
|
||||||
|
</profile>
|
||||||
|
<profile name="Annotation profile for gameplatform-server" enabled="true">
|
||||||
|
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||||
|
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||||
|
<outputRelativeToContentRoot value="true" />
|
||||||
|
<processorPath useClasspath="false">
|
||||||
|
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.32/lombok-1.18.32.jar" />
|
||||||
|
</processorPath>
|
||||||
<module name="server" />
|
<module name="server" />
|
||||||
</profile>
|
</profile>
|
||||||
</annotationProcessing>
|
</annotationProcessing>
|
||||||
|
|||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -8,7 +8,7 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="graalvm-jdk-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="graalvm-jdk-21 (2)" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
43
device_cleanup_test.sql
Normal file
43
device_cleanup_test.sql
Normal file
@@ -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);
|
||||||
9
pom.xml
9
pom.xml
@@ -16,7 +16,7 @@
|
|||||||
<description>Spring Boot WebFlux + MyBatis + MySQL backend</description>
|
<description>Spring Boot WebFlux + MyBatis + MySQL backend</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>17</java.version>
|
<java.version>21</java.version>
|
||||||
<mybatis-plus.version>3.5.8</mybatis-plus.version>
|
<mybatis-plus.version>3.5.8</mybatis-plus.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
@@ -135,6 +135,13 @@
|
|||||||
<compilerArgs>
|
<compilerArgs>
|
||||||
<arg>-parameters</arg>
|
<arg>-parameters</arg>
|
||||||
</compilerArgs>
|
</compilerArgs>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.32</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
|
|||||||
209
src/main/java/com/gameplatform/server/device/Detection.java
Normal file
209
src/main/java/com/gameplatform/server/device/Detection.java
Normal file
@@ -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<String, DeviceEntry> 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<DeviceStatusResponse> 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<Map<String, Object>> readDeviceAsync(String machineId) {
|
||||||
|
Map<String, Object> cached = getFreshDevice(machineId);
|
||||||
|
if (cached != null) {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("readDeviceAsync use cache: machineId={}", machineId);
|
||||||
|
}
|
||||||
|
return Mono.just(cached);
|
||||||
|
}
|
||||||
|
return scriptClient.getDeviceStatus(machineId)
|
||||||
|
.doOnSuccess(map -> {
|
||||||
|
updateDeviceCache(machineId, map);
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("readDeviceAsync fetched fresh: machineId={}", machineId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阻塞读取单个设备状态(包含 f0/f1 等信息,带短TTL缓存)。
|
||||||
|
*/
|
||||||
|
public Map<String, Object> readDevice(String machineId) {
|
||||||
|
Map<String, Object> cached = getFreshDevice(machineId);
|
||||||
|
if (cached != null) {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("readDevice use cache: machineId={}", machineId);
|
||||||
|
}
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
Map<String, Object> map = scriptClient.getDeviceStatus(machineId).block();
|
||||||
|
updateDeviceCache(machineId, map);
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("readDevice fetched fresh: machineId={}", machineId);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- 内部:缓存管理 ----------------------
|
||||||
|
|
||||||
|
private DeviceStatusResponse getFreshSnapshot() {
|
||||||
|
DeviceStatusResponse snapshot = this.lastSnapshot;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (snapshot != null && (now - lastSnapshotAtMs) < listAllCacheTtlMs) {
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSnapshotCache(DeviceStatusResponse snapshot) {
|
||||||
|
if (snapshot == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastSnapshot = snapshot;
|
||||||
|
this.lastSnapshotAtMs = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> getFreshDevice(String machineId) {
|
||||||
|
DeviceEntry entry = deviceCache.get(machineId);
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if ((now - entry.cachedAtMs) < readOneCacheTtlMs) {
|
||||||
|
return entry.status;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDeviceCache(String machineId, Map<String, Object> status) {
|
||||||
|
if (machineId == null || status == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deviceCache.put(machineId, new DeviceEntry(status, System.currentTimeMillis()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DeviceEntry {
|
||||||
|
final Map<String, Object> status;
|
||||||
|
final long cachedAtMs;
|
||||||
|
DeviceEntry(Map<String, Object> status, long cachedAtMs) {
|
||||||
|
this.status = status;
|
||||||
|
this.cachedAtMs = cachedAtMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
304
src/main/java/com/gameplatform/server/device/DeviceStats.java
Normal file
304
src/main/java/com/gameplatform/server/device/DeviceStats.java
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
package com.gameplatform.server.device;
|
||||||
|
|
||||||
|
import com.gameplatform.server.model.dto.device.DeviceStatusResponse;
|
||||||
|
import com.gameplatform.server.model.entity.agent.LinkTask;
|
||||||
|
import com.gameplatform.server.model.entity.history.DeviceStatusTransition;
|
||||||
|
import com.gameplatform.server.service.cooldown.MemoryMachineCooldownService;
|
||||||
|
import com.gameplatform.server.service.link.DeviceAllocationService;
|
||||||
|
import com.gameplatform.server.mapper.agent.LinkTaskMapper;
|
||||||
|
import com.gameplatform.server.mapper.history.DeviceStatusTransitionMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备状态分组统计:
|
||||||
|
* - RUNNING:已上号(LOGGED_IN 任务或脚本返回“已运行”)
|
||||||
|
* - USING:登录中/选区中(USING 任务或脚本返回数字/“正在登录中”)
|
||||||
|
* - IDLE_COOLDOWN:处于冷却或脚本返回“已打完”(刚完成)
|
||||||
|
* - IDLE_FREE:脚本返回“空的/空闲”或其他均视为空闲可用
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class DeviceStats {
|
||||||
|
// 设备分组统计日志记录器
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DeviceStats.class);
|
||||||
|
// 审计专用日志(输出到独立文件)
|
||||||
|
private static final Logger auditLog = LoggerFactory.getLogger("com.gameplatform.server.audit");
|
||||||
|
|
||||||
|
public enum Category {
|
||||||
|
RUNNING,
|
||||||
|
USING,
|
||||||
|
IDLE_COOLDOWN,
|
||||||
|
IDLE_FREE
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Snapshot {
|
||||||
|
private final Map<Category, List<String>> categoryToDevices;
|
||||||
|
private final int totalDevices;
|
||||||
|
private final int runningCount;
|
||||||
|
private final int usingCount;
|
||||||
|
private final int idleCooldownCount;
|
||||||
|
private final int idleFreeCount;
|
||||||
|
|
||||||
|
public Snapshot(Map<Category, List<String>> categoryToDevices) {
|
||||||
|
this.categoryToDevices = categoryToDevices;
|
||||||
|
this.totalDevices = categoryToDevices.values().stream().mapToInt(List::size).sum();
|
||||||
|
this.runningCount = categoryToDevices.getOrDefault(Category.RUNNING, List.of()).size();
|
||||||
|
this.usingCount = categoryToDevices.getOrDefault(Category.USING, List.of()).size();
|
||||||
|
this.idleCooldownCount = categoryToDevices.getOrDefault(Category.IDLE_COOLDOWN, List.of()).size();
|
||||||
|
this.idleFreeCount = categoryToDevices.getOrDefault(Category.IDLE_FREE, List.of()).size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Category, List<String>> getCategoryToDevices() { return categoryToDevices; }
|
||||||
|
public int getTotalDevices() { return totalDevices; }
|
||||||
|
public int getRunningCount() { return runningCount; }
|
||||||
|
public int getUsingCount() { return usingCount; }
|
||||||
|
public int getIdleCooldownCount() { return idleCooldownCount; }
|
||||||
|
public int getIdleFreeCount() { return idleFreeCount; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private final LinkTaskMapper linkTaskMapper;
|
||||||
|
private final MemoryMachineCooldownService cooldownService;
|
||||||
|
private final DeviceStatusTransitionMapper transitionMapper;
|
||||||
|
private final DeviceAllocationService deviceAllocationService;
|
||||||
|
|
||||||
|
// 记录上一次统计时每台设备的分类结果,用于检测状态变更
|
||||||
|
private final Map<String, Category> lastStatusByDevice = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public DeviceStats(LinkTaskMapper linkTaskMapper,
|
||||||
|
MemoryMachineCooldownService cooldownService,
|
||||||
|
DeviceStatusTransitionMapper transitionMapper,
|
||||||
|
DeviceAllocationService deviceAllocationService) {
|
||||||
|
this.linkTaskMapper = linkTaskMapper;
|
||||||
|
this.cooldownService = cooldownService;
|
||||||
|
this.transitionMapper = transitionMapper;
|
||||||
|
this.deviceAllocationService = deviceAllocationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据外部传入的全量设备快照进行分组统计,并更新内存中的状态与变更记录。
|
||||||
|
* 分类优先级(高→低):
|
||||||
|
* 1) RUNNING:存在 LOGGED_IN 任务 或 脚本返回“已运行”
|
||||||
|
* 2) USING:存在 USING 任务 或 脚本返回“正在登录中/数字(进度/积分)”
|
||||||
|
* 3) IDLE_COOLDOWN:脚本返回“已打完” 或 处于冷却服务中
|
||||||
|
* 4) IDLE_FREE:其余所有(包括脚本返回“空的/空闲/空字符串/未知”)
|
||||||
|
*/
|
||||||
|
public Snapshot updateWithSnapshot(DeviceStatusResponse snapshot) {
|
||||||
|
Map<String, DeviceStatusResponse.DeviceInfo> devices = snapshot != null ? snapshot.getDevices() : Collections.emptyMap();
|
||||||
|
if (devices == null) {
|
||||||
|
devices = Collections.emptyMap();
|
||||||
|
}
|
||||||
|
log.info("接收设备快照:设备总数={}", devices.size());
|
||||||
|
|
||||||
|
// 初始化分类容器
|
||||||
|
Map<Category, List<String>> bucket = new EnumMap<>(Category.class);
|
||||||
|
for (Category c : Category.values()) {
|
||||||
|
bucket.put(c, new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
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 cooldown = cooldownService.isMachineInCooldown(deviceId);
|
||||||
|
boolean numeric = isNumeric(v);
|
||||||
|
|
||||||
|
// log.debug("设备[{}] 原始脚本值='{}' | LOGGED_IN={} USING={} COOLDOWN={} NUMERIC={}",
|
||||||
|
// deviceId, v, loggedIn, usingTask, cooldown, numeric);
|
||||||
|
|
||||||
|
// 分类与原因
|
||||||
|
Category newCategory;
|
||||||
|
String reason;
|
||||||
|
if (loggedIn || "已运行".equals(v)) {
|
||||||
|
newCategory = Category.RUNNING;
|
||||||
|
reason = loggedIn ? "存在LOGGED_IN任务" : "脚本值=已运行";
|
||||||
|
} else if (usingTask || "正在登录中".equals(v) || numeric) {
|
||||||
|
newCategory = Category.USING;
|
||||||
|
reason = usingTask ? "存在USING任务" : ("正在登录中".equals(v) ? "脚本值=正在登录中" : "脚本值为数字");
|
||||||
|
} else if ("已打完".equals(v) || cooldown) {
|
||||||
|
newCategory = Category.IDLE_COOLDOWN;
|
||||||
|
reason = "已打完".equals(v) ? "脚本值=已打完" : "处于冷却服务中";
|
||||||
|
} else {
|
||||||
|
newCategory = Category.RUNNING;
|
||||||
|
reason = "默认运行中/未知状态";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 放入桶
|
||||||
|
bucket.get(newCategory).add(deviceId);
|
||||||
|
// log.debug("设备[{}] 归类为 {},原因={}", deviceId, newCategory, reason);
|
||||||
|
|
||||||
|
// 检测并记录状态变更
|
||||||
|
Category prevCategory = lastStatusByDevice.get(deviceId);
|
||||||
|
boolean changed = prevCategory != null && prevCategory != newCategory;
|
||||||
|
if (changed) {
|
||||||
|
LocalDateTime occurredAt = parseTimeOrNow(info != null ? info.getTime() : null);
|
||||||
|
String series = info != null ? info.getSeries() : null;
|
||||||
|
Integer indexNo = info != null ? info.getIndex() : null;
|
||||||
|
|
||||||
|
// 审计日志(独立文件)
|
||||||
|
auditLog.info("device={} prev={} next={} reason={} val='{}' at={}",
|
||||||
|
deviceId,
|
||||||
|
prevCategory != null ? prevCategory.name() : null,
|
||||||
|
newCategory.name(),
|
||||||
|
reason,
|
||||||
|
v,
|
||||||
|
occurredAt);
|
||||||
|
|
||||||
|
// 入库(仅状态变化时)
|
||||||
|
DeviceStatusTransition t = new DeviceStatusTransition(
|
||||||
|
deviceId,
|
||||||
|
prevCategory != null ? prevCategory.name() : null,
|
||||||
|
newCategory.name(),
|
||||||
|
reason,
|
||||||
|
v,
|
||||||
|
series,
|
||||||
|
indexNo,
|
||||||
|
occurredAt
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
transitionMapper.insert(t);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("写入设备状态变更失败 device={} prev={} next={} err={}",
|
||||||
|
deviceId,
|
||||||
|
prevCategory != null ? prevCategory.name() : null,
|
||||||
|
newCategory.name(),
|
||||||
|
e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无论是否变化,都更新内存快照为最新分类
|
||||||
|
lastStatusByDevice.put(deviceId, newCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
Snapshot result = new Snapshot(bucket);
|
||||||
|
log.info("设备分组统计完成:total={} running={} using={} idleCooldown={} idleFree={}",
|
||||||
|
result.getTotalDevices(), result.getRunningCount(), result.getUsingCount(),
|
||||||
|
result.getIdleCooldownCount(), result.getIdleFreeCount());
|
||||||
|
this.lastComputedSnapshot = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最近一次分组统计结果(供查询展示)
|
||||||
|
private volatile Snapshot lastComputedSnapshot;
|
||||||
|
|
||||||
|
public Snapshot getLastComputedSnapshot() {
|
||||||
|
return lastComputedSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于传入的快照进行设备分配,并立即将该设备的分组状态切换为 USING(内存与审计)。
|
||||||
|
* 注意:数据库中 LinkTask 的状态更新与原子占用仍由上层调用完成(reserveDeviceIfFree)。
|
||||||
|
* 这里仅完成:
|
||||||
|
* 1) 使用 DeviceAllocationService 基于快照可用列表进行原子分配(冷却+并发安全)
|
||||||
|
* 2) 如分配成功,在本地分类快照中将该设备标记为 USING,并记录 DeviceStatusTransition 审计
|
||||||
|
*
|
||||||
|
* @param snapshot 全量设备快照
|
||||||
|
* @param linkTaskId 链接任务ID
|
||||||
|
* @param reason 分配原因(审计用途)
|
||||||
|
* @return 分配成功的设备ID;若无可分配设备则返回 null
|
||||||
|
*/
|
||||||
|
public String allocateFromSnapshot(DeviceStatusResponse snapshot, Long linkTaskId, String reason) {
|
||||||
|
if (snapshot == null || snapshot.getAvailableDevices() == null || snapshot.getAvailableDevices().isEmpty()) {
|
||||||
|
log.warn("allocateFromSnapshot: 无可用设备,跳过分配");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过分配服务进行原子分配(含冷却与并发控制)
|
||||||
|
String selectedDeviceId = deviceAllocationService.allocateDevice(snapshot.getAvailableDevices(), linkTaskId, reason);
|
||||||
|
if (selectedDeviceId == null) {
|
||||||
|
log.warn("allocateFromSnapshot: 原子分配失败(可能在冷却或被占用),linkTaskId={}", linkTaskId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审计与内存分类:将该设备在本地快照中标记为 USING
|
||||||
|
try {
|
||||||
|
DeviceStatusResponse.DeviceInfo info = null;
|
||||||
|
if (snapshot.getDevices() != null) {
|
||||||
|
info = snapshot.getDevices().get(selectedDeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Category prevCategory = lastStatusByDevice.get(selectedDeviceId);
|
||||||
|
Category newCategory = Category.USING;
|
||||||
|
boolean changed = prevCategory == null || prevCategory != newCategory;
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
LocalDateTime occurredAt = parseTimeOrNow(info != null ? info.getTime() : null);
|
||||||
|
String series = info != null ? info.getSeries() : null;
|
||||||
|
Integer indexNo = info != null ? info.getIndex() : null;
|
||||||
|
String snapshotVal = info != null ? info.getVal() : null;
|
||||||
|
|
||||||
|
// 写入设备状态变更历史
|
||||||
|
DeviceStatusTransition t = new DeviceStatusTransition(
|
||||||
|
selectedDeviceId,
|
||||||
|
prevCategory != null ? prevCategory.name() : null,
|
||||||
|
newCategory.name(),
|
||||||
|
reason != null ? ("ALLOCATE:" + reason) : "ALLOCATE",
|
||||||
|
snapshotVal,
|
||||||
|
series,
|
||||||
|
indexNo,
|
||||||
|
occurredAt
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
transitionMapper.insert(t);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("写入设备状态变更(ALLOCATE)失败 device={} prev={} next={} err={}",
|
||||||
|
selectedDeviceId,
|
||||||
|
prevCategory != null ? prevCategory.name() : null,
|
||||||
|
newCategory.name(),
|
||||||
|
e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastStatusByDevice.put(selectedDeviceId, Category.USING);
|
||||||
|
log.info("allocateFromSnapshot: 设备分配并标记USING完成 device={} linkTaskId={}", selectedDeviceId, linkTaskId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("allocateFromSnapshot: 标记内存分类为USING时发生异常 device={} err={}", selectedDeviceId, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasLoggedInTask(String deviceId) {
|
||||||
|
List<LinkTask> loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "LOGGED_IN");
|
||||||
|
return loggedInTasks != null && !loggedInTasks.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasUsingTask(String deviceId) {
|
||||||
|
List<LinkTask> usingTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "USING");
|
||||||
|
return usingTasks != null && !usingTasks.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isNumeric(String text) {
|
||||||
|
if (text == null) return false;
|
||||||
|
int len = text.length();
|
||||||
|
if (len == 0) return false;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
char c = text.charAt(i);
|
||||||
|
if (c < '0' || c > '9') return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalDateTime parseTimeOrNow(String timeStr) {
|
||||||
|
if (timeStr == null || timeStr.isEmpty()) {
|
||||||
|
return LocalDateTime.now();
|
||||||
|
}
|
||||||
|
// 常见格式:yyyy-MM-dd HH:mm:ss
|
||||||
|
try {
|
||||||
|
return LocalDateTime.parse(timeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
// 回退当前时间
|
||||||
|
return LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,12 @@ public interface LinkTaskMapper extends BaseMapper<LinkTask> {
|
|||||||
@Param("machineId") String machineId,
|
@Param("machineId") String machineId,
|
||||||
@Param("loginAt") LocalDateTime loginAt);
|
@Param("loginAt") LocalDateTime loginAt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅当新的点数更大时才更新 completed_points,避免不必要写锁
|
||||||
|
*/
|
||||||
|
int updatePointsIfGreater(@Param("id") Long id,
|
||||||
|
@Param("newPoints") Integer newPoints);
|
||||||
|
|
||||||
List<LinkTask> findByAgentId(@Param("agentId") Long agentId,
|
List<LinkTask> findByAgentId(@Param("agentId") Long agentId,
|
||||||
@Param("size") int size,
|
@Param("size") int size,
|
||||||
@Param("offset") int offset);
|
@Param("offset") int offset);
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.gameplatform.server.mapper.history;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.gameplatform.server.model.entity.history.DeviceStatusTransition;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface DeviceStatusTransitionMapper extends BaseMapper<DeviceStatusTransition> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定设备的最近状态变更记录
|
||||||
|
*/
|
||||||
|
List<DeviceStatusTransition> findRecentByDeviceId(@Param("deviceId") String deviceId, @Param("size") int size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除24小时前的历史记录
|
||||||
|
* @return 删除的记录数
|
||||||
|
*/
|
||||||
|
int deleteOldRecords();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.gameplatform.server.model.entity.history;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@TableName("device_status_transition")
|
||||||
|
public class DeviceStatusTransition {
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String deviceId;
|
||||||
|
private String prevStatus; // 例如 RUNNING/USING/IDLE_COOLDOWN/IDLE_FREE
|
||||||
|
private String newStatus; // 同上
|
||||||
|
private String reason; // 变更原因(来源数据)
|
||||||
|
private String snapshotVal; // 当次脚本原始值
|
||||||
|
private String series; // 设备系列
|
||||||
|
private Integer indexNo; // 设备序号
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime occurredAt; // 发生时间
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime createdAt; // 记录创建时间
|
||||||
|
|
||||||
|
public DeviceStatusTransition() {}
|
||||||
|
|
||||||
|
public DeviceStatusTransition(String deviceId, String prevStatus, String newStatus, String reason,
|
||||||
|
String snapshotVal, String series, Integer indexNo,
|
||||||
|
LocalDateTime occurredAt) {
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.prevStatus = prevStatus;
|
||||||
|
this.newStatus = newStatus;
|
||||||
|
this.reason = reason;
|
||||||
|
this.snapshotVal = snapshotVal;
|
||||||
|
this.series = series;
|
||||||
|
this.indexNo = indexNo;
|
||||||
|
this.occurredAt = occurredAt;
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
public String getDeviceId() { return deviceId; }
|
||||||
|
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
|
||||||
|
public String getPrevStatus() { return prevStatus; }
|
||||||
|
public void setPrevStatus(String prevStatus) { this.prevStatus = prevStatus; }
|
||||||
|
public String getNewStatus() { return newStatus; }
|
||||||
|
public void setNewStatus(String newStatus) { this.newStatus = newStatus; }
|
||||||
|
public String getReason() { return reason; }
|
||||||
|
public void setReason(String reason) { this.reason = reason; }
|
||||||
|
public String getSnapshotVal() { return snapshotVal; }
|
||||||
|
public void setSnapshotVal(String snapshotVal) { this.snapshotVal = snapshotVal; }
|
||||||
|
public String getSeries() { return series; }
|
||||||
|
public void setSeries(String series) { this.series = series; }
|
||||||
|
public Integer getIndexNo() { return indexNo; }
|
||||||
|
public void setIndexNo(Integer indexNo) { this.indexNo = indexNo; }
|
||||||
|
public LocalDateTime getOccurredAt() { return occurredAt; }
|
||||||
|
public void setOccurredAt(LocalDateTime occurredAt) { this.occurredAt = occurredAt; }
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ public class GameCompletionDetectionService {
|
|||||||
// 查找该设备上的 LOGGED_IN 状态任务
|
// 查找该设备上的 LOGGED_IN 状态任务
|
||||||
List<LinkTask> loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(machineId, "LOGGED_IN");
|
List<LinkTask> loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(machineId, "LOGGED_IN");
|
||||||
if (loggedInTasks.isEmpty()) {
|
if (loggedInTasks.isEmpty()) {
|
||||||
log.debug("设备{}没有LOGGED_IN状态的任务,跳过完成检测", machineId);
|
// log.debug("设备{}没有LOGGED_IN状态的任务,跳过完成检测", machineId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 设备状态服务
|
* 设备状态服务
|
||||||
|
*
|
||||||
|
* 负责:
|
||||||
|
* - 解析设备状态 JSON 字符串为结构化对象
|
||||||
|
* - 识别空闲设备(支持配置项定义空闲值)
|
||||||
|
* - 提供针对单机的 f0/f1 状态解析辅助方法
|
||||||
|
* - 提供按系列分组和筛选空闲设备的便捷方法
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class DeviceStatusService {
|
public class DeviceStatusService {
|
||||||
@@ -33,24 +39,32 @@ public class DeviceStatusService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析设备状态JSON响应
|
* 解析设备状态 JSON 响应。
|
||||||
|
*
|
||||||
|
* 输入 JSON 示例(简化):
|
||||||
|
* {
|
||||||
|
* "f1": { "val": "已运行", "time": "2025-09-15 10:00:00" },
|
||||||
|
* "f2": { "val": "正在登录中", "time": "2025-09-15 10:00:02" },
|
||||||
|
* "s1": { "val": "123", "time": "2025-09-15 10:01:00" }
|
||||||
|
* }
|
||||||
|
* 其中 val 可能取值:数字(积分/进度)、"正在登录中"、"已运行"、"已打完"、"空的"、"空闲" 等。
|
||||||
*/
|
*/
|
||||||
public DeviceStatusResponse parseDeviceStatus(String jsonResponse) {
|
public DeviceStatusResponse parseDeviceStatus(String jsonResponse) {
|
||||||
try {
|
try {
|
||||||
log.debug("开始解析设备状态响应");
|
log.debug("开始解析设备状态响应,长度={} 字符", jsonResponse != null ? jsonResponse.length() : 0);
|
||||||
|
|
||||||
JsonNode rootNode = objectMapper.readTree(jsonResponse);
|
JsonNode rootNode = objectMapper.readTree(jsonResponse);
|
||||||
Map<String, DeviceStatusResponse.DeviceInfo> devices = new HashMap<>();
|
Map<String, DeviceStatusResponse.DeviceInfo> devices = new HashMap<>();
|
||||||
List<String> availableDevices = new ArrayList<>();
|
List<String> availableDevices = new ArrayList<>();
|
||||||
|
|
||||||
// 遍历所有设备
|
// 遍历所有设备节点,逐个解析
|
||||||
Iterator<Map.Entry<String, JsonNode>> fields = rootNode.fields();
|
Iterator<Map.Entry<String, JsonNode>> fields = rootNode.fields();
|
||||||
while (fields.hasNext()) {
|
while (fields.hasNext()) {
|
||||||
Map.Entry<String, JsonNode> field = fields.next();
|
Map.Entry<String, JsonNode> field = fields.next();
|
||||||
String deviceId = field.getKey();
|
String deviceId = field.getKey();
|
||||||
JsonNode deviceNode = field.getValue();
|
JsonNode deviceNode = field.getValue();
|
||||||
|
|
||||||
// 解析设备信息
|
// 解析设备信息(含系列、序号、是否空闲)
|
||||||
DeviceStatusResponse.DeviceInfo deviceInfo = parseDeviceInfo(deviceId, deviceNode);
|
DeviceStatusResponse.DeviceInfo deviceInfo = parseDeviceInfo(deviceId, deviceNode);
|
||||||
devices.put(deviceId, deviceInfo);
|
devices.put(deviceId, deviceInfo);
|
||||||
|
|
||||||
@@ -67,7 +81,7 @@ public class DeviceStatusService {
|
|||||||
response.setTotalDevices(devices.size());
|
response.setTotalDevices(devices.size());
|
||||||
response.setAvailableCount(availableDevices.size());
|
response.setAvailableCount(availableDevices.size());
|
||||||
|
|
||||||
log.info("设备状态解析完成: 总设备数={}, 空闲设备数={}, 空闲设备={}",
|
log.info("设备状态解析完成: total={} availableCount={} availableDevices={}",
|
||||||
response.getTotalDevices(), response.getAvailableCount(), availableDevices);
|
response.getTotalDevices(), response.getAvailableCount(), availableDevices);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -79,7 +93,9 @@ public class DeviceStatusService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析单个设备信息
|
* 解析单个设备信息。
|
||||||
|
* - 识别设备系列与序号
|
||||||
|
* - 计算是否空闲(依赖配置项)
|
||||||
*/
|
*/
|
||||||
private DeviceStatusResponse.DeviceInfo parseDeviceInfo(String deviceId, JsonNode deviceNode) {
|
private DeviceStatusResponse.DeviceInfo parseDeviceInfo(String deviceId, JsonNode deviceNode) {
|
||||||
DeviceStatusResponse.DeviceInfo deviceInfo = new DeviceStatusResponse.DeviceInfo();
|
DeviceStatusResponse.DeviceInfo deviceInfo = new DeviceStatusResponse.DeviceInfo();
|
||||||
@@ -98,14 +114,15 @@ public class DeviceStatusService {
|
|||||||
boolean available = isDeviceAvailable(val);
|
boolean available = isDeviceAvailable(val);
|
||||||
deviceInfo.setAvailable(available);
|
deviceInfo.setAvailable(available);
|
||||||
|
|
||||||
log.debug("解析设备信息: deviceId={}, val={}, time={}, available={}, series={}, index={}",
|
// log.debug("解析设备信息: id={} val='{}' time='{}' available={} series={} index={}",
|
||||||
deviceId, val, time, available, deviceInfo.getSeries(), deviceInfo.getIndex());
|
// deviceId, val, time, available, deviceInfo.getSeries(), deviceInfo.getIndex());
|
||||||
|
|
||||||
return deviceInfo;
|
return deviceInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析设备编号的组成部分
|
* 解析设备编号的组成部分。
|
||||||
|
* 支持的前缀:f/s/g/d/ss/gg,后缀为数字序号。
|
||||||
*/
|
*/
|
||||||
private void parseDeviceIdComponents(String deviceId, DeviceStatusResponse.DeviceInfo deviceInfo) {
|
private void parseDeviceIdComponents(String deviceId, DeviceStatusResponse.DeviceInfo deviceInfo) {
|
||||||
Matcher matcher = DEVICE_PATTERN.matcher(deviceId);
|
Matcher matcher = DEVICE_PATTERN.matcher(deviceId);
|
||||||
@@ -117,7 +134,7 @@ public class DeviceStatusService {
|
|||||||
try {
|
try {
|
||||||
deviceInfo.setIndex(Integer.parseInt(indexStr));
|
deviceInfo.setIndex(Integer.parseInt(indexStr));
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
log.warn("解析设备序号失败: deviceId={}, indexStr={}", deviceId, indexStr);
|
log.warn("解析设备序号失败: id={} indexStr='{}'", deviceId, indexStr);
|
||||||
deviceInfo.setIndex(null);
|
deviceInfo.setIndex(null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -127,7 +144,9 @@ public class DeviceStatusService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断设备是否空闲
|
* 判断设备是否空闲。
|
||||||
|
* 依据系统配置 `deviceIdleStatus` 的值进行比对。
|
||||||
|
* 约定:返回 true 表示设备可分配使用。
|
||||||
*/
|
*/
|
||||||
private boolean isDeviceAvailable(String val) {
|
private boolean isDeviceAvailable(String val) {
|
||||||
String idleStatus = systemConfigService.getDeviceIdleStatus();
|
String idleStatus = systemConfigService.getDeviceIdleStatus();
|
||||||
@@ -135,23 +154,23 @@ public class DeviceStatusService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析特定设备的状态信息(用于检查是否完成游戏)
|
* 解析特定设备的状态信息(用于检查是否完成/或提取分数)。
|
||||||
* @param jsonResponse JSON响应字符串
|
* @param jsonResponse JSON字符串
|
||||||
* @param machineId 设备ID
|
* @param machineId 设备ID
|
||||||
* @return 包含f0(点数)和f1(状态)的Map
|
* @return 返回 map,可能包含键 f0(分数) 与 f1(状态)。
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> parseDeviceStatusForMachine(String jsonResponse, String machineId) {
|
public Map<String, Object> parseDeviceStatusForMachine(String jsonResponse, String machineId) {
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.debug("解析设备 {} 的状态信息", machineId);
|
log.debug("解析设备状态:id={},payloadLen={}", machineId, jsonResponse != null ? jsonResponse.length() : 0);
|
||||||
|
|
||||||
JsonNode rootNode = objectMapper.readTree(jsonResponse);
|
JsonNode rootNode = objectMapper.readTree(jsonResponse);
|
||||||
|
|
||||||
// 查找指定设备的信息
|
// 查找指定设备的信息
|
||||||
JsonNode deviceNode = rootNode.get(machineId);
|
JsonNode deviceNode = rootNode.get(machineId);
|
||||||
if (deviceNode == null) {
|
if (deviceNode == null) {
|
||||||
log.warn("未找到设备 {} 的状态信息", machineId);
|
log.warn("未找到设备状态节点:id={}", machineId);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,12 +202,12 @@ public class DeviceStatusService {
|
|||||||
result.put("f1", f1Info);
|
result.put("f1", f1Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("设备 {} 状态解析完成: {}", machineId, result);
|
log.debug("设备状态解析完成:id={} result={}", machineId, result);
|
||||||
|
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
log.error("解析设备 {} 状态JSON失败: {}", machineId, e.getMessage(), e);
|
log.error("设备状态JSON解析失败:id={} err={}", machineId, e.getMessage(), e);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("解析设备 {} 状态时发生异常", machineId, e);
|
log.error("解析设备状态发生异常:id={}", machineId, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
import org.springframework.web.reactive.function.client.ExchangeStrategies;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.util.retry.Retry;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
@@ -97,25 +99,55 @@ public class ScriptClient {
|
|||||||
* 检查空闲设备(返回原始字符串)
|
* 检查空闲设备(返回原始字符串)
|
||||||
*/
|
*/
|
||||||
public Mono<String> checkAvailableDevice() {
|
public Mono<String> checkAvailableDevice() {
|
||||||
String url = apiBaseUrl + "/yijianwan_netfile/readAllMsg?文件名=判断分数";
|
|
||||||
log.debug("检查空闲设备: {}", url);
|
|
||||||
return webClient.get()
|
return webClient.get()
|
||||||
.uri(url)
|
.uri(apiBaseUrl + "/yijianwan_netfile/readAllMsg?文件名=判断分数")
|
||||||
.accept(MediaType.APPLICATION_JSON)
|
.accept(MediaType.ALL) // 先别卡死在 JSON
|
||||||
.retrieve()
|
.exchangeToMono(resp -> {
|
||||||
.bodyToMono(String.class)
|
HttpStatus sc = resp.statusCode();
|
||||||
|
return resp.bodyToMono(String.class)
|
||||||
|
.defaultIfEmpty("")
|
||||||
|
.map(body -> {
|
||||||
|
log.debug("status={}, headers={}, body={}", sc.value(), resp.headers().asHttpHeaders(), body);
|
||||||
|
if (sc.is2xxSuccessful()) return body;
|
||||||
|
throw new IllegalStateException("HTTP " + sc.value() + " - " + body);
|
||||||
|
});
|
||||||
|
})
|
||||||
.timeout(Duration.ofSeconds(10))
|
.timeout(Duration.ofSeconds(10))
|
||||||
.retry(3) // 失败时重试3次
|
// 仅在 5xx 或 IO 问题时重试;4xx/超时不重试,避免凑满 40s
|
||||||
.doOnSuccess(result -> log.debug("检查空闲设备成功: {}", result))
|
.retryWhen(Retry.fixedDelay(2, Duration.ofMillis(500))
|
||||||
.doOnError(e -> log.warn("检查空闲设备失败: {}", e.toString()));
|
.filter(ex -> {
|
||||||
|
if (ex instanceof java.io.IOException) return true;
|
||||||
|
if (ex instanceof IllegalStateException ise) {
|
||||||
|
String m = ise.getMessage();
|
||||||
|
return m != null && m.startsWith("HTTP 5");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.onRetryExhaustedThrow((spec, signal) -> signal.failure()))
|
||||||
|
.doOnError(e -> log.warn("checkAvailableDevice failed: {}", e.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查空闲设备(解析后的结构化数据)
|
* 检查空闲设备(解析后的结构化数据)
|
||||||
*/
|
*/
|
||||||
public Mono<DeviceStatusResponse> checkAvailableDeviceStatus() {
|
public Mono<DeviceStatusResponse> checkAvailableDeviceStatus() {
|
||||||
|
long tStart = System.currentTimeMillis();
|
||||||
|
|
||||||
return checkAvailableDevice()
|
return checkAvailableDevice()
|
||||||
.map(jsonResponse -> deviceStatusService.parseDeviceStatus(jsonResponse))
|
.map(jsonResponse -> {
|
||||||
|
long tHttpEnd = System.currentTimeMillis();
|
||||||
|
long tParseStart = tHttpEnd;
|
||||||
|
DeviceStatusResponse parsed = deviceStatusService.parseDeviceStatus(jsonResponse);
|
||||||
|
long tParseEnd = System.currentTimeMillis();
|
||||||
|
log.debug("checkAvailableDeviceStatus jsonResponse: {}", jsonResponse);
|
||||||
|
// 在对象里临时塞入测量数据不可取,这里通过日志输出
|
||||||
|
if (log.isInfoEnabled()) {
|
||||||
|
long httpCost = tHttpEnd - tStart;
|
||||||
|
long parseCost = tParseEnd - tParseStart;
|
||||||
|
log.info("checkAvailableDeviceStatus stage cost: http={} ms, parse={} ms", httpCost, parseCost);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
})
|
||||||
.doOnSuccess(deviceStatus -> {
|
.doOnSuccess(deviceStatus -> {
|
||||||
log.info("设备状态检查完成: 总设备数={}, 空闲设备数={}",
|
log.info("设备状态检查完成: 总设备数={}, 空闲设备数={}",
|
||||||
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount());
|
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount());
|
||||||
@@ -123,11 +155,18 @@ public class ScriptClient {
|
|||||||
log.info("空闲设备列表: {}", deviceStatus.getAvailableDevices());
|
log.info("空闲设备列表: {}", deviceStatus.getAvailableDevices());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发布设备状态更新事件,由事件监听器处理任务更新
|
long tEventStart = System.currentTimeMillis();
|
||||||
try {
|
try {
|
||||||
eventPublisher.publishEvent(new com.gameplatform.server.event.DeviceStatusUpdatedEvent(this, deviceStatus));
|
eventPublisher.publishEvent(new com.gameplatform.server.event.DeviceStatusUpdatedEvent(this, deviceStatus));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("发布设备状态更新事件时发生异常", e);
|
log.error("发布设备状态更新事件时发生异常", e);
|
||||||
|
} finally {
|
||||||
|
long tEventEnd = System.currentTimeMillis();
|
||||||
|
long total = tEventEnd - tStart;
|
||||||
|
long eventCost = tEventEnd - tEventStart;
|
||||||
|
if (log.isInfoEnabled()) {
|
||||||
|
log.info("checkAvailableDeviceStatus stage cost: event={} ms, total={} ms", eventCost, total);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.doOnError(e -> log.error("设备状态解析失败: {}", e.getMessage(), e));
|
.doOnError(e -> log.error("设备状态解析失败: {}", e.getMessage(), e));
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.dao.CannotAcquireLockException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,13 +45,12 @@ public class DeviceTaskUpdateService {
|
|||||||
* 根据设备状态信息更新链接任务 - 改进版
|
* 根据设备状态信息更新链接任务 - 改进版
|
||||||
* @param deviceInfo 设备状态信息
|
* @param deviceInfo 设备状态信息
|
||||||
*/
|
*/
|
||||||
@Transactional
|
|
||||||
public void updateTaskByDeviceStatus(DeviceStatusResponse.DeviceInfo deviceInfo) {
|
public void updateTaskByDeviceStatus(DeviceStatusResponse.DeviceInfo deviceInfo) {
|
||||||
String deviceId = deviceInfo.getDeviceId();
|
String deviceId = deviceInfo.getDeviceId();
|
||||||
String val = deviceInfo.getVal();
|
String val = deviceInfo.getVal();
|
||||||
|
|
||||||
log.debug("开始处理设备 {} 的状态更新: val={}, available={}",
|
// log.debug("开始处理设备 {} 的状态更新: val={}, available={}",
|
||||||
deviceId, val, deviceInfo.isAvailable());
|
// deviceId, val, deviceInfo.isAvailable());
|
||||||
|
|
||||||
// 使用改进的游戏完成检测服务
|
// 使用改进的游戏完成检测服务
|
||||||
boolean completionDetected = completionDetectionService.detectGameCompletion(
|
boolean completionDetected = completionDetectionService.detectGameCompletion(
|
||||||
@@ -68,30 +67,68 @@ public class DeviceTaskUpdateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅更新任务点数(不改变状态)
|
* 仅更新任务点数(不改变状态)- 带重试机制
|
||||||
*/
|
*/
|
||||||
private void updateTaskPointsOnly(String deviceId, Integer points) {
|
private void updateTaskPointsOnly(String deviceId, Integer points) {
|
||||||
List<LinkTask> loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "LOGGED_IN");
|
List<LinkTask> loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "LOGGED_IN");
|
||||||
|
|
||||||
for (LinkTask task : loggedInTasks) {
|
for (LinkTask task : loggedInTasks) {
|
||||||
try {
|
try {
|
||||||
// 只更新点数,保持LOGGED_IN状态
|
updateSingleTaskPoints(task, points, 3); // 最多重试3次
|
||||||
task.setCompletedPoints(points);
|
} catch (Exception e) {
|
||||||
task.setUpdatedAt(LocalDateTime.now());
|
log.error("更新任务 {} 点数时发生异常,跳过该任务继续处理其他任务", task.getCodeNo(), e);
|
||||||
|
// 继续处理其他任务,不因单个任务失败而中断
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int updated = linkTaskMapper.update(task);
|
/**
|
||||||
if (updated > 0) {
|
* 更新单个任务点数,带重试机制
|
||||||
log.debug("任务 {} 点数已更新为: {}", task.getCodeNo(), points);
|
*/
|
||||||
} else {
|
private void updateSingleTaskPoints(LinkTask task, Integer points, int maxRetries) {
|
||||||
log.warn("更新任务 {} 点数失败", task.getCodeNo());
|
for (int attempts = 1; attempts <= maxRetries; attempts++) {
|
||||||
|
try {
|
||||||
|
updateTaskPointsTransaction(task, points);
|
||||||
|
return; // 成功则退出
|
||||||
|
} catch (CannotAcquireLockException e) {
|
||||||
|
log.warn("任务 {} 更新时遇到锁等待超时,重试次数: {}/{}",
|
||||||
|
task.getCodeNo(), attempts, maxRetries);
|
||||||
|
|
||||||
|
if (attempts >= maxRetries) {
|
||||||
|
log.error("任务 {} 点数更新失败,已达到最大重试次数", task.getCodeNo());
|
||||||
|
throw e; // 重新抛出异常让上层处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 短暂等待后重试
|
||||||
|
try {
|
||||||
|
long jitter = (long) (Math.random() * 50L);
|
||||||
|
Thread.sleep(100L * attempts + jitter); // 递增 + 抖动
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new RuntimeException("重试被中断", ie);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("更新任务 {} 点数时发生异常", task.getCodeNo(), e);
|
log.error("更新任务 {} 点数时发生异常", task.getCodeNo(), e);
|
||||||
|
throw e; // 其他异常直接抛出
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在事务中更新任务点数
|
||||||
|
*/
|
||||||
|
@Transactional(timeout = 10)
|
||||||
|
private void updateTaskPointsTransaction(LinkTask task, Integer points) {
|
||||||
|
// 仅当点数更大时才更新,减少写锁争用
|
||||||
|
int updated = linkTaskMapper.updatePointsIfGreater(task.getId(), points);
|
||||||
|
if (updated > 0) {
|
||||||
|
log.debug("任务 {} 点数已更新为: {}", task.getCodeNo(), points);
|
||||||
|
} else {
|
||||||
|
log.debug("任务 {} 点数未更新(新值不大于现有值或记录已变更)", task.getCodeNo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 保留原方法以兼容现有代码,但标记为已弃用
|
// 保留原方法以兼容现有代码,但标记为已弃用
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,18 +165,21 @@ public class DeviceTaskUpdateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量处理设备状态更新
|
* 批量处理设备状态更新 - 优化版本
|
||||||
|
* 移除大事务,改为单个设备独立处理以避免锁等待超时
|
||||||
* @param deviceStatus 设备状态响应
|
* @param deviceStatus 设备状态响应
|
||||||
*/
|
*/
|
||||||
@Transactional
|
|
||||||
public void batchUpdateTasksByDeviceStatus(DeviceStatusResponse deviceStatus) {
|
public void batchUpdateTasksByDeviceStatus(DeviceStatusResponse deviceStatus) {
|
||||||
log.debug("开始批量处理设备状态更新,设备数量: {}", deviceStatus.getTotalDevices());
|
log.debug("开始批量处理设备状态更新,设备数量: {}", deviceStatus.getTotalDevices());
|
||||||
|
|
||||||
|
// 移除 @Transactional 注解,让每个设备独立处理
|
||||||
for (DeviceStatusResponse.DeviceInfo deviceInfo : deviceStatus.getDevices().values()) {
|
for (DeviceStatusResponse.DeviceInfo deviceInfo : deviceStatus.getDevices().values()) {
|
||||||
try {
|
try {
|
||||||
|
// 每个设备状态更新都是独立的小事务
|
||||||
updateTaskByDeviceStatus(deviceInfo);
|
updateTaskByDeviceStatus(deviceInfo);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("处理设备 {} 状态更新时发生异常", deviceInfo.getDeviceId(), e);
|
log.error("处理设备 {} 状态更新时发生异常", deviceInfo.getDeviceId(), e);
|
||||||
|
// 继续处理其他设备,不因单个设备失败而中断整个批次
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,11 +187,11 @@ public class DeviceTaskUpdateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听设备状态更新事件
|
* 监听设备状态更新事件 - 优化版本
|
||||||
|
* 移除事务注解,避免长事务导致的锁等待
|
||||||
* @param event 设备状态更新事件
|
* @param event 设备状态更新事件
|
||||||
*/
|
*/
|
||||||
@EventListener
|
@EventListener
|
||||||
@Transactional
|
|
||||||
public void handleDeviceStatusUpdatedEvent(DeviceStatusUpdatedEvent event) {
|
public void handleDeviceStatusUpdatedEvent(DeviceStatusUpdatedEvent event) {
|
||||||
log.debug("收到设备状态更新事件,开始处理任务更新");
|
log.debug("收到设备状态更新事件,开始处理任务更新");
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import com.gameplatform.server.model.entity.agent.LinkBatch;
|
|||||||
import com.gameplatform.server.model.entity.agent.LinkTask;
|
import com.gameplatform.server.model.entity.agent.LinkTask;
|
||||||
|
|
||||||
import com.gameplatform.server.service.external.ScriptClient;
|
import com.gameplatform.server.service.external.ScriptClient;
|
||||||
|
import com.gameplatform.server.device.Detection;
|
||||||
|
import com.gameplatform.server.device.DeviceStats;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import com.gameplatform.server.service.device.DeviceStatusCheckService;
|
import com.gameplatform.server.service.device.DeviceStatusCheckService;
|
||||||
import com.gameplatform.server.service.admin.SystemConfigService;
|
import com.gameplatform.server.service.admin.SystemConfigService;
|
||||||
@@ -44,6 +46,8 @@ public class LinkStatusService {
|
|||||||
private final SystemConfigService systemConfigService;
|
private final SystemConfigService systemConfigService;
|
||||||
private final MemoryMachineCooldownService machineCooldownService;
|
private final MemoryMachineCooldownService machineCooldownService;
|
||||||
private final DeviceAllocationService deviceAllocationService;
|
private final DeviceAllocationService deviceAllocationService;
|
||||||
|
private final Detection detection;
|
||||||
|
private final DeviceStats deviceStats;
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private com.gameplatform.server.service.detection.GameCompletionDetectionService completionDetectionService;
|
private com.gameplatform.server.service.detection.GameCompletionDetectionService completionDetectionService;
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
@@ -62,7 +66,7 @@ public class LinkStatusService {
|
|||||||
|
|
||||||
public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper,
|
public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper,
|
||||||
ScriptClient scriptClient,
|
ScriptClient scriptClient,
|
||||||
DeviceStatusCheckService deviceStatusCheckService, SystemConfigService systemConfigService, MemoryMachineCooldownService machineCooldownService, DeviceAllocationService deviceAllocationService) {
|
DeviceStatusCheckService deviceStatusCheckService, SystemConfigService systemConfigService, MemoryMachineCooldownService machineCooldownService, DeviceAllocationService deviceAllocationService, Detection detection, DeviceStats deviceStats) {
|
||||||
this.linkTaskMapper = linkTaskMapper;
|
this.linkTaskMapper = linkTaskMapper;
|
||||||
this.linkBatchMapper = linkBatchMapper;
|
this.linkBatchMapper = linkBatchMapper;
|
||||||
this.scriptClient = scriptClient;
|
this.scriptClient = scriptClient;
|
||||||
@@ -70,6 +74,8 @@ public class LinkStatusService {
|
|||||||
this.systemConfigService = systemConfigService;
|
this.systemConfigService = systemConfigService;
|
||||||
this.machineCooldownService = machineCooldownService;
|
this.machineCooldownService = machineCooldownService;
|
||||||
this.deviceAllocationService = deviceAllocationService;
|
this.deviceAllocationService = deviceAllocationService;
|
||||||
|
this.detection = detection;
|
||||||
|
this.deviceStats = deviceStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -699,7 +705,12 @@ private UserLinkStatusResponse doGetUserLinkStatus(Long linkId, String codeNo) {
|
|||||||
linkTaskMapper.updateById(linkTask);
|
linkTaskMapper.updateById(linkTask);
|
||||||
}
|
}
|
||||||
log.info("首次选区: 开始检查和分配空闲设备");
|
log.info("首次选区: 开始检查和分配空闲设备");
|
||||||
DeviceStatusResponse deviceStatus = scriptClient.checkAvailableDeviceStatus().block();
|
// 使用 Detection 的短TTL缓存,避免频繁触发事件引发并发写
|
||||||
|
DeviceStatusResponse deviceStatus = detection.listAllDevices();
|
||||||
|
if (deviceStatus == null) {
|
||||||
|
log.error("获取设备快照失败,无法进行选区");
|
||||||
|
throw new RuntimeException("暂时无法获取设备状态,请稍后再试");
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否有空闲设备
|
// 检查是否有空闲设备
|
||||||
if (deviceStatus.getAvailableCount() == 0) {
|
if (deviceStatus.getAvailableCount() == 0) {
|
||||||
@@ -711,12 +722,12 @@ private UserLinkStatusResponse doGetUserLinkStatus(Long linkId, String codeNo) {
|
|||||||
log.info("空闲设备检查完成 总设备数={}, 空闲设备数{}, 空闲设备列表={}",
|
log.info("空闲设备检查完成 总设备数={}, 空闲设备数{}, 空闲设备列表={}",
|
||||||
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices());
|
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices());
|
||||||
|
|
||||||
// 使用新的设备分配服务进行原子设备分配
|
// 由 DeviceStats 使用快照完成分配与内存分组更新
|
||||||
List<String> availableDevices = deviceStatus.getAvailableDevices();
|
selectedDeviceId = deviceStats.allocateFromSnapshot(deviceStatus, linkTask.getId(), "首次选区");
|
||||||
selectedDeviceId = deviceAllocationService.allocateDevice(availableDevices, linkTask.getId(), "首次选区");
|
|
||||||
|
|
||||||
if (selectedDeviceId == null) {
|
if (selectedDeviceId == null) {
|
||||||
log.error("设备分配失败: 所有空闲设备都在冷却期内或被占用 总空闲设备数={}", availableDevices.size());
|
int candidateSize = deviceStatus.getAvailableDevices() != null ? deviceStatus.getAvailableDevices().size() : 0;
|
||||||
|
log.error("设备分配失败: 所有空闲设备都在冷却期内或被占用 总空闲设备数={}", candidateSize);
|
||||||
throw new RuntimeException("所有设备都在冷却期内或被占用,请稍后再试");
|
throw new RuntimeException("所有设备都在冷却期内或被占用,请稍后再试");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class DeviceStatusCheckTask {
|
|||||||
/**
|
/**
|
||||||
* 每分钟检查一次空闲设备,并更新相关链接任务状态
|
* 每分钟检查一次空闲设备,并更新相关链接任务状态
|
||||||
*/
|
*/
|
||||||
@Scheduled(fixedRate = 60000) // 每60秒执行一次
|
// @Scheduled(fixedRate = 60000) // 每60秒执行一次
|
||||||
public void checkIdleDevicesAndUpdateTasks() {
|
public void checkIdleDevicesAndUpdateTasks() {
|
||||||
log.debug("开始定时检查空闲设备");
|
log.debug("开始定时检查空闲设备");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.gameplatform.server.task;
|
||||||
|
|
||||||
|
import com.gameplatform.server.mapper.history.DeviceStatusTransitionMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备状态变更记录清理任务
|
||||||
|
* 定期清理24小时前的设备状态变更历史记录,避免表数据过大影响性能
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DeviceStatusTransitionCleanupTask {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DeviceStatusTransitionCleanupTask.class);
|
||||||
|
|
||||||
|
private final DeviceStatusTransitionMapper deviceStatusTransitionMapper;
|
||||||
|
|
||||||
|
public DeviceStatusTransitionCleanupTask(DeviceStatusTransitionMapper deviceStatusTransitionMapper) {
|
||||||
|
this.deviceStatusTransitionMapper = deviceStatusTransitionMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每6小时清理一次24小时前的记录
|
||||||
|
* 执行时间:每天的 02:00, 08:00, 14:00, 20:00
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 0 2,8,14,20 * * ?")
|
||||||
|
public void cleanupOldRecords() {
|
||||||
|
try {
|
||||||
|
log.info("开始清理设备状态变更历史记录...");
|
||||||
|
|
||||||
|
int deletedCount = deviceStatusTransitionMapper.deleteOldRecords();
|
||||||
|
|
||||||
|
if (deletedCount > 0) {
|
||||||
|
log.info("设备状态变更记录清理完成: 删除了{}条24小时前的记录", deletedCount);
|
||||||
|
} else {
|
||||||
|
log.debug("设备状态变更记录清理完成: 无需要清理的记录");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("设备状态变更记录清理失败: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发清理任务(用于测试或紧急清理)
|
||||||
|
* 可通过管理接口或其他方式调用
|
||||||
|
*/
|
||||||
|
public int manualCleanup() {
|
||||||
|
try {
|
||||||
|
log.info("手动触发设备状态变更记录清理...");
|
||||||
|
int deletedCount = deviceStatusTransitionMapper.deleteOldRecords();
|
||||||
|
log.info("手动清理完成: 删除了{}条记录", deletedCount);
|
||||||
|
return deletedCount;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("手动清理失败: {}", e.getMessage(), e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ public class MachineCooldownCleanupTask {
|
|||||||
/**
|
/**
|
||||||
* 每30分钟清理一次过期的冷却记录
|
* 每30分钟清理一次过期的冷却记录
|
||||||
*/
|
*/
|
||||||
@Scheduled(fixedRate = 30 * 60 * 1000) // 30分钟
|
// @Scheduled(fixedRate = 30 * 60 * 1000) // 30分钟
|
||||||
public void cleanupExpiredCooldowns() {
|
public void cleanupExpiredCooldowns() {
|
||||||
try {
|
try {
|
||||||
int sizeBefore = machineCooldownService.getCooldownQueueSize();
|
int sizeBefore = machineCooldownService.getCooldownQueueSize();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class UsingLinkCheckTask {
|
|||||||
/**
|
/**
|
||||||
* 每30秒检测一次USING状态的链接
|
* 每30秒检测一次USING状态的链接
|
||||||
*/
|
*/
|
||||||
@Scheduled(fixedRate = 30000) // 每30秒执行一次
|
// @Scheduled(fixedRate = 30000) // 每30秒执行一次
|
||||||
public void checkUsingLinksAndHandleLoginStatus() {
|
public void checkUsingLinksAndHandleLoginStatus() {
|
||||||
log.debug("开始定时检查USING状态的链接");
|
log.debug("开始定时检查USING状态的链接");
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,30 @@ spring:
|
|||||||
name: gameplatform-server
|
name: gameplatform-server
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://192.140.164.137:3306/login_task_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf8&allowPublicKeyRetrieval=true
|
url: jdbc:mysql://120.46.74.24:3306/login_task_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&sessionVariables=innodb_lock_wait_timeout=30
|
||||||
username: login_task_db
|
username: login_task_db
|
||||||
password: 3MaXfeWJ4d6cGMrL
|
password: 3MaXfeWJ4d6cGMrL
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
hikari:
|
hikari:
|
||||||
maximum-pool-size: 10
|
maximum-pool-size: 50
|
||||||
minimum-idle: 2
|
minimum-idle: 10
|
||||||
connection-timeout: 30000
|
connection-timeout: 30000
|
||||||
|
idle-timeout: 600000
|
||||||
|
max-lifetime: 1800000
|
||||||
|
leak-detection-threshold: 60000
|
||||||
|
validation-timeout: 5000
|
||||||
|
connection-test-query: SELECT 1
|
||||||
|
# 连接池健康监控和自动重连
|
||||||
|
initialization-fail-timeout: 1
|
||||||
|
register-mbeans: true
|
||||||
|
# 自动重连配置
|
||||||
|
auto-commit: true
|
||||||
|
task:
|
||||||
|
detection:
|
||||||
|
poll:
|
||||||
|
size: 4
|
||||||
|
transaction:
|
||||||
|
default-timeout: 30
|
||||||
|
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
mapper-locations: classpath:mapper/**/*.xml
|
mapper-locations: classpath:mapper/**/*.xml
|
||||||
@@ -39,16 +55,16 @@ logging:
|
|||||||
root: info
|
root: info
|
||||||
com.gameplatform.server: debug # 保持整体调试
|
com.gameplatform.server: debug # 保持整体调试
|
||||||
# 仅保留设备解析最终汇总(INFO),其余降级
|
# 仅保留设备解析最终汇总(INFO),其余降级
|
||||||
com.gameplatform.server.service.device.DeviceStatusService: info
|
# com.gameplatform.server.service.device.DeviceStatusService: info
|
||||||
com.gameplatform.server.service.device.DeviceStatusCheckService: info
|
# com.gameplatform.server.service.device.DeviceStatusCheckService: info
|
||||||
# 脚本客户端与定时任务降噪
|
# # 脚本客户端与定时任务降噪
|
||||||
com.gameplatform.server.service.external.ScriptClient: warn
|
# com.gameplatform.server.service.external.ScriptClient: warn
|
||||||
com.gameplatform.server.task.DeviceStatusCheckTask: warn
|
# com.gameplatform.server.task.DeviceStatusCheckTask: warn
|
||||||
com.gameplatform.server.task.UsingLinkCheckTask: warn
|
# com.gameplatform.server.task.UsingLinkCheckTask: warn
|
||||||
# 完成检测服务降噪(屏蔽debug“置信度低”之类日志)
|
# # 完成检测服务降噪(屏蔽debug“置信度低”之类日志)
|
||||||
com.gameplatform.server.service.detection.GameCompletionDetectionService: warn
|
# com.gameplatform.server.service.detection.GameCompletionDetectionService: warn
|
||||||
# 设备任务更新服务:只保留警告/错误(不输出“开始处理设备/点数已更新为”等调试信息)
|
# # 设备任务更新服务:只保留警告/错误(不输出“开始处理设备/点数已更新为”等调试信息)
|
||||||
com.gameplatform.server.service.link.DeviceTaskUpdateService: warn
|
# com.gameplatform.server.service.link.DeviceTaskUpdateService: warn
|
||||||
# Mapper 与 SQL 调用降噪(屏蔽 MyBatis 的参数/SQL DEBUG)
|
# Mapper 与 SQL 调用降噪(屏蔽 MyBatis 的参数/SQL DEBUG)
|
||||||
com.gameplatform.server.mapper: warn
|
com.gameplatform.server.mapper: warn
|
||||||
com.baomidou.mybatisplus: warn
|
com.baomidou.mybatisplus: warn
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- Device status transition audit table
|
||||||
|
CREATE TABLE IF NOT EXISTS device_status_transition (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
device_id VARCHAR(64) NOT NULL,
|
||||||
|
prev_status VARCHAR(32) NULL,
|
||||||
|
new_status VARCHAR(32) NOT NULL,
|
||||||
|
reason VARCHAR(255) NULL,
|
||||||
|
snapshot_val VARCHAR(64) NULL,
|
||||||
|
series VARCHAR(16) NULL,
|
||||||
|
index_no INT NULL,
|
||||||
|
occurred_at DATETIME NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_device_created (device_id, created_at),
|
||||||
|
INDEX idx_created (created_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
|
||||||
@@ -82,6 +82,14 @@
|
|||||||
WHERE id = #{id}
|
WHERE id = #{id}
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<!-- 仅当新点数更大时更新,减少写冲突和无效更新 -->
|
||||||
|
<update id="updatePointsIfGreater">
|
||||||
|
UPDATE link_task
|
||||||
|
SET completed_points = #{newPoints}, updated_at = NOW()
|
||||||
|
WHERE id = #{id}
|
||||||
|
AND (completed_points IS NULL OR completed_points < #{newPoints})
|
||||||
|
</update>
|
||||||
|
|
||||||
<select id="findByAgentId" resultMap="LinkTaskMap">
|
<select id="findByAgentId" resultMap="LinkTaskMap">
|
||||||
SELECT id, batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at, created_at, updated_at, need_refresh, refresh_time, qr_created_at, qr_expire_at, first_region_select_at, completed_points, completion_images
|
SELECT id, batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at, created_at, updated_at, need_refresh, refresh_time, qr_created_at, qr_expire_at, first_region_select_at, completed_points, completion_images
|
||||||
FROM link_task
|
FROM link_task
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.gameplatform.server.mapper.history.DeviceStatusTransitionMapper">
|
||||||
|
<resultMap id="DeviceStatusTransitionMap" type="com.gameplatform.server.model.entity.history.DeviceStatusTransition">
|
||||||
|
<id property="id" column="id" />
|
||||||
|
<result property="deviceId" column="device_id" />
|
||||||
|
<result property="prevStatus" column="prev_status" />
|
||||||
|
<result property="newStatus" column="new_status" />
|
||||||
|
<result property="reason" column="reason" />
|
||||||
|
<result property="snapshotVal" column="snapshot_val" />
|
||||||
|
<result property="series" column="series" />
|
||||||
|
<result property="indexNo" column="index_no" />
|
||||||
|
<result property="occurredAt" column="occurred_at" />
|
||||||
|
<result property="createdAt" column="created_at" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<insert id="insert" parameterType="com.gameplatform.server.model.entity.history.DeviceStatusTransition" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
INSERT INTO device_status_transition (device_id, prev_status, new_status, reason, snapshot_val, series, index_no, occurred_at, created_at)
|
||||||
|
VALUES (#{deviceId}, #{prevStatus}, #{newStatus}, #{reason}, #{snapshotVal}, #{series}, #{indexNo}, #{occurredAt}, #{createdAt})
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<select id="findRecentByDeviceId" resultMap="DeviceStatusTransitionMap">
|
||||||
|
SELECT id, device_id, prev_status, new_status, reason, snapshot_val, series, index_no, occurred_at, created_at
|
||||||
|
FROM device_status_transition
|
||||||
|
WHERE device_id = #{deviceId}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT #{size}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<delete id="deleteOldRecords">
|
||||||
|
DELETE FROM device_status_transition
|
||||||
|
WHERE created_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user