From 86140b1294826c4d8de3256e089a32496e9639af Mon Sep 17 00:00:00 2001 From: zyh Date: Tue, 9 Sep 2025 20:57:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=E6=B8=B8=E6=88=8F?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=A3=80=E6=B5=8B=E6=9C=8D=E5=8A=A1=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AE=BE=E5=A4=87=E7=8A=B6=E6=80=81=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E5=92=8C=E4=BB=BB=E5=8A=A1=E5=AE=8C=E6=88=90=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detection/GameCompletionLogMapper.java | 10 +++ .../history/LinkTaskStatusHistoryMapper.java | 10 +++ .../entity/history/LinkTaskStatusHistory.java | 68 +++++++++++++++++++ .../GameCompletionDetectionService.java | 56 ++++++++++----- .../device/DeviceStatusCheckService.java | 12 +++- .../service/link/LinkStatusService.java | 30 ++++++++ .../gameplatform/server/util/AuditLogger.java | 23 +++++++ .../db_migration_completion_and_history.sql | 36 ++++++++++ src/main/resources/logback-spring.xml | 28 ++++++++ 9 files changed, 253 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/gameplatform/server/mapper/detection/GameCompletionLogMapper.java create mode 100644 src/main/java/com/gameplatform/server/mapper/history/LinkTaskStatusHistoryMapper.java create mode 100644 src/main/java/com/gameplatform/server/model/entity/history/LinkTaskStatusHistory.java create mode 100644 src/main/java/com/gameplatform/server/util/AuditLogger.java create mode 100644 src/main/resources/db_migration_completion_and_history.sql create mode 100644 src/main/resources/logback-spring.xml diff --git a/src/main/java/com/gameplatform/server/mapper/detection/GameCompletionLogMapper.java b/src/main/java/com/gameplatform/server/mapper/detection/GameCompletionLogMapper.java new file mode 100644 index 0000000..281081c --- /dev/null +++ b/src/main/java/com/gameplatform/server/mapper/detection/GameCompletionLogMapper.java @@ -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 { +} + diff --git a/src/main/java/com/gameplatform/server/mapper/history/LinkTaskStatusHistoryMapper.java b/src/main/java/com/gameplatform/server/mapper/history/LinkTaskStatusHistoryMapper.java new file mode 100644 index 0000000..a5a84b1 --- /dev/null +++ b/src/main/java/com/gameplatform/server/mapper/history/LinkTaskStatusHistoryMapper.java @@ -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 { +} + diff --git a/src/main/java/com/gameplatform/server/model/entity/history/LinkTaskStatusHistory.java b/src/main/java/com/gameplatform/server/model/entity/history/LinkTaskStatusHistory.java new file mode 100644 index 0000000..f942b9e --- /dev/null +++ b/src/main/java/com/gameplatform/server/model/entity/history/LinkTaskStatusHistory.java @@ -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; } +} + diff --git a/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java b/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java index fe869c6..c316bfc 100644 --- a/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java +++ b/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java @@ -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 pendingCompletions = new ConcurrentHashMap<>(); @@ -43,9 +48,13 @@ public class GameCompletionDetectionService { private final ConcurrentMap 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); } diff --git a/src/main/java/com/gameplatform/server/service/device/DeviceStatusCheckService.java b/src/main/java/com/gameplatform/server/service/device/DeviceStatusCheckService.java index 6a6c13f..43d08b9 100644 --- a/src/main/java/com/gameplatform/server/service/device/DeviceStatusCheckService.java +++ b/src/main/java/com/gameplatform/server/service/device/DeviceStatusCheckService.java @@ -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()); } diff --git a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java index 0d7969a..4e04436 100644 --- a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java +++ b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java @@ -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,为了兼容性,暂时记录日志 diff --git a/src/main/java/com/gameplatform/server/util/AuditLogger.java b/src/main/java/com/gameplatform/server/util/AuditLogger.java new file mode 100644 index 0000000..b65cd13 --- /dev/null +++ b/src/main/java/com/gameplatform/server/util/AuditLogger.java @@ -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); + } +} + diff --git a/src/main/resources/db_migration_completion_and_history.sql b/src/main/resources/db_migration_completion_and_history.sql new file mode 100644 index 0000000..734abf1 --- /dev/null +++ b/src/main/resources/db_migration_completion_and_history.sql @@ -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) +); + diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..fdaeccc --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,28 @@ + + + + + + ${LOG_PATH}/audit-status.log + + ${LOG_PATH}/audit-status.%d{yyyy-MM-dd}.%i.log.gz + + 10MB + + 14 + 1GB + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + UTF-8 + + + + + + + + + + +