feat: 集成游戏完成检测服务,优化设备状态检查和任务完成确认逻辑

This commit is contained in:
zyh
2025-09-09 20:57:24 +08:00
parent aaee312662
commit 86140b1294
9 changed files with 253 additions and 20 deletions

View File

@@ -0,0 +1,10 @@
package com.gameplatform.server.mapper.detection;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gameplatform.server.model.entity.detection.GameCompletionLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface GameCompletionLogMapper extends BaseMapper<GameCompletionLog> {
}

View File

@@ -0,0 +1,10 @@
package com.gameplatform.server.mapper.history;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gameplatform.server.model.entity.history.LinkTaskStatusHistory;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LinkTaskStatusHistoryMapper extends BaseMapper<LinkTaskStatusHistory> {
}

View File

@@ -0,0 +1,68 @@
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("link_task_status_history")
public class LinkTaskStatusHistory {
@TableId(type = IdType.AUTO)
private Long id;
private Long linkTaskId;
private String codeNo;
private String machineId;
private String prevStatus;
private String newStatus;
private String source;
private String reason;
private Integer sinceLoginSeconds;
private String extraJson;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
public LinkTaskStatusHistory() {}
public LinkTaskStatusHistory(Long linkTaskId, String codeNo, String machineId,
String prevStatus, String newStatus, String source,
String reason, Integer sinceLoginSeconds, String extraJson) {
this.linkTaskId = linkTaskId;
this.codeNo = codeNo;
this.machineId = machineId;
this.prevStatus = prevStatus;
this.newStatus = newStatus;
this.source = source;
this.reason = reason;
this.sinceLoginSeconds = sinceLoginSeconds;
this.extraJson = extraJson;
this.createdAt = LocalDateTime.now();
}
// getters/setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getLinkTaskId() { return linkTaskId; }
public void setLinkTaskId(Long linkTaskId) { this.linkTaskId = linkTaskId; }
public String getCodeNo() { return codeNo; }
public void setCodeNo(String codeNo) { this.codeNo = codeNo; }
public String getMachineId() { return machineId; }
public void setMachineId(String machineId) { this.machineId = machineId; }
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 getSource() { return source; }
public void setSource(String source) { this.source = source; }
public String getReason() { return reason; }
public void setReason(String reason) { this.reason = reason; }
public Integer getSinceLoginSeconds() { return sinceLoginSeconds; }
public void setSinceLoginSeconds(Integer sinceLoginSeconds) { this.sinceLoginSeconds = sinceLoginSeconds; }
public String getExtraJson() { return extraJson; }
public void setExtraJson(String extraJson) { this.extraJson = extraJson; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

View File

@@ -4,6 +4,9 @@ import com.gameplatform.server.mapper.agent.LinkTaskMapper;
import com.gameplatform.server.model.entity.agent.LinkTask;
import com.gameplatform.server.model.entity.detection.GameCompletionLog;
import com.gameplatform.server.service.cooldown.MachineCooldownService;
import com.gameplatform.server.mapper.detection.GameCompletionLogMapper;
import com.gameplatform.server.mapper.history.LinkTaskStatusHistoryMapper;
import com.gameplatform.server.model.entity.history.LinkTaskStatusHistory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@@ -35,6 +38,8 @@ public class GameCompletionDetectionService {
private final LinkTaskMapper linkTaskMapper;
private final MachineCooldownService machineCooldownService;
private final GameCompletionLogMapper gameCompletionLogMapper;
private final LinkTaskStatusHistoryMapper statusHistoryMapper;
// 待确认的完成检测machineId -> 检测时间
private final ConcurrentMap<String, LocalDateTime> pendingCompletions = new ConcurrentHashMap<>();
@@ -43,9 +48,13 @@ public class GameCompletionDetectionService {
private final ConcurrentMap<String, LocalDateTime> recentLogins = new ConcurrentHashMap<>();
public GameCompletionDetectionService(LinkTaskMapper linkTaskMapper,
MachineCooldownService machineCooldownService) {
MachineCooldownService machineCooldownService,
GameCompletionLogMapper gameCompletionLogMapper,
LinkTaskStatusHistoryMapper statusHistoryMapper) {
this.linkTaskMapper = linkTaskMapper;
this.machineCooldownService = machineCooldownService;
this.gameCompletionLogMapper = gameCompletionLogMapper;
this.statusHistoryMapper = statusHistoryMapper;
}
/**
@@ -93,7 +102,7 @@ public class GameCompletionDetectionService {
// 高置信度直接标记完成
return markTasksCompleted(loggedInTasks, machineId, result.points, detectionSource);
} else {
// 中等置信度需要确认
// 中等置信度需要确认:首次记录待确认,等待下一次同状态触发且间隔足够再完成
return scheduleCompletionConfirmation(machineId, loggedInTasks, result, detectionSource);
}
}
@@ -165,19 +174,20 @@ public class GameCompletionDetectionService {
LocalDateTime now = LocalDateTime.now();
LocalDateTime lastPending = pendingCompletions.get(machineId);
if (lastPending != null &&
now.isBefore(lastPending.plusSeconds(COMPLETION_CONFIRMATION_INTERVAL_SECONDS))) {
if (lastPending == null) {
pendingCompletions.put(machineId, now);
log.info("设备{}游戏完成检测待确认,首次记录,等待{}秒后再次确认", machineId, COMPLETION_CONFIRMATION_INTERVAL_SECONDS);
com.gameplatform.server.util.AuditLogger.info("CompletionPending: device={}, source={}, interval={}s", machineId, detectionSource, COMPLETION_CONFIRMATION_INTERVAL_SECONDS);
return false;
}
if (now.isBefore(lastPending.plusSeconds(COMPLETION_CONFIRMATION_INTERVAL_SECONDS))) {
log.debug("设备{}完成确认间隔太短,跳过", machineId);
return false;
}
pendingCompletions.put(machineId, now);
// 延迟确认(这里可以实现定时任务或消息队列)
log.info("设备{}游戏完成检测待确认,将在{}秒后确认", machineId, COMPLETION_CONFIRMATION_INTERVAL_SECONDS);
// 简化实现:直接标记完成(实际应该延迟处理)
return markTasksCompleted(tasks, machineId, result.points, detectionSource + "_CONFIRMED");
// 到达确认时间窗口,进行最终标记
boolean res = markTasksCompleted(tasks, machineId, result.points, detectionSource + "_CONFIRMED");
pendingCompletions.remove(machineId);
return res;
}
/**
@@ -200,10 +210,21 @@ public class GameCompletionDetectionService {
task.setCompletedPoints(0);
}
String prev = task.getStatus();
int updated = linkTaskMapper.update(task);
if (updated > 0) {
log.info("任务{}已标记完成:设备={},点数={},检测来源={}",
task.getCodeNo(), machineId, task.getCompletedPoints(), detectionSource);
com.gameplatform.server.util.AuditLogger.info("StatusTransition: codeNo={}, device={}, {}->COMPLETED, source={}, points={}",
task.getCodeNo(), machineId, prev, detectionSource, task.getCompletedPoints());
try {
if (statusHistoryMapper != null) {
statusHistoryMapper.insert(new LinkTaskStatusHistory(
task.getId(), task.getCodeNo(), machineId, prev, "COMPLETED", detectionSource,
"detectionComplete", null, null
));
}
} catch (Exception ignore) {}
anyCompleted = true;
// 将设备加入冷却队列
@@ -234,12 +255,13 @@ public class GameCompletionDetectionService {
private void recordDetectionLog(Long linkTaskId, String machineId, String detectionSource,
String deviceStatus, Integer points, String confidence) {
try {
GameCompletionLog log = new GameCompletionLog(
GameCompletionLog rec = new GameCompletionLog(
linkTaskId, machineId, detectionSource, deviceStatus, points, confidence);
// 这里应该保存到数据库,简化实现直接记录日志
this.log.info("游戏完成检测日志:{}", log);
if (gameCompletionLogMapper != null) {
gameCompletionLogMapper.insert(rec);
}
com.gameplatform.server.util.AuditLogger.info("CompletionDetect: taskId={}, device={}, source={}, status={}, points={}, confidence={}",
linkTaskId, machineId, detectionSource, deviceStatus, points, confidence);
} catch (Exception e) {
this.log.error("记录游戏完成检测日志失败", e);
}

View File

@@ -4,6 +4,7 @@ import com.gameplatform.server.mapper.agent.LinkTaskMapper;
import com.gameplatform.server.model.entity.agent.LinkTask;
import com.gameplatform.server.service.admin.SystemConfigService;
import com.gameplatform.server.service.external.ScriptClient;
import com.gameplatform.server.service.detection.GameCompletionDetectionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -19,11 +20,13 @@ public class DeviceStatusCheckService {
private final ScriptClient scriptClient;
private final LinkTaskMapper linkTaskMapper;
private final SystemConfigService systemConfigService;
private final GameCompletionDetectionService completionDetectionService;
public DeviceStatusCheckService(ScriptClient scriptClient, LinkTaskMapper linkTaskMapper, SystemConfigService systemConfigService) {
public DeviceStatusCheckService(ScriptClient scriptClient, LinkTaskMapper linkTaskMapper, SystemConfigService systemConfigService, GameCompletionDetectionService completionDetectionService) {
this.scriptClient = scriptClient;
this.linkTaskMapper = linkTaskMapper;
this.systemConfigService = systemConfigService;
this.completionDetectionService = completionDetectionService;
}
/**
@@ -47,9 +50,12 @@ public class DeviceStatusCheckService {
DeviceStatusInfo statusInfo = parseDeviceStatus(deviceStatus);
log.debug("设备 {} 状态解析结果: {}", machineId, statusInfo);
// 3. 如果设备空闲,检查是否有使用该设备的LOGGED_IN状态链接
// 3. 如果设备空闲,通过完成检测服务进行判定(含缓冲与二次确认)
if (statusInfo.isIdle()) {
updateCompletedTasks(machineId, statusInfo.getPoints());
String source = "TIMER_TASK";
com.gameplatform.server.util.AuditLogger.debug("IdleDetected: device={}, source={}, status={}, points={}", machineId, source, statusInfo.getStatus(), statusInfo.getPoints());
// 调用完成检测服务(包含登录缓冲与二次确认)
completionDetectionService.detectGameCompletion(machineId, statusInfo.getStatus(), source);
} else {
log.debug("设备 {} 状态为 [{}],非空闲状态,跳过检查", machineId, statusInfo.getStatus());
}

View File

@@ -13,6 +13,7 @@ import com.gameplatform.server.model.entity.agent.LinkBatch;
import com.gameplatform.server.model.entity.agent.LinkTask;
import com.gameplatform.server.service.external.ScriptClient;
import org.springframework.beans.factory.annotation.Autowired;
import com.gameplatform.server.service.device.DeviceStatusCheckService;
import com.gameplatform.server.service.admin.SystemConfigService;
import com.gameplatform.server.service.cooldown.MachineCooldownService;
@@ -42,6 +43,10 @@ public class LinkStatusService {
private final DeviceStatusCheckService deviceStatusCheckService;
private final SystemConfigService systemConfigService;
private final MachineCooldownService machineCooldownService;
@Autowired(required = false)
private com.gameplatform.server.service.detection.GameCompletionDetectionService completionDetectionService;
@Autowired(required = false)
private com.gameplatform.server.mapper.history.LinkTaskStatusHistoryMapper statusHistoryMapper;
// 状态描述映射
@@ -785,6 +790,7 @@ public class LinkStatusService {
log.info("数据库更新前状态: id={}, status={}, region={}, machineId={}, firstRegionSelectAt={}",
linkTask.getId(), linkTask.getStatus(), linkTask.getRegion(), linkTask.getMachineId(), linkTask.getFirstRegionSelectAt());
String prev = linkTask.getStatus();
linkTask.setStatus("USING");
linkTask.setRegion(region);
linkTask.setCodeNo(code);
@@ -799,6 +805,15 @@ public class LinkStatusService {
linkTask.setMachineId(selectedDeviceId);
linkTaskMapper.update(linkTask);
// Audit and history for USING transition
try {
com.gameplatform.server.util.AuditLogger.info("StatusTransition: codeNo={}, device={}, {}->USING, source=REGION_SELECT", linkTask.getCodeNo(), selectedDeviceId, prev);
if (statusHistoryMapper != null) {
statusHistoryMapper.insert(new com.gameplatform.server.model.entity.history.LinkTaskStatusHistory(
linkTask.getId(), linkTask.getCodeNo(), selectedDeviceId, prev, "USING", "REGION_SELECT", "regionSelected", null, null
));
}
} catch (Exception ignore) {}
log.info("数据库更新成功: id={}, status=USING, region={}, machineId={}, qrCreatedAt={}, qrExpireAt={}, firstRegionSelectAt={}",
linkTask.getId(), region, selectedDeviceId, now, qrExpireAt, linkTask.getFirstRegionSelectAt());
@@ -954,6 +969,21 @@ public class LinkStatusService {
linkTask.setUpdatedAt(LocalDateTime.now());
linkTaskMapper.updateById(linkTask);
// Record login buffer and audit
try {
if (completionDetectionService != null) {
completionDetectionService.recordDeviceLogin(deviceId);
}
com.gameplatform.server.util.AuditLogger.info("DeviceLogin: codeNo={}, device={}, status=LOGGED_IN", linkTask.getCodeNo(), deviceId);
if (statusHistoryMapper != null) {
statusHistoryMapper.insert(new com.gameplatform.server.model.entity.history.LinkTaskStatusHistory(
linkTask.getId(), linkTask.getCodeNo(), deviceId, "USING", "LOGGED_IN", "POLL_LOGIN", "loginDetected", 0, null
));
}
} catch (Exception e) {
log.warn("记录设备登录缓冲/审计失败", e);
}
// 记录设备登录时间(用于完成检测的缓冲期判断)
try {
// 这里需要注入 GameCompletionDetectionService为了兼容性暂时记录日志

View File

@@ -0,0 +1,23 @@
package com.gameplatform.server.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class AuditLogger {
private static final Logger AUDIT = LoggerFactory.getLogger("com.gameplatform.server.audit");
private AuditLogger() {}
public static void info(String fmt, Object... args) {
AUDIT.info(fmt, args);
}
public static void warn(String fmt, Object... args) {
AUDIT.warn(fmt, args);
}
public static void debug(String fmt, Object... args) {
AUDIT.debug(fmt, args);
}
}

View File

@@ -0,0 +1,36 @@
-- Game completion detection log table
CREATE TABLE IF NOT EXISTS game_completion_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
link_task_id BIGINT,
machine_id VARCHAR(64),
detection_source VARCHAR(32),
device_status VARCHAR(64),
points_detected INT NULL,
completion_confidence VARCHAR(16),
is_confirmed TINYINT(1) DEFAULT 0,
confirmation_time DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_gcl_task (link_task_id),
INDEX idx_gcl_machine (machine_id),
INDEX idx_gcl_created (created_at)
);
-- Link task status transition history
CREATE TABLE IF NOT EXISTS link_task_status_history (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
link_task_id BIGINT,
code_no VARCHAR(64),
machine_id VARCHAR(64),
prev_status VARCHAR(32),
new_status VARCHAR(32),
source VARCHAR(32),
reason VARCHAR(128),
since_login_seconds INT NULL,
extra_json TEXT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ltsh_task (link_task_id),
INDEX idx_ltsh_code (code_no),
INDEX idx_ltsh_machine (machine_id),
INDEX idx_ltsh_created (created_at)
);

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true">
<property name="LOG_PATH" value="logs"/>
<appender name="AUDIT-FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/audit-status.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/audit-status.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>14</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- Dedicated logger for status audit -->
<logger name="com.gameplatform.server.audit" additivity="false" level="INFO">
<appender-ref ref="AUDIT-FILE"/>
</logger>
<!-- Let Spring Boot default console/file config handle others -->
</configuration>