feat: 集成游戏完成检测服务,优化设备状态检查和任务完成确认逻辑
This commit is contained in:
@@ -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> {
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,为了兼容性,暂时记录日志
|
||||
|
||||
23
src/main/java/com/gameplatform/server/util/AuditLogger.java
Normal file
23
src/main/java/com/gameplatform/server/util/AuditLogger.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
36
src/main/resources/db_migration_completion_and_history.sql
Normal file
36
src/main/resources/db_migration_completion_and_history.sql
Normal 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)
|
||||
);
|
||||
|
||||
28
src/main/resources/logback-spring.xml
Normal file
28
src/main/resources/logback-spring.xml
Normal 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>
|
||||
|
||||
Reference in New Issue
Block a user