feat: 更新项目配置,升级Java版本至21,添加Lombok注解处理器,优化设备状态解析和任务更新逻辑

This commit is contained in:
yahaozhang
2025-09-16 01:21:24 +08:00
parent cb69777499
commit b14573bb88
22 changed files with 988 additions and 73 deletions

View 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;
}
}
}

View 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();
}
}
}

View File

@@ -27,6 +27,12 @@ public interface LinkTaskMapper extends BaseMapper<LinkTask> {
@Param("region") String region,
@Param("machineId") String machineId,
@Param("loginAt") LocalDateTime loginAt);
/**
* 仅当新的点数更大时才更新 completed_points避免不必要写锁
*/
int updatePointsIfGreater(@Param("id") Long id,
@Param("newPoints") Integer newPoints);
List<LinkTask> findByAgentId(@Param("agentId") Long agentId,
@Param("size") int size,

View File

@@ -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();
}

View File

@@ -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; }
}

View File

@@ -73,7 +73,7 @@ public class GameCompletionDetectionService {
// 查找该设备上的 LOGGED_IN 状态任务
List<LinkTask> loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(machineId, "LOGGED_IN");
if (loggedInTasks.isEmpty()) {
log.debug("设备{}没有LOGGED_IN状态的任务跳过完成检测", machineId);
// log.debug("设备{}没有LOGGED_IN状态的任务跳过完成检测", machineId);
return false;
}

View File

@@ -16,6 +16,12 @@ import java.util.regex.Pattern;
/**
* 设备状态服务
*
* 负责:
* - 解析设备状态 JSON 字符串为结构化对象
* - 识别空闲设备(支持配置项定义空闲值)
* - 提供针对单机的 f0/f1 状态解析辅助方法
* - 提供按系列分组和筛选空闲设备的便捷方法
*/
@Service
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) {
try {
log.debug("开始解析设备状态响应");
log.debug("开始解析设备状态响应,长度={} 字符", jsonResponse != null ? jsonResponse.length() : 0);
JsonNode rootNode = objectMapper.readTree(jsonResponse);
Map<String, DeviceStatusResponse.DeviceInfo> devices = new HashMap<>();
List<String> availableDevices = new ArrayList<>();
// 遍历所有设备
// 遍历所有设备节点,逐个解析
Iterator<Map.Entry<String, JsonNode>> fields = rootNode.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
String deviceId = field.getKey();
JsonNode deviceNode = field.getValue();
// 解析设备信息
// 解析设备信息(含系列、序号、是否空闲)
DeviceStatusResponse.DeviceInfo deviceInfo = parseDeviceInfo(deviceId, deviceNode);
devices.put(deviceId, deviceInfo);
@@ -67,7 +81,7 @@ public class DeviceStatusService {
response.setTotalDevices(devices.size());
response.setAvailableCount(availableDevices.size());
log.info("设备状态解析完成: 总设备数={}, 空闲设备数={}, 空闲设备={}",
log.info("设备状态解析完成: total={} availableCount={} availableDevices={}",
response.getTotalDevices(), response.getAvailableCount(), availableDevices);
return response;
@@ -79,7 +93,9 @@ public class DeviceStatusService {
}
/**
* 解析单个设备信息
* 解析单个设备信息
* - 识别设备系列与序号
* - 计算是否空闲(依赖配置项)
*/
private DeviceStatusResponse.DeviceInfo parseDeviceInfo(String deviceId, JsonNode deviceNode) {
DeviceStatusResponse.DeviceInfo deviceInfo = new DeviceStatusResponse.DeviceInfo();
@@ -98,14 +114,15 @@ public class DeviceStatusService {
boolean available = isDeviceAvailable(val);
deviceInfo.setAvailable(available);
log.debug("解析设备信息: deviceId={}, val={}, time={}, available={}, series={}, index={}",
deviceId, val, time, available, deviceInfo.getSeries(), deviceInfo.getIndex());
// log.debug("解析设备信息: id={} val='{}' time='{}' available={} series={} index={}",
// deviceId, val, time, available, deviceInfo.getSeries(), deviceInfo.getIndex());
return deviceInfo;
}
/**
* 解析设备编号的组成部分
* 解析设备编号的组成部分
* 支持的前缀f/s/g/d/ss/gg后缀为数字序号。
*/
private void parseDeviceIdComponents(String deviceId, DeviceStatusResponse.DeviceInfo deviceInfo) {
Matcher matcher = DEVICE_PATTERN.matcher(deviceId);
@@ -117,7 +134,7 @@ public class DeviceStatusService {
try {
deviceInfo.setIndex(Integer.parseInt(indexStr));
} catch (NumberFormatException e) {
log.warn("解析设备序号失败: deviceId={}, indexStr={}", deviceId, indexStr);
log.warn("解析设备序号失败: id={} indexStr='{}'", deviceId, indexStr);
deviceInfo.setIndex(null);
}
} else {
@@ -127,7 +144,9 @@ public class DeviceStatusService {
}
/**
* 判断设备是否空闲
* 判断设备是否空闲
* 依据系统配置 `deviceIdleStatus` 的值进行比对。
* 约定:返回 true 表示设备可分配使用。
*/
private boolean isDeviceAvailable(String val) {
String idleStatus = systemConfigService.getDeviceIdleStatus();
@@ -135,23 +154,23 @@ public class DeviceStatusService {
}
/**
* 解析特定设备的状态信息(用于检查是否完成游戏)
* @param jsonResponse JSON响应字符串
* 解析特定设备的状态信息(用于检查是否完成/或提取分数)。
* @param jsonResponse JSON字符串
* @param machineId 设备ID
* @return 包含f0(数)f1(状态)的Map
* @return 返回 map可能包含键 f0(数)f1(状态)
*/
public Map<String, Object> parseDeviceStatusForMachine(String jsonResponse, String machineId) {
Map<String, Object> result = new HashMap<>();
try {
log.debug("解析设备 {} 的状态信息", machineId);
log.debug("解析设备状态id={}payloadLen={}", machineId, jsonResponse != null ? jsonResponse.length() : 0);
JsonNode rootNode = objectMapper.readTree(jsonResponse);
// 查找指定设备的信息
JsonNode deviceNode = rootNode.get(machineId);
if (deviceNode == null) {
log.warn("未找到设备 {} 的状态信息", machineId);
log.warn("未找到设备状态节点id={}", machineId);
return result;
}
@@ -183,12 +202,12 @@ public class DeviceStatusService {
result.put("f1", f1Info);
}
log.debug("设备 {} 状态解析完成: {}", machineId, result);
log.debug("设备状态解析完成id={} result={}", machineId, result);
} catch (JsonProcessingException e) {
log.error("解析设备 {} 状态JSON失败: {}", machineId, e.getMessage(), e);
log.error("设备状态JSON解析失败id={} err={}", machineId, e.getMessage(), e);
} catch (Exception e) {
log.error("解析设备 {} 状态发生异常", machineId, e);
log.error("解析设备状态发生异常id={}", machineId, e);
}
return result;

View File

@@ -7,11 +7,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import java.time.Duration;
@@ -97,37 +99,74 @@ public class ScriptClient {
* 检查空闲设备(返回原始字符串)
*/
public Mono<String> checkAvailableDevice() {
String url = apiBaseUrl + "/yijianwan_netfile/readAllMsg?文件名=判断分数";
log.debug("检查空闲设备: {}", url);
return webClient.get()
.uri(url)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.uri(apiBaseUrl + "/yijianwan_netfile/readAllMsg?文件名=判断分数")
.accept(MediaType.ALL) // 先别卡死在 JSON
.exchangeToMono(resp -> {
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))
.retry(3) // 失败时重试3次
.doOnSuccess(result -> log.debug("检查空闲设备成功: {}", result))
.doOnError(e -> log.warn("检查空闲设备失败: {}", e.toString()));
// 仅在 5xx 或 IO 问题时重试4xx/超时不重试,避免凑满 40s
.retryWhen(Retry.fixedDelay(2, Duration.ofMillis(500))
.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() {
long tStart = System.currentTimeMillis();
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 -> {
log.info("设备状态检查完成: 总设备数={}, 空闲设备数={}",
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount());
if (deviceStatus.getAvailableCount() > 0) {
log.info("空闲设备列表: {}", deviceStatus.getAvailableDevices());
}
// 发布设备状态更新事件,由事件监听器处理任务更新
long tEventStart = System.currentTimeMillis();
try {
eventPublisher.publishEvent(new com.gameplatform.server.event.DeviceStatusUpdatedEvent(this, deviceStatus));
} catch (Exception 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));

View File

@@ -10,10 +10,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.dao.CannotAcquireLockException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -45,13 +45,12 @@ public class DeviceTaskUpdateService {
* 根据设备状态信息更新链接任务 - 改进版
* @param deviceInfo 设备状态信息
*/
@Transactional
public void updateTaskByDeviceStatus(DeviceStatusResponse.DeviceInfo deviceInfo) {
String deviceId = deviceInfo.getDeviceId();
String val = deviceInfo.getVal();
log.debug("开始处理设备 {} 的状态更新: val={}, available={}",
deviceId, val, deviceInfo.isAvailable());
// log.debug("开始处理设备 {} 的状态更新: val={}, available={}",
// deviceId, val, deviceInfo.isAvailable());
// 使用改进的游戏完成检测服务
boolean completionDetected = completionDetectionService.detectGameCompletion(
@@ -68,30 +67,68 @@ public class DeviceTaskUpdateService {
}
/**
* 仅更新任务点数(不改变状态)
* 仅更新任务点数(不改变状态)- 带重试机制
*/
private void updateTaskPointsOnly(String deviceId, Integer points) {
List<LinkTask> loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "LOGGED_IN");
for (LinkTask task : loggedInTasks) {
try {
// 只更新点数保持LOGGED_IN状态
task.setCompletedPoints(points);
task.setUpdatedAt(LocalDateTime.now());
updateSingleTaskPoints(task, points, 3); // 最多重试3次
} catch (Exception e) {
log.error("更新任务 {} 点数时发生异常,跳过该任务继续处理其他任务", task.getCodeNo(), e);
// 继续处理其他任务,不因单个任务失败而中断
}
}
}
/**
* 更新单个任务点数,带重试机制
*/
private void updateSingleTaskPoints(LinkTask task, Integer points, int maxRetries) {
for (int attempts = 1; attempts <= maxRetries; attempts++) {
try {
updateTaskPointsTransaction(task, points);
return; // 成功则退出
} catch (CannotAcquireLockException e) {
log.warn("任务 {} 更新时遇到锁等待超时,重试次数: {}/{}",
task.getCodeNo(), attempts, maxRetries);
int updated = linkTaskMapper.update(task);
if (updated > 0) {
log.debug("任务 {} 点数已更新为: {}", task.getCodeNo(), points);
} else {
log.warn("更新任务 {} 点数失败", task.getCodeNo());
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) {
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 设备状态响应
*/
@Transactional
public void batchUpdateTasksByDeviceStatus(DeviceStatusResponse deviceStatus) {
log.debug("开始批量处理设备状态更新,设备数量: {}", deviceStatus.getTotalDevices());
// 移除 @Transactional 注解,让每个设备独立处理
for (DeviceStatusResponse.DeviceInfo deviceInfo : deviceStatus.getDevices().values()) {
try {
// 每个设备状态更新都是独立的小事务
updateTaskByDeviceStatus(deviceInfo);
} catch (Exception e) {
log.error("处理设备 {} 状态更新时发生异常", deviceInfo.getDeviceId(), e);
// 继续处理其他设备,不因单个设备失败而中断整个批次
}
}
@@ -147,11 +187,11 @@ public class DeviceTaskUpdateService {
}
/**
* 监听设备状态更新事件
* 监听设备状态更新事件 - 优化版本
* 移除事务注解,避免长事务导致的锁等待
* @param event 设备状态更新事件
*/
@EventListener
@Transactional
public void handleDeviceStatusUpdatedEvent(DeviceStatusUpdatedEvent event) {
log.debug("收到设备状态更新事件,开始处理任务更新");
try {

View File

@@ -13,6 +13,8 @@ import com.gameplatform.server.model.entity.agent.LinkBatch;
import com.gameplatform.server.model.entity.agent.LinkTask;
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 com.gameplatform.server.service.device.DeviceStatusCheckService;
import com.gameplatform.server.service.admin.SystemConfigService;
@@ -44,6 +46,8 @@ public class LinkStatusService {
private final SystemConfigService systemConfigService;
private final MemoryMachineCooldownService machineCooldownService;
private final DeviceAllocationService deviceAllocationService;
private final Detection detection;
private final DeviceStats deviceStats;
@Autowired(required = false)
private com.gameplatform.server.service.detection.GameCompletionDetectionService completionDetectionService;
@Autowired(required = false)
@@ -62,7 +66,7 @@ public class LinkStatusService {
public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper,
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.linkBatchMapper = linkBatchMapper;
this.scriptClient = scriptClient;
@@ -70,6 +74,8 @@ public class LinkStatusService {
this.systemConfigService = systemConfigService;
this.machineCooldownService = machineCooldownService;
this.deviceAllocationService = deviceAllocationService;
this.detection = detection;
this.deviceStats = deviceStats;
}
/**
@@ -699,7 +705,12 @@ private UserLinkStatusResponse doGetUserLinkStatus(Long linkId, String codeNo) {
linkTaskMapper.updateById(linkTask);
}
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) {
@@ -711,12 +722,12 @@ private UserLinkStatusResponse doGetUserLinkStatus(Long linkId, String codeNo) {
log.info("空闲设备检查完成 总设备数={}, 空闲设备数{}, 空闲设备列表={}",
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices());
// 使用新的设备分配服务进行原子设备分配
List<String> availableDevices = deviceStatus.getAvailableDevices();
selectedDeviceId = deviceAllocationService.allocateDevice(availableDevices, linkTask.getId(), "首次选区");
// 由 DeviceStats 使用快照完成分配与内存分组更新
selectedDeviceId = deviceStats.allocateFromSnapshot(deviceStatus, linkTask.getId(), "首次选区");
if (selectedDeviceId == null) {
log.error("设备分配失败: 所有空闲设备都在冷却期内或被占用 总空闲设备数={}", availableDevices.size());
int candidateSize = deviceStatus.getAvailableDevices() != null ? deviceStatus.getAvailableDevices().size() : 0;
log.error("设备分配失败: 所有空闲设备都在冷却期内或被占用 总空闲设备数={}", candidateSize);
throw new RuntimeException("所有设备都在冷却期内或被占用,请稍后再试");
}

View File

@@ -27,7 +27,7 @@ public class DeviceStatusCheckTask {
/**
* 每分钟检查一次空闲设备,并更新相关链接任务状态
*/
@Scheduled(fixedRate = 60000) // 每60秒执行一次
// @Scheduled(fixedRate = 60000) // 每60秒执行一次
public void checkIdleDevicesAndUpdateTasks() {
log.debug("开始定时检查空闲设备");

View File

@@ -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;
}
}
}

View File

@@ -23,7 +23,7 @@ public class MachineCooldownCleanupTask {
/**
* 每30分钟清理一次过期的冷却记录
*/
@Scheduled(fixedRate = 30 * 60 * 1000) // 30分钟
// @Scheduled(fixedRate = 30 * 60 * 1000) // 30分钟
public void cleanupExpiredCooldowns() {
try {
int sizeBefore = machineCooldownService.getCooldownQueueSize();

View File

@@ -28,7 +28,7 @@ public class UsingLinkCheckTask {
/**
* 每30秒检测一次USING状态的链接
*/
@Scheduled(fixedRate = 30000) // 每30秒执行一次
// @Scheduled(fixedRate = 30000) // 每30秒执行一次
public void checkUsingLinksAndHandleLoginStatus() {
log.debug("开始定时检查USING状态的链接");

View File

@@ -3,14 +3,30 @@ spring:
name: gameplatform-server
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
password: 3MaXfeWJ4d6cGMrL
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 2
maximum-pool-size: 50
minimum-idle: 10
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:
mapper-locations: classpath:mapper/**/*.xml
@@ -39,16 +55,16 @@ logging:
root: info
com.gameplatform.server: debug # 保持整体调试
# 仅保留设备解析最终汇总INFO其余降级
com.gameplatform.server.service.device.DeviceStatusService: info
com.gameplatform.server.service.device.DeviceStatusCheckService: info
# 脚本客户端与定时任务降噪
com.gameplatform.server.service.external.ScriptClient: warn
com.gameplatform.server.task.DeviceStatusCheckTask: warn
com.gameplatform.server.task.UsingLinkCheckTask: warn
# 完成检测服务降噪屏蔽debug“置信度低”之类日志
com.gameplatform.server.service.detection.GameCompletionDetectionService: warn
# 设备任务更新服务:只保留警告/错误(不输出“开始处理设备/点数已更新为”等调试信息)
com.gameplatform.server.service.link.DeviceTaskUpdateService: warn
# com.gameplatform.server.service.device.DeviceStatusService: info
# com.gameplatform.server.service.device.DeviceStatusCheckService: info
# # 脚本客户端与定时任务降噪
# com.gameplatform.server.service.external.ScriptClient: warn
# com.gameplatform.server.task.DeviceStatusCheckTask: warn
# com.gameplatform.server.task.UsingLinkCheckTask: warn
# # 完成检测服务降噪屏蔽debug“置信度低”之类日志
# com.gameplatform.server.service.detection.GameCompletionDetectionService: warn
# # 设备任务更新服务:只保留警告/错误(不输出“开始处理设备/点数已更新为”等调试信息)
# com.gameplatform.server.service.link.DeviceTaskUpdateService: warn
# Mapper 与 SQL 调用降噪(屏蔽 MyBatis 的参数/SQL DEBUG
com.gameplatform.server.mapper: warn
com.baomidou.mybatisplus: warn

View File

@@ -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;

View File

@@ -82,6 +82,14 @@
WHERE id = #{id}
</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 &lt; #{newPoints})
</update>
<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
FROM link_task

View File

@@ -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 &lt; DATE_SUB(NOW(), INTERVAL 24 HOUR)
</delete>
</mapper>