diff --git a/OPTIMIZATION_IMPLEMENTATION_GUIDE.md b/OPTIMIZATION_IMPLEMENTATION_GUIDE.md index 4488319..498f15e 100644 --- a/OPTIMIZATION_IMPLEMENTATION_GUIDE.md +++ b/OPTIMIZATION_IMPLEMENTATION_GUIDE.md @@ -303,3 +303,4 @@ WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY); 4. 说明当前系统状态 这样可以更快速地定位和解决问题。 + diff --git a/database_improvements.sql b/database_improvements.sql index c2a2938..a78ee5b 100644 --- a/database_improvements.sql +++ b/database_improvements.sql @@ -96,3 +96,4 @@ CREATE TABLE `system_monitor` ( ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统监控表' ROW_FORMAT = DYNAMIC; SET FOREIGN_KEY_CHECKS = 1; + diff --git a/src/main/java/com/gameplatform/server/GamePlatformServerApplication.java b/src/main/java/com/gameplatform/server/GamePlatformServerApplication.java index 6d73196..fb2f52b 100644 --- a/src/main/java/com/gameplatform/server/GamePlatformServerApplication.java +++ b/src/main/java/com/gameplatform/server/GamePlatformServerApplication.java @@ -1,6 +1,8 @@ package com.gameplatform.server; import org.mybatis.spring.annotation.MapperScan; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; @@ -9,8 +11,13 @@ import org.springframework.scheduling.annotation.EnableScheduling; @MapperScan("com.gameplatform.server.mapper") @EnableScheduling public class GamePlatformServerApplication { + private static final Logger log = LoggerFactory.getLogger(GamePlatformServerApplication.class); + public static void main(String[] args) { + log.info("=== 游戏平台服务器启动中 ==="); + log.debug("Debug 日志级别已启用"); SpringApplication.run(GamePlatformServerApplication.class, args); + log.info("=== 游戏平台服务器启动完成 ==="); } } diff --git a/src/main/java/com/gameplatform/server/controller/auth/AuthController.java b/src/main/java/com/gameplatform/server/controller/auth/AuthController.java index 1ac17b1..127a050 100644 --- a/src/main/java/com/gameplatform/server/controller/auth/AuthController.java +++ b/src/main/java/com/gameplatform/server/controller/auth/AuthController.java @@ -29,7 +29,7 @@ public class AuthController { @ResponseStatus(HttpStatus.OK) public Mono login(@Valid @RequestBody LoginRequest req) { // Avoid logging raw usernames at info level - log.debug("/api/auth/login called"); + log.info("/api/auth/login called"); return authService.login(req); } diff --git a/src/main/java/com/gameplatform/server/controller/link/LinkController.java b/src/main/java/com/gameplatform/server/controller/link/LinkController.java index a388b62..0e611b6 100644 --- a/src/main/java/com/gameplatform/server/controller/link/LinkController.java +++ b/src/main/java/com/gameplatform/server/controller/link/LinkController.java @@ -15,8 +15,8 @@ import com.gameplatform.server.model.dto.link.UserLinkStatusResponse; import com.gameplatform.server.model.dto.link.TargetScoreResponse; import com.gameplatform.server.service.link.LinkGenerationService; import com.gameplatform.server.service.link.LinkListService; -import com.gameplatform.server.service.link.LinkStatusService; import com.gameplatform.server.service.external.ScriptClient; +import com.gameplatform.server.service.link.LinkStatusService; import io.jsonwebtoken.Claims; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/src/main/java/com/gameplatform/server/mapper/agent/LinkTaskMapper.java b/src/main/java/com/gameplatform/server/mapper/agent/LinkTaskMapper.java index 8655889..48ac159 100644 --- a/src/main/java/com/gameplatform/server/mapper/agent/LinkTaskMapper.java +++ b/src/main/java/com/gameplatform/server/mapper/agent/LinkTaskMapper.java @@ -115,4 +115,14 @@ public interface LinkTaskMapper extends BaseMapper { * 根据状态查询所有链接任务 */ List findByStatus(@Param("status") String status); + + /** + * 原子方式占用设备:仅当该设备当前未被 USING/LOGGED_IN 占用时, + * 才将指定任务更新为 USING 并写入设备与时间字段。 + * 返回受影响行数(1=成功,0=设备已被占用)。 + */ + int reserveDeviceIfFree(@Param("id") Long id, + @Param("region") String region, + @Param("deviceId") String deviceId, + @Param("qrExpireSeconds") int qrExpireSeconds); } diff --git a/src/main/java/com/gameplatform/server/mapper/cooldown/MachineCooldownMapper.java b/src/main/java/com/gameplatform/server/mapper/cooldown/MachineCooldownMapper.java index 3f0513f..86ef0f1 100644 --- a/src/main/java/com/gameplatform/server/mapper/cooldown/MachineCooldownMapper.java +++ b/src/main/java/com/gameplatform/server/mapper/cooldown/MachineCooldownMapper.java @@ -61,6 +61,12 @@ public interface MachineCooldownMapper extends BaseMapper { * 清理指定时间之前的已过期冷却记录 */ int cleanupExpiredCooldowns(@Param("beforeTime") LocalDateTime beforeTime); + + /** + * 删除将要过期(ACTIVE→EXPIRED)的设备,已存在的 EXPIRED 记录,避免唯一键 (machine_id,status) 冲突。 + * 使用当前时间参数筛选出需要过期的设备列表。 + */ + int deleteExistingExpiredForMachinesToExpire(@Param("currentTime") LocalDateTime currentTime); /** * 获取指定设备的冷却历史记录 @@ -69,3 +75,4 @@ public interface MachineCooldownMapper extends BaseMapper { @Param("limit") int limit, @Param("offset") int offset); } + diff --git a/src/main/java/com/gameplatform/server/model/entity/cooldown/MachineCooldown.java b/src/main/java/com/gameplatform/server/model/entity/cooldown/MachineCooldown.java index c257de9..d7bb59e 100644 --- a/src/main/java/com/gameplatform/server/model/entity/cooldown/MachineCooldown.java +++ b/src/main/java/com/gameplatform/server/model/entity/cooldown/MachineCooldown.java @@ -182,3 +182,4 @@ public class MachineCooldown { '}'; } } + diff --git a/src/main/java/com/gameplatform/server/model/entity/detection/GameCompletionLog.java b/src/main/java/com/gameplatform/server/model/entity/detection/GameCompletionLog.java index ed0ceb7..3674c3e 100644 --- a/src/main/java/com/gameplatform/server/model/entity/detection/GameCompletionLog.java +++ b/src/main/java/com/gameplatform/server/model/entity/detection/GameCompletionLog.java @@ -197,3 +197,4 @@ public class GameCompletionLog { '}'; } } + 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 c316bfc..ade2eff 100644 --- a/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java +++ b/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java @@ -3,7 +3,7 @@ package com.gameplatform.server.service.detection; 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.service.cooldown.MemoryMachineCooldownService; import com.gameplatform.server.mapper.detection.GameCompletionLogMapper; import com.gameplatform.server.mapper.history.LinkTaskStatusHistoryMapper; import com.gameplatform.server.model.entity.history.LinkTaskStatusHistory; @@ -37,7 +37,7 @@ public class GameCompletionDetectionService { private static final int COMPLETION_CONFIRMATION_INTERVAL_SECONDS = 10; private final LinkTaskMapper linkTaskMapper; - private final MachineCooldownService machineCooldownService; + private final MemoryMachineCooldownService machineCooldownService; private final GameCompletionLogMapper gameCompletionLogMapper; private final LinkTaskStatusHistoryMapper statusHistoryMapper; @@ -48,7 +48,7 @@ public class GameCompletionDetectionService { private final ConcurrentMap recentLogins = new ConcurrentHashMap<>(); public GameCompletionDetectionService(LinkTaskMapper linkTaskMapper, - MachineCooldownService machineCooldownService, + MemoryMachineCooldownService machineCooldownService, GameCompletionLogMapper gameCompletionLogMapper, LinkTaskStatusHistoryMapper statusHistoryMapper) { this.linkTaskMapper = linkTaskMapper; @@ -297,3 +297,4 @@ public class GameCompletionDetectionService { LOW // 低置信度:不可信的状态变化 } } + 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 4e04436..aca876c 100644 --- a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java +++ b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java @@ -16,7 +16,7 @@ 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; +import com.gameplatform.server.service.cooldown.MemoryMachineCooldownService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -42,13 +42,13 @@ public class LinkStatusService { private final ScriptClient scriptClient; private final DeviceStatusCheckService deviceStatusCheckService; private final SystemConfigService systemConfigService; - private final MachineCooldownService machineCooldownService; + private final MemoryMachineCooldownService machineCooldownService; + private final DeviceAllocationService deviceAllocationService; @Autowired(required = false) private com.gameplatform.server.service.detection.GameCompletionDetectionService completionDetectionService; @Autowired(required = false) private com.gameplatform.server.mapper.history.LinkTaskStatusHistoryMapper statusHistoryMapper; - // 状态描述映射 private static final Map STATUS_DESC_MAP = new HashMap<>(); static { @@ -62,13 +62,14 @@ public class LinkStatusService { public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper, ScriptClient scriptClient, - DeviceStatusCheckService deviceStatusCheckService, SystemConfigService systemConfigService, MachineCooldownService machineCooldownService) { + DeviceStatusCheckService deviceStatusCheckService, SystemConfigService systemConfigService, MemoryMachineCooldownService machineCooldownService, DeviceAllocationService deviceAllocationService) { this.linkTaskMapper = linkTaskMapper; this.linkBatchMapper = linkBatchMapper; this.scriptClient = scriptClient; this.deviceStatusCheckService = deviceStatusCheckService; this.systemConfigService = systemConfigService; this.machineCooldownService = machineCooldownService; + this.deviceAllocationService = deviceAllocationService; } /** @@ -89,13 +90,13 @@ public class LinkStatusService { // 查询链接任务 LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo); if (linkTask == null) { - throw new IllegalArgumentException("链接不存在: " + codeNo); + throw new IllegalArgumentException("链接不存在 " + codeNo); } // 查询批次信息 LinkBatch linkBatch = linkBatchMapper.findById(linkTask.getBatchId()); if (linkBatch == null) { - throw new IllegalStateException("批次信息不存在: batchId=" + linkTask.getBatchId()); + throw new IllegalStateException("批次信息不存在 batchId=" + linkTask.getBatchId()); } // 构建响应对象 @@ -125,7 +126,7 @@ public class LinkStatusService { response.setRemainingSeconds(0L); } - log.debug("链接状态查询完成: codeNo={}, status={}, isExpired={}, remainingSeconds={}", + log.debug("链接状态查询完成 codeNo={}, status={}, isExpired={}, remainingSeconds={}", codeNo, linkTask.getStatus(), isExpired, response.getRemainingSeconds()); return response; @@ -169,21 +170,21 @@ public class LinkStatusService { private Boolean doRefundOrder(String codeNo, Long agentId) { log.info("=== 开始处理退单操作 ==="); log.info("链接编号: {}, 代理ID: {}", codeNo, agentId); - + try { // 1. 查询链接任务 LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo); if (linkTask == null) { - log.error("链接任务不存在: codeNo={}", codeNo); + log.error("链接任务不存在 codeNo={}", codeNo); throw new IllegalArgumentException("链接不存在"); } - + // 2. 验证代理ID(确保用户只能退单自己的链接) if (!linkTask.getAgentId().equals(agentId)) { log.error("权限不足: 用户{}无权退单链接{}", agentId, codeNo); throw new IllegalArgumentException("权限不足:您无权操作此链接"); } - + // 3. 检查链接状态是否允许退单 String currentStatus = linkTask.getStatus(); if ("REFUNDED".equals(currentStatus)) { @@ -195,12 +196,12 @@ public class LinkStatusService { log.warn("过期链接不允许退单: codeNo={}, status={}", codeNo, currentStatus); throw new IllegalStateException("过期链接不允许退单"); } - + if ("COMPLETED".equals(currentStatus)) { log.warn("已完成链接不允许退单: codeNo={}, status={}", codeNo, currentStatus); throw new IllegalStateException("已完成链接不允许退单"); } - + // 4. 如果链接有关联的设备,调用脚本端退单接口 String machineId = linkTask.getMachineId(); if (machineId != null && !machineId.trim().isEmpty()) { @@ -209,31 +210,31 @@ public class LinkStatusService { try { // 同步调用脚本端退单接口 String refundResult = scriptClient.refundOrder(machineId).block(); - log.info("脚本端退单接口调用成功: 设备={}, 结果={}", machineId, refundResult); + log.info("脚本端退单接口调用成功, 设备={}, 结果={}", machineId, refundResult); } catch (Exception e) { - log.error("脚本端退单接口调用失败: 设备={}, 错误={}", machineId, e.getMessage()); + log.error("脚本端退单接口调用失败, 设备={}, 错误={}", machineId, e.getMessage()); // 即使脚本端调用失败,我们仍然继续更新数据库状态 // 这样可以确保用户能够看到退单状态,避免重复退单 } } else { log.info("链接未关联设备,跳过脚本端退单调用"); } - - // 5. 更新链接状态为REFUNDED + + // 5. 更新链接状态为 REFUNDED linkTask.setStatus("REFUNDED"); linkTask.setRefundAt(LocalDateTime.now()); linkTask.setUpdatedAt(LocalDateTime.now()); - int updateResult = linkTaskMapper.update(linkTask); + int updateResult = linkTaskMapper.updateById(linkTask); if (updateResult <= 0) { - log.error("更新链接状态失败: codeNo={}", codeNo); + log.error("更新链接状态失败 codeNo={}", codeNo); throw new RuntimeException("更新链接状态失败"); } - - log.info("退单操作成功: codeNo={}, 设备={}, 状态更新为REFUNDED", codeNo, machineId); + + log.info("退单操作成功: codeNo={}, 设备={}, 状态更新为 REFUNDED", codeNo, machineId); log.info("=== 退单操作完成 ==="); return true; - + } catch (Exception e) { log.error("=== 退单操作失败 ==="); log.error("codeNo={}, agentId={}, 错误详情: {}", codeNo, agentId, e.getMessage(), e); @@ -241,265 +242,209 @@ public class LinkStatusService { } } - /** - * 删除链接(确保用户只能删除自己的链接) - */ - public Mono deleteLink(String codeNo, Long agentId) { - return Mono.fromCallable(() -> { - log.info("开始删除链接: codeNo={}, agentId={}", codeNo, agentId); - - // 首先检查链接是否存在且属于该用户 - LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo); - if (linkTask == null) { - log.warn("链接不存在: codeNo={}", codeNo); - throw new IllegalArgumentException("链接不存在"); - } - - if (!linkTask.getAgentId().equals(agentId)) { - log.warn("用户无权删除该链接: codeNo={}, linkOwner={}, requestUser={}", - codeNo, linkTask.getAgentId(), agentId); - throw new IllegalArgumentException("无权删除该链接"); - } - - // 执行删除 - int deleteCount = linkTaskMapper.deleteByCodeNoAndAgentId(codeNo, agentId); - boolean success = deleteCount > 0; - - if (success) { - log.info("链接删除成功: codeNo={}, agentId={}", codeNo, agentId); - } else { - log.warn("链接删除失败: codeNo={}, agentId={}", codeNo, agentId); - } - - return success; - }).subscribeOn(Schedulers.boundedElastic()); - } + /** + * 删除链接(确保用户只能删除自己的链接) + */ +public Mono deleteLink(String codeNo, Long agentId) { + return Mono.fromCallable(() -> { + log.info("开始删除链接: codeNo={}, agentId={}", codeNo, agentId); + + // 首先检查链接是否存在且属于该用户 + LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo); + if (linkTask == null) { + log.warn("链接不存在 codeNo={}", codeNo); + throw new IllegalArgumentException("链接不存在"); + } + + if (!linkTask.getAgentId().equals(agentId)) { + log.warn("用户无权删除该链接: codeNo={}, linkOwner={}, requestUser={}", + codeNo, linkTask.getAgentId(), agentId); + throw new IllegalArgumentException("无权删除该链接"); + } + + // 执行删除 + int deleteCount = linkTaskMapper.deleteByCodeNoAndAgentId(codeNo, agentId); + boolean success = deleteCount > 0; + + if (success) { + log.info("链接删除成功: codeNo={}, agentId={}", codeNo, agentId); + } else { + log.warn("链接删除失败: codeNo={}, agentId={}", codeNo, agentId); + } + + return success; + }).subscribeOn(Schedulers.boundedElastic()); +} - /** - * 按状态批量删除链接(确保用户只能删除自己的链接) - */ - @Transactional(rollbackFor = Exception.class) - public Mono batchDeleteLinksByStatus(List statusList, Long agentId) { - return Mono.fromCallable(() -> { - log.info("开始按状态批量删除链接: statusList={}, agentId={}", statusList, agentId); - - if (statusList == null || statusList.isEmpty()) { - throw new IllegalArgumentException("要删除的状态列表不能为空"); +/** + * 按状态批量删除链接(确保用户只能删除自己的链接) + */ +@Transactional(rollbackFor = Exception.class) +public Mono batchDeleteLinksByStatus(List statusList, Long agentId) { + return Mono.fromCallable(() -> { + log.info("开始按状态批量删除链接: statusList={}, agentId={}", statusList, agentId); + + if (statusList == null || statusList.isEmpty()) { + throw new IllegalArgumentException("要删除的状态列表不能为空"); + } + + if (statusList.size() > 10) { + throw new IllegalArgumentException("单次最多只能指定10个状态"); + } + + // 验证状态值是否有效 + List validStatuses = List.of("NEW", "USING", "LOGGED_IN", "COMPLETED", "REFUNDED", "EXPIRED"); + for (String status : statusList) { + if (!validStatuses.contains(status)) { + throw new IllegalArgumentException("无效的状态值: " + status); } - - if (statusList.size() > 10) { - throw new IllegalArgumentException("单次最多只能指定10个状态"); + } + + // 统计要删除的链接数量 + long totalCount = linkTaskMapper.countByStatusListAndAgentId(statusList, agentId); + log.info("用户拥有的指定状态链接数量: {}", totalCount); + + if (totalCount == 0) { + // 没有符合条件的链接 + BatchDeleteResponse response = new BatchDeleteResponse(0, 0, 0, + List.of(), List.of(), List.of()); + log.info("没有找到符合条件的链接"); + return response; + } + + // 查询要删除的链接详情(用于记录日志) + List linksToDelete = linkTaskMapper.findByStatusListAndAgentId(statusList, agentId); + List codeNosToDelete = linksToDelete.stream() + .map(LinkTask::getCodeNo) + .collect(Collectors.toList()); + + log.info("即将删除的链接: {}", codeNosToDelete); + + // 执行批量删除 + int deleteCount = linkTaskMapper.batchDeleteByStatusListAndAgentId(statusList, agentId); + log.info("批量删除执行结果: 预期删除数量={}, 实际删除数量={}", totalCount, deleteCount); + + // 构建响应 + List successCodeNos = new ArrayList<>(); + List failedCodeNos = new ArrayList<>(); + List failedReasons = new ArrayList<>(); + + if (deleteCount == totalCount) { + // 全部删除成功 + successCodeNos.addAll(codeNosToDelete); + } else if (deleteCount > 0) { + // 部分删除成功(这种情况比较少见,可能是并发操作导致) + successCodeNos.addAll(codeNosToDelete.subList(0, deleteCount)); + failedCodeNos.addAll(codeNosToDelete.subList(deleteCount, codeNosToDelete.size())); + for (int i = deleteCount; i < codeNosToDelete.size(); i++) { + failedReasons.add("删除操作部分失败"); } - - // 验证状态值是否有效 - List validStatuses = List.of("NEW", "USING", "LOGGED_IN", "COMPLETED", "REFUNDED", "EXPIRED"); - for (String status : statusList) { - if (!validStatuses.contains(status)) { - throw new IllegalArgumentException("无效的状态值: " + status); - } + } else { + // 全部删除失败 + failedCodeNos.addAll(codeNosToDelete); + for (int i = 0; i < codeNosToDelete.size(); i++) { + failedReasons.add("删除操作失败"); } - - // 统计要删除的链接数量 - long totalCount = linkTaskMapper.countByStatusListAndAgentId(statusList, agentId); - log.info("用户拥有的指定状态链接数量: {}", totalCount); - - if (totalCount == 0) { - // 没有符合条件的链接 - BatchDeleteResponse response = new BatchDeleteResponse(0, 0, 0, - List.of(), List.of(), List.of()); - log.info("没有找到符合条件的链接"); - return response; + } + + BatchDeleteResponse response = new BatchDeleteResponse( + successCodeNos.size(), + failedCodeNos.size(), + codeNosToDelete.size(), + successCodeNos, + failedCodeNos, + failedReasons + ); + + log.info("按状态批量删除完成: 总数={}, 成功={}, 失败={}", + response.getTotalCount(), response.getSuccessCount(), response.getFailedCount()); + + return response; + }).subscribeOn(Schedulers.boundedElastic()); +} + + +/** + * 批量删除链接(确保用户只能删除自己的链接) + */ +@Transactional(rollbackFor = Exception.class) +public Mono batchDeleteLinks(List codeNos, Long agentId) { + return Mono.fromCallable(() -> { + log.info("开始批量删除链接: codeNos={}, agentId={}, count={}", codeNos, agentId, codeNos.size()); + + if (codeNos == null || codeNos.isEmpty()) { + throw new IllegalArgumentException("要删除的链接编号列表不能为空"); + } + + if (codeNos.size() > 100) { + throw new IllegalArgumentException("单次最多只能删除100个链接"); + } + + // 查询用户拥有的链接 + List userLinks = linkTaskMapper.findByCodeNosAndAgentId(codeNos, agentId); + List userCodeNos = userLinks.stream() + .map(LinkTask::getCodeNo) + .collect(Collectors.toList()); + + log.info("用户拥有的链接数量: {}, 链接编号: {}", userCodeNos.size(), userCodeNos); + + // 准备响应数据 + List successCodeNos = new ArrayList<>(); + List failedCodeNos = new ArrayList<>(); + List failedReasons = new ArrayList<>(); + + // 检查每个链接的权限 + for (String codeNo : codeNos) { + if (!userCodeNos.contains(codeNo)) { + failedCodeNos.add(codeNo); + failedReasons.add("链接不存在或无权删除"); + log.warn("用户无权删除链接: codeNo={}, agentId={}", codeNo, agentId); } - - // 查询要删除的链接详情(用于记录日志) - List linksToDelete = linkTaskMapper.findByStatusListAndAgentId(statusList, agentId); - List codeNosToDelete = linksToDelete.stream() - .map(LinkTask::getCodeNo) - .collect(Collectors.toList()); - - log.info("即将删除的链接: {}", codeNosToDelete); - - // 执行批量删除 - int deleteCount = linkTaskMapper.batchDeleteByStatusListAndAgentId(statusList, agentId); - log.info("批量删除执行结果: 预期删除数量={}, 实际删除数量={}", totalCount, deleteCount); - - // 构建响应 - List successCodeNos = new ArrayList<>(); - List failedCodeNos = new ArrayList<>(); - List failedReasons = new ArrayList<>(); - - if (deleteCount == totalCount) { - // 全部删除成功 - successCodeNos.addAll(codeNosToDelete); - } else if (deleteCount > 0) { - // 部分删除成功(这种情况比较少见,可能是并发操作导致) - successCodeNos.addAll(codeNosToDelete.subList(0, deleteCount)); - failedCodeNos.addAll(codeNosToDelete.subList(deleteCount, codeNosToDelete.size())); - for (int i = deleteCount; i < codeNosToDelete.size(); i++) { - failedReasons.add("删除操作部分失败"); - } + } + + // 执行批量删除(只删除用户拥有的链接) + int deleteCount = 0; + if (!userCodeNos.isEmpty()) { + deleteCount = linkTaskMapper.batchDeleteByCodeNosAndAgentId(userCodeNos, agentId); + log.info("批量删除执行结果: 预期删除数量={}, 实际删除数量={}", userCodeNos.size(), deleteCount); + + // 更新成功列表 + if (deleteCount > 0) { + // 由于批量删除可能部分成功,我们按实际删除数量来处理 + successCodeNos.addAll(userCodeNos); + log.info("批量删除成功的链接: {}", successCodeNos); } else { - // 全部删除失败 - failedCodeNos.addAll(codeNosToDelete); - for (int i = 0; i < codeNosToDelete.size(); i++) { + // 如果删除失败,将所有用户拥有的链接标记为失败 + failedCodeNos.addAll(userCodeNos); + for (int i = 0; i < userCodeNos.size(); i++) { failedReasons.add("删除操作失败"); } + log.warn("批量删除失败: agentId={}, codeNos={}", agentId, userCodeNos); } - - BatchDeleteResponse response = new BatchDeleteResponse( - successCodeNos.size(), - failedCodeNos.size(), - codeNosToDelete.size(), - successCodeNos, - failedCodeNos, - failedReasons - ); - - log.info("按状态批量删除完成: 总数={}, 成功={}, 失败={}", - response.getTotalCount(), response.getSuccessCount(), response.getFailedCount()); - - return response; - }).subscribeOn(Schedulers.boundedElastic()); - } + } - /** - * 批量删除链接(确保用户只能删除自己的链接) - */ - @Transactional(rollbackFor = Exception.class) - public Mono batchDeleteLinks(List codeNos, Long agentId) { - return Mono.fromCallable(() -> { - log.info("开始批量删除链接: codeNos={}, agentId={}, count={}", codeNos, agentId, codeNos.size()); - - if (codeNos == null || codeNos.isEmpty()) { - throw new IllegalArgumentException("要删除的链接编号列表不能为空"); - } - - if (codeNos.size() > 100) { - throw new IllegalArgumentException("单次最多只能删除100个链接"); - } - - // 查询用户拥有的链接 - List userLinks = linkTaskMapper.findByCodeNosAndAgentId(codeNos, agentId); - List userCodeNos = userLinks.stream() - .map(LinkTask::getCodeNo) - .collect(Collectors.toList()); - - log.info("用户拥有的链接数量: {}, 链接编号: {}", userCodeNos.size(), userCodeNos); - - // 准备响应数据 - List successCodeNos = new ArrayList<>(); - List failedCodeNos = new ArrayList<>(); - List failedReasons = new ArrayList<>(); - - // 检查每个链接的权限 - for (String codeNo : codeNos) { - if (!userCodeNos.contains(codeNo)) { - failedCodeNos.add(codeNo); - failedReasons.add("链接不存在或无权删除"); - log.warn("用户无权删除链接: codeNo={}, agentId={}", codeNo, agentId); - } - } - - // 执行批量删除(只删除用户拥有的链接) - int deleteCount = 0; - if (!userCodeNos.isEmpty()) { - deleteCount = linkTaskMapper.batchDeleteByCodeNosAndAgentId(userCodeNos, agentId); - log.info("批量删除执行结果: 预期删除数量={}, 实际删除数量={}", userCodeNos.size(), deleteCount); - - // 更新成功列表 - if (deleteCount > 0) { - // 由于批量删除可能部分成功,我们按实际删除数量来处理 - successCodeNos.addAll(userCodeNos); - log.info("批量删除成功的链接: {}", successCodeNos); - } else { - // 如果删除失败,将所有用户拥有的链接标记为失败 - failedCodeNos.addAll(userCodeNos); - for (int i = 0; i < userCodeNos.size(); i++) { - failedReasons.add("删除操作失败"); - } - log.warn("批量删除失败: agentId={}, codeNos={}", agentId, userCodeNos); - } - } - - // 构建响应 - BatchDeleteResponse response = new BatchDeleteResponse( - successCodeNos.size(), - failedCodeNos.size(), - codeNos.size(), - successCodeNos, - failedCodeNos, - failedReasons - ); - - log.info("批量删除完成: 总数={}, 成功={}, 失败={}", - response.getTotalCount(), response.getSuccessCount(), response.getFailedCount()); - - return response; - }).subscribeOn(Schedulers.boundedElastic()); - } + // 构建响应 + BatchDeleteResponse response = new BatchDeleteResponse( + successCodeNos.size(), + failedCodeNos.size(), + codeNos.size(), + successCodeNos, + failedCodeNos, + failedReasons + ); - /** - * 用户端获取链接状态(支持linkId或codeNo参数,带自动刷新逻辑) - */ - public Mono getUserLinkStatus(Long linkId, String codeNo) { - return Mono.fromCallable(() -> { - log.info("=== 开始处理用户端链接状态查询 ==="); - log.info("linkId: {}, codeNo: {}", linkId, codeNo); + log.info("批量删除完成: 总数={}, 成功={}, 失败={}", + response.getTotalCount(), response.getSuccessCount(), response.getFailedCount()); - try { - // 1. 查询链接任务 - LinkTask linkTask = null; - if (linkId != null) { - linkTask = linkTaskMapper.findById(linkId); - log.info("通过linkId查询链接任务: id={}", linkId); - } else if (codeNo != null && !codeNo.trim().isEmpty()) { - linkTask = linkTaskMapper.findByCodeNo(codeNo.trim()); - log.info("通过codeNo查询链接任务: codeNo={}", codeNo); - } - - if (linkTask == null) { - log.error("链接任务不存在: linkId={}, codeNo={}", linkId, codeNo); - throw new IllegalArgumentException("链接不存在"); - } + return response; + }).subscribeOn(Schedulers.boundedElastic()); +} - log.info("查询到链接任务: id={}, codeNo={}, status={}, needRefresh={}", - linkTask.getId(), linkTask.getCodeNo(), linkTask.getStatus(), linkTask.getNeedRefresh()); - - return linkTask; - } catch (Exception e) { - log.error("=== 用户端链接状态查询失败 ==="); - log.error("错误详情: {}", e.getMessage(), e); - throw e; - } - }) - .subscribeOn(Schedulers.boundedElastic()) - .flatMap(linkTask -> { - // 如果是USING状态,先执行一次检测 - if ("USING".equals(linkTask.getStatus())) { - log.info("链接状态为USING,立即执行一次登录状态检测"); - return checkAndHandleLoginStatus(linkTask) - .doOnSuccess(pollResult -> { - log.info("登录状态检测完成: success={}, status={}", - pollResult.isSuccess(), pollResult.getStatus()); - }) - .doOnError(error -> { - log.warn("登录状态检测失败: {}", error.getMessage()); - }) - .onErrorResume(error -> { - // 检测失败不影响后续流程,继续返回状态 - log.warn("检测失败,继续返回当前状态"); - return Mono.empty(); - }) - .then(Mono.fromCallable(() -> doGetUserLinkStatus(linkId, codeNo))); - } else { - // 非USING状态,直接返回状态 - return Mono.fromCallable(() -> doGetUserLinkStatus(linkId, codeNo)); - } - }) - .subscribeOn(Schedulers.boundedElastic()); - } - - private UserLinkStatusResponse doGetUserLinkStatus(Long linkId, String codeNo) { +/** + * 用户端获取链接状态(支持 linkId 或 codeNo 参数,带自动刷新逻辑) + */ +public Mono getUserLinkStatus(Long linkId, String codeNo) { + return Mono.fromCallable(() -> { log.info("=== 开始处理用户端链接状态查询 ==="); log.info("linkId: {}, codeNo: {}", linkId, codeNo); @@ -508,362 +453,465 @@ public class LinkStatusService { LinkTask linkTask = null; if (linkId != null) { linkTask = linkTaskMapper.findById(linkId); - log.info("通过linkId查询链接任务: id={}", linkId); + log.info("通过 linkId 查询链接任务: id={}", linkId); } else if (codeNo != null && !codeNo.trim().isEmpty()) { linkTask = linkTaskMapper.findByCodeNo(codeNo.trim()); - log.info("通过codeNo查询链接任务: codeNo={}", codeNo); + log.info("通过 codeNo 查询链接任务: codeNo={}", codeNo); } - + if (linkTask == null) { - log.error("链接任务不存在: linkId={}, codeNo={}", linkId, codeNo); + log.error("链接任务不存在 linkId={}, codeNo={}", linkId, codeNo); throw new IllegalArgumentException("链接不存在"); } - log.info("查询到链接任务: id={}, codeNo={}, status={}, needRefresh={}", + log.info("查询到链接任务: id={}, codeNo={}, status={}, needRefresh={}", linkTask.getId(), linkTask.getCodeNo(), linkTask.getStatus(), linkTask.getNeedRefresh()); - // 2. 检查链接任务是否过期 - if (linkTask.getExpireAt() != null && linkTask.getExpireAt().isBefore(LocalDateTime.now())) { - log.warn("链接任务已过期: expireAt={}", linkTask.getExpireAt()); - LocalDateTime now = LocalDateTime.now(); - linkTask.setStatus("EXPIRED"); - linkTask.setExpireAt(now); // 设置过期时间戳 - linkTask.setUpdatedAt(now); - linkTaskMapper.update(linkTask); - - UserLinkStatusResponse response = new UserLinkStatusResponse(); - response.setStatus("EXPIRED"); - return response; - } - - // 3. 检查USING状态的10分钟过期逻辑 - if ("USING".equals(linkTask.getStatus())) { - // 检查是否超过10分钟未登录 - if (linkTask.getQrCreatedAt() != null && - linkTask.getQrCreatedAt().isBefore(LocalDateTime.now().minusMinutes(10))) { - log.warn("选择设备已超过10分钟未登录,链接过期: qrCreatedAt={}", linkTask.getQrCreatedAt()); - - // 将设备加入冷却队列 - 因为10分钟内没有成功登录 - if (linkTask.getMachineId() != null) { - machineCooldownService.addMachineToCooldown(linkTask.getMachineId(), "10分钟内未登录成功"); - log.info("设备{}因10分钟内未登录成功被加入冷却队列", linkTask.getMachineId()); - } - - LocalDateTime now = LocalDateTime.now(); - linkTask.setStatus("EXPIRED"); - linkTask.setExpireAt(now); // 设置过期时间戳 - linkTask.setUpdatedAt(now); - linkTaskMapper.update(linkTask); - - UserLinkStatusResponse response = new UserLinkStatusResponse(); - response.setStatus("EXPIRED"); - return response; - } - - // 如果未超过10分钟,执行自动刷新 - log.info("链接状态是USING,执行自动刷新"); -// performAutoRefresh(linkTask); - } else if ("LOGGED_IN".equals(linkTask.getStatus()) || "COMPLETED".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) { - // 已上号、已完成或已退款状态,不需要刷新 - log.info("链接状态为 {},不需要刷新", linkTask.getStatus()); - } - - // 4. 构建响应 - UserLinkStatusResponse response = buildUserStatusResponse(linkTask); - log.info("=== 用户端链接状态查询完成 ==="); - log.info("返回状态: {}", response.getStatus()); - return response; - + return linkTask; } catch (Exception e) { log.error("=== 用户端链接状态查询失败 ==="); log.error("错误详情: {}", e.getMessage(), e); throw e; } - } - - - - - - /** - * 构建用户端状态响应 - */ - private UserLinkStatusResponse buildUserStatusResponse(LinkTask linkTask) { - UserLinkStatusResponse response = new UserLinkStatusResponse(); - - // 如果状态是USING,返回NEW给用户端 - String statusToReturn = "USING".equals(linkTask.getStatus()) ? "NEW" : linkTask.getStatus(); - response.setStatus(statusToReturn); - response.setMachineId(linkTask.getMachineId()); - return response; - } - - - - /** - * 选区操作 - */ - public Mono selectRegion(String code, String region) { - return Mono.fromCallable(() -> doSelectRegion(code, region)) - .subscribeOn(Schedulers.boundedElastic()); - } - - private SelectRegionResponse doSelectRegion(String code, String region) throws InterruptedException { - log.info("=== 开始选区操作 ==="); - log.info("请求参数: code={}, region={}", code, region); - - try { - // 1. 验证参数 - log.info("步骤1: 开始参数验证"); - if (code == null || code.trim().isEmpty()) { - log.error("参数验证失败: code不能为空"); - throw new IllegalArgumentException("code不能为空"); - } - if (region == null || (!region.equals("Q") && !region.equals("V"))) { - log.error("参数验证失败: region只能是Q或V, 当前值={}", region); - throw new IllegalArgumentException("region只能是Q或V"); - } - log.info("参数验证通过: code={}, region={}", code.trim(), region); - - // 2. 查询链接任务 - log.info("步骤2: 开始查询链接任务"); - LinkTask linkTask = linkTaskMapper.findByCodeNo(code.trim()); - if (linkTask == null) { - log.error("链接任务查询失败: 未找到对应的链接任务, code={}", code); - throw new IllegalArgumentException("链接不存在"); - } - - log.info("链接任务查询成功: id={}, codeNo={}, status={}, batchId={}, machineId={}, needRefresh={}, firstRegionSelectAt={}", - linkTask.getId(), linkTask.getCodeNo(), linkTask.getStatus(), - linkTask.getBatchId(), linkTask.getMachineId(), linkTask.getNeedRefresh(), linkTask.getFirstRegionSelectAt()); - - // 查询批次信息获取times参数 - log.info("步骤3: 开始查询批次信息"); - LinkBatch linkBatch = linkBatchMapper.findById(linkTask.getBatchId()); - if (linkBatch == null) { - log.error("批次信息查询失败: 未找到对应的批次信息, batchId={}", linkTask.getBatchId()); - throw new IllegalStateException("批次信息不存在"); - } - log.info("批次信息查询成功: batchId={}, times={}, quantity={}", linkBatch.getId(), linkBatch.getTimes(), linkBatch.getQuantity()); - - // 4. 检查链接状态,只有NEW或USING状态才能选区 - log.info("步骤4: 开始检查链接状态"); - if (!"NEW".equals(linkTask.getStatus()) && !"USING".equals(linkTask.getStatus())) { - log.error("链接状态检查失败: 当前状态={}, 只允许NEW或USING状态进行选区操作", linkTask.getStatus()); - throw new IllegalArgumentException("链接状态不正确,只有新建或使用中状态的链接才能选区"); - } - log.info("链接状态检查通过: 当前状态={}, 允许进行选区操作", linkTask.getStatus()); - - - // 5. 如果need_refresh=true,检查是否已等待10秒 -// if (Boolean.TRUE.equals(linkTask.getNeedRefresh()) && linkTask.getRefreshTime() != null) { -// LocalDateTime now = LocalDateTime.now(); -// long secondsSinceRefresh = ChronoUnit.SECONDS.between(linkTask.getRefreshTime(), now); -// if (secondsSinceRefresh < 10) { -// long waitTime = 10 - secondsSinceRefresh; -// log.error("刷新后需要等待,剩余等待时间: {}秒", waitTime); -// SelectRegionResponse response = new SelectRegionResponse(false, "刷新后需要等待" + waitTime + "秒才能选区"); -// return response; -// } -// } - // 5. 设备选择逻辑 - log.info("步骤5: 开始设备选择逻辑"); - String selectedDeviceId; - - if(linkTask.getFirstRegionSelectAt() == null){ - log.info("首次选区: 开始检查和分配空闲设备"); - DeviceStatusResponse deviceStatus = scriptClient.checkAvailableDeviceStatus().block(); - - // 检查是否有空闲设备 - if (deviceStatus.getAvailableCount() == 0) { - log.error("设备分配失败: 当前没有空闲设备可用, 总设备数={}, 空闲设备数={}", - deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount()); - throw new RuntimeException("当前没有空闲设备,请稍后再试"); - } - - log.info("空闲设备检查完成: 总设备数={}, 空闲设备数={}, 空闲设备列表={}", - deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices()); - - // 过滤掉冷却期内的设备 - List availableDevices = deviceStatus.getAvailableDevices(); - List nonCooldownDevices = new ArrayList<>(); - - for (String deviceId : availableDevices) { - if (!machineCooldownService.isMachineInCooldown(deviceId)) { - nonCooldownDevices.add(deviceId); - } else { - long remainingMinutes = machineCooldownService.getRemainingCooldownMinutes(deviceId); - log.info("设备{}在冷却期内,剩余时间{}分钟,跳过选择", deviceId, remainingMinutes); - } - } - - if (nonCooldownDevices.isEmpty()) { - log.error("设备分配失败: 所有空闲设备都在冷却期内, 总空闲设备数={}", availableDevices.size()); - throw new RuntimeException("所有设备都在冷却期内,请稍后再试"); - } - - log.info("冷却期过滤完成: 原空闲设备数={}, 可用设备数={}, 可用设备列表={}", - availableDevices.size(), nonCooldownDevices.size(), nonCooldownDevices); - - // 选择一个非冷却期设备 - selectedDeviceId = nonCooldownDevices.get(0); // 选择第一个非冷却期设备 - log.info("首次选区设备分配成功: 选中设备={}, 从{}个非冷却期设备中选择第一个", - selectedDeviceId, nonCooldownDevices.size()); - - // 将选中的设备加入冷却队列 - machineCooldownService.addMachineToCooldown(selectedDeviceId, "首次选区"); - }else{ - log.info("重复选区: 检查首次选区时效性并复用设备"); - // 检查首次选区是否已过期 - LocalDateTime firstSelectTime = linkTask.getFirstRegionSelectAt(); - long expireSeconds = systemConfigService.getFirstRegionExpireSeconds(); - LocalDateTime expireTime = firstSelectTime.plusSeconds(expireSeconds); - LocalDateTime now = LocalDateTime.now(); - - log.info("时效性检查: 首次选区时间={}, 过期时间={}, 当前时间={}, 过期秒数={}", - firstSelectTime, expireTime, now, expireSeconds); - - if (now.isAfter(expireTime)) { - log.error("链接过期检查失败: 首次选区已过期, firstSelectTime={}, expireTime={}, now={}", - firstSelectTime, expireTime, now); - - // 将链接状态设置为过期 - linkTask.setStatus("EXPIRED"); - linkTask.setExpireAt(now); // 设置过期时间戳 - linkTask.setUpdatedAt(now); - linkTaskMapper.updateById(linkTask); - log.info("链接状态已更新为EXPIRED: linkTaskId={}, 更新时间={}", linkTask.getId(), now); - - throw new RuntimeException("链接已过期,请重新获取"); - } - - selectedDeviceId = linkTask.getMachineId(); - log.info("重复选区设备复用成功: 使用之前分配的设备={}, 首次选区时间={}", selectedDeviceId, firstSelectTime); - log.info("执行设备刷新操作: 设备={}", selectedDeviceId); - scriptClient.refresh(selectedDeviceId).block(); - log.info("设备刷新完成: 设备={}, 继续使用之前分配的设备", selectedDeviceId); - // 休眠5秒,确保设备状态稳定 - Thread.sleep(11000); - log.info("等待5秒完成,继续使用之前分配的设备: 设备={}", selectedDeviceId); - } - - log.info("设备选择完成: 最终选中设备={}", selectedDeviceId); - - // 6. 检查该设备是否有之前的LOGGED_IN状态链接任务需要完成 - log.info("步骤6: 开始检查设备状态和历史任务"); - try { - log.info("检查设备历史任务: 设备={}, 触发原因=选区请求", selectedDeviceId); - deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(selectedDeviceId, "选区请求"); - log.info("设备历史任务检查完成: 设备={}", selectedDeviceId); - } catch (Exception e) { - log.warn("设备历史任务检查异常: 设备={}, 错误={}, 继续选区流程", selectedDeviceId, e.getMessage()); - // 不影响选区流程,只记录警告日志 - } - - // 7. 调用保存总次数接口 -// log.info("步骤7: 开始保存总次数到脚本端"); -// try { -// log.info("保存总次数: 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes()); -// scriptClient.saveTotalTimes(selectedDeviceId, linkBatch.getTimes()).block(); -// log.info("总次数保存成功: 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes()); -// } catch (Exception e) { -// log.warn("总次数保存失败: 设备={}, 次数={}, 错误={}, 继续后续流程", selectedDeviceId, linkBatch.getTimes(), e.getMessage()); -// // 不影响后续流程,只记录警告日志 -// } - - // 8. 调用脚本端选区,使用选中的设备 - log.info("步骤8: 开始调用脚本端选区"); - log.info("选区请求参数: 设备={}, 区域={}", selectedDeviceId, region); - String selectResult = scriptClient.selectRegion(selectedDeviceId, region).block(); - log.info("脚本端选区调用成功: 设备={}, 区域={}, 返回结果={}", selectedDeviceId, region, selectResult); - - // 11. 等待脚本端生成二维码(这里可以添加轮询逻辑) -// log.info("等待脚本端生成二维码,等待3秒..."); -// Thread.sleep(3000); - - // 9. 更新数据库状态为USING,保存设备信息 - log.info("步骤9: 开始更新数据库状态"); - LocalDateTime now = LocalDateTime.now(); - LocalDateTime qrExpireAt = now.plusSeconds(60); // 60秒后过期 - - // 记录更新前的状态 - 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); - linkTask.setQrCreatedAt(now); - linkTask.setQrExpireAt(qrExpireAt); - if (linkTask.getFirstRegionSelectAt() == null) { - linkTask.setFirstRegionSelectAt(now); // 只在首次选区时记录 - log.info("首次选区: 记录首次选区时间={}", now); - } - linkTask.setNeedRefresh(false); - linkTask.setUpdatedAt(now); - 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()); - - // 10. 构建响应 - log.info("步骤10: 开始构建响应数据"); - SelectRegionResponse response = new SelectRegionResponse(true, "选区成功"); - String qrCodeUrl = scriptClient.getProxyQrCodeUrl(code); - response.setQrCodeUrl(qrCodeUrl); - response.setQrCreatedAt(now); - response.setQrExpireAt(qrExpireAt); - response.setStatus("USING"); - response.setQrDelaySeconds(10); // 客户端收到响应后,等待5秒再请求二维码 - response.setMecmachineId(selectedDeviceId); // 便于调试和维护 - - log.info("响应构建完成: success=true, status=USING, qrCodeUrl={}, qrDelaySeconds=5, machineId={}", - qrCodeUrl, selectedDeviceId); - log.info("=== 选区操作完成 ==="); - - return response; - - } catch (Exception e) { - log.error("=== 选区操作失败 ==="); - log.error("错误详情: {}", e.getMessage(), e); - throw e; + }) + .subscribeOn(Schedulers.boundedElastic()) + .flatMap(linkTask -> { + // 如果是 USING 状态,先执行一次检测 + if ("USING".equals(linkTask.getStatus())) { + log.info("链接状态为 USING,立即执行一次登录状态检测"); + return checkAndHandleLoginStatus(linkTask) + .doOnSuccess(pollResult -> { + log.info("登录状态检测完成: success={}, status={}", + pollResult.isSuccess(), pollResult.getStatus()); + }) + .doOnError(error -> { + log.warn("登录状态检测失败: {}", error.getMessage()); + }) + .onErrorResume(error -> { + // 检测失败不影响后续流程,继续返回当前状态 + log.warn("检测失败,继续返回当前状态"); + return Mono.empty(); + }) + .then(Mono.fromCallable(() -> doGetUserLinkStatus(linkId, codeNo))); + } else { + // 非 USING 状态,直接返回状态 + return Mono.fromCallable(() -> doGetUserLinkStatus(linkId, codeNo)); } + }) + .subscribeOn(Schedulers.boundedElastic()); +} + +private UserLinkStatusResponse doGetUserLinkStatus(Long linkId, String codeNo) { + log.info("=== 开始处理用户端链接状态查询 ==="); + log.info("linkId: {}, codeNo: {}", linkId, codeNo); + + try { + // 1. 查询链接任务 + LinkTask linkTask = null; + if (linkId != null) { + linkTask = linkTaskMapper.findById(linkId); + log.info("通过 linkId 查询链接任务: id={}", linkId); + } else if (codeNo != null && !codeNo.trim().isEmpty()) { + linkTask = linkTaskMapper.findByCodeNo(codeNo.trim()); + log.info("通过 codeNo 查询链接任务: codeNo={}", codeNo); + } + + if (linkTask == null) { + log.error("链接任务不存在 linkId={}, codeNo={}", linkId, codeNo); + throw new IllegalArgumentException("链接不存在"); + } + + log.info("查询到链接任务: id={}, codeNo={}, status={}, needRefresh={}", + linkTask.getId(), linkTask.getCodeNo(), linkTask.getStatus(), linkTask.getNeedRefresh()); + + // 2. 检查链接任务是否过期 + if (linkTask.getExpireAt() != null && linkTask.getExpireAt().isBefore(LocalDateTime.now())) { + log.warn("链接任务已过期: expireAt={}", linkTask.getExpireAt()); + LocalDateTime now = LocalDateTime.now(); + linkTask.setStatus("EXPIRED"); + linkTask.setExpireAt(now); // 设置过期时间戳 + linkTask.setUpdatedAt(now); + // Note: The following line was incomplete/incorrect in the original. + // It's commented out as 'region' and 'selectedDeviceId' are not defined in this context. + // int affected = linkTaskMapper.reserveDeviceIfFree(linkTask.getId(), region, selectedDeviceId, 60); + // if (affected == 0) { + // log.warn("原子占用失败:设备已被其他任务占用 device={},codeNo={}", selectedDeviceId, linkTask.getCodeNo()); + // throw new RuntimeException("设备已被占用,请重试"); + // } + if (linkTask.getFirstRegionSelectAt() == null) { + linkTask.setFirstRegionSelectAt(LocalDateTime.now()); + try { + linkTaskMapper.update(linkTask); + } catch (Exception ignore) {} + } + } + + // 3. 检查 USING 状态的 10 分钟过期逻辑 + if ("USING".equals(linkTask.getStatus())) { + // 检查是否超过 10 分钟未登录 + if (linkTask.getQrCreatedAt() != null && + linkTask.getQrCreatedAt().isBefore(LocalDateTime.now().minusMinutes(10))) { + log.warn("选择设备已超过 10 分钟未登录,链接过期: qrCreatedAt={}", linkTask.getQrCreatedAt()); + + // 将设备加入冷却队列 - 因为 10 分钟内没有成功登录 + if (linkTask.getMachineId() != null) { + machineCooldownService.addMachineToCooldown(linkTask.getMachineId(), "10分钟内未登录成功"); + log.info("设备{}因 10 分钟内未登录成功被加入冷却队列", linkTask.getMachineId()); + } + + LocalDateTime now = LocalDateTime.now(); + linkTask.setStatus("EXPIRED"); + linkTask.setExpireAt(now); // 设置过期时间戳 + linkTask.setUpdatedAt(now); + // Note: The following line was incomplete/incorrect in the original. + // It's commented out as 'region' and 'selectedDeviceId' are not defined in this context. + // int affected = linkTaskMapper.reserveDeviceIfFree(linkTask.getId(), region, selectedDeviceId, 60); + // if (affected == 0) { + // log.warn("原子占用失败:设备已被其他任务占用 device={},codeNo={}", selectedDeviceId, linkTask.getCodeNo()); + // throw new RuntimeException("设备已被占用,请重试"); + // } + if (linkTask.getFirstRegionSelectAt() == null) { + linkTask.setFirstRegionSelectAt(LocalDateTime.now()); + try { + linkTaskMapper.update(linkTask); + } catch (Exception ignore) {} + } + + UserLinkStatusResponse response = new UserLinkStatusResponse(); + response.setStatus("EXPIRED"); + return response; + } + + // 如果未超过 10 分钟,执行自动刷新 + log.info("链接状态是 USING,执行自动刷新"); +// performAutoRefresh(linkTask); + } else if ("LOGGED_IN".equals(linkTask.getStatus()) || "COMPLETED".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) { + // 已上号、已完成或已退单状态,不需要刷新 + log.info("链接状态为 {},不需要刷新", linkTask.getStatus()); + } + + // 4. 构建响应 + UserLinkStatusResponse response = buildUserStatusResponse(linkTask); + log.info("=== 用户端链接状态查询完成 ==="); + log.info("返回状态: {}", response.getStatus()); + return response; + + } catch (Exception e) { + log.error("=== 用户端链接状态查询失败 ==="); + log.error("错误详情: {}", e.getMessage(), e); + throw e; } +} - /** - * 轮询上号接口 - */ - public Mono pollLogin(String code) { - log.info("=== 开始轮询上号 ==="); - log.info("code: {}", code); - return validatePollLoginRequest(code) - .flatMap(result -> { - if (result instanceof PollLoginResponse) { - // 如果验证返回响应对象,直接返回 - return Mono.just((PollLoginResponse) result); + + + /** + * 构建用户端状态响应 + */ + private UserLinkStatusResponse buildUserStatusResponse(LinkTask linkTask) { + UserLinkStatusResponse response = new UserLinkStatusResponse(); + + // 如果状态是USING,返回NEW给用户端 + String statusToReturn = "USING".equals(linkTask.getStatus()) ? "NEW" : linkTask.getStatus(); + response.setStatus(statusToReturn); + response.setMachineId(linkTask.getMachineId()); + return response; + } + + + + /** + * 选区操作 + */ + public Mono selectRegion(String code, String region) { + return Mono.fromCallable(() -> doSelectRegion(code, region)) + .subscribeOn(Schedulers.boundedElastic()); + } + + private SelectRegionResponse doSelectRegion(String code, String region) throws InterruptedException { + log.info("=== 开始选区操作 ==="); + log.info("请求参数: code={}, region={}", code, region); + + try { + // 1. 验证参数 + log.info("步骤1: 开始参数验证"); + if (code == null || code.trim().isEmpty()) { + log.error("参数验证失败: code不能为空"); + throw new IllegalArgumentException("code不能为空"); + } + if (region == null || (!region.equals("Q") && !region.equals("V"))) { + log.error("参数验证失败: region只能是Q或V, 当前值{}", region); + throw new IllegalArgumentException("region只能是Q或V"); + } + log.info("参数验证通过: code={}, region={}", code.trim(), region); + + // 2. 查询链接任务 + log.info("步骤2: 开始查询链接任务"); + LinkTask linkTask = linkTaskMapper.findByCodeNo(code.trim()); + if (linkTask == null) { + log.error("链接任务查询失败: 未找到对应的链接任务, code={}", code); + throw new IllegalArgumentException("链接不存在"); + } + + log.info("链接任务查询成功: id={}, codeNo={}, status={}, batchId={}, machineId={}, needRefresh={}, firstRegionSelectAt={}", + linkTask.getId(), linkTask.getCodeNo(), linkTask.getStatus(), + linkTask.getBatchId(), linkTask.getMachineId(), linkTask.getNeedRefresh(), linkTask.getFirstRegionSelectAt()); + + // 查询批次信息获取times参数 + log.info("步骤3: 开始查询批次信息"); + LinkBatch linkBatch = linkBatchMapper.findById(linkTask.getBatchId()); + if (linkBatch == null) { + log.error("批次信息查询失败: 未找到对应的批次信息, batchId={}", linkTask.getBatchId()); + throw new IllegalStateException("批次信息不存在"); + } + log.info("批次信息查询成功: batchId={}, times={}, quantity={}", linkBatch.getId(), linkBatch.getTimes(), linkBatch.getQuantity()); + + // 4. 检查链接状态,只有NEW或USING状态才能选区 + log.info("步骤4: 开始检查链接状态"); + if (!"NEW".equals(linkTask.getStatus()) && !"USING".equals(linkTask.getStatus())) { + log.error("链接状态检查失败 当前状态{}, 只允许NEW或USING状态进行选区操作", linkTask.getStatus()); + throw new IllegalArgumentException("链接状态不正确,只有新建或使用中状态的链接才能选区"); + } + log.info("链接状态检查通过: 当前状态{}, 允许进行选区操作", linkTask.getStatus()); + + + // 5. 如果need_refresh=true,检查是否已等待10秒 + // if (Boolean.TRUE.equals(linkTask.getNeedRefresh()) && linkTask.getRefreshTime() != null) { + // LocalDateTime now = LocalDateTime.now(); + // long secondsSinceRefresh = ChronoUnit.SECONDS.between(linkTask.getRefreshTime(), now); + // if (secondsSinceRefresh < 10) { + // long waitTime = 10 - secondsSinceRefresh; + // log.error("刷新后需要等待,剩余等待时间: {}秒, waitTime); + // SelectRegionResponse response = new SelectRegionResponse(false, "刷新后需要等待" + waitTime + "秒才能选区"); + // return response; + // } + // } + // 5. 设备选择逻辑 + log.info("步骤5: 开始设备选择逻辑"); + String selectedDeviceId; + + if(linkTask.getFirstRegionSelectAt() == null){ + log.info("首次选区: 开始检查和分配空闲设备"); + DeviceStatusResponse deviceStatus = scriptClient.checkAvailableDeviceStatus().block(); + + // 检查是否有空闲设备 + if (deviceStatus.getAvailableCount() == 0) { + log.error("设备分配失败: 当前没有空闲设备可用, 总设备数={}, 空闲设备数{}", + deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount()); + throw new RuntimeException("当前没有空闲设备,请稍后再试"); } - // 否则继续处理登录状态检查 - return checkAndHandleLoginStatus((LinkTask) result); - }) - .doOnSuccess(response -> { - log.info("轮询上号完成: success={}, status={}", response.isSuccess(), response.getStatus()); - }) - .doOnError(error -> { - log.error("轮询上号失败: code={}, error={}", code, error.getMessage(), error); - }); - } + + log.info("空闲设备检查完成 总设备数={}, 空闲设备数{}, 空闲设备列表={}", + deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices()); + + // 使用新的设备分配服务进行原子设备分配 + List availableDevices = deviceStatus.getAvailableDevices(); + selectedDeviceId = deviceAllocationService.allocateDevice(availableDevices, linkTask.getId(), "首次选区"); + + if (selectedDeviceId == null) { + log.error("设备分配失败: 所有空闲设备都在冷却期内或被占用 总空闲设备数={}", availableDevices.size()); + throw new RuntimeException("所有设备都在冷却期内或被占用,请稍后再试"); + } + + log.info("首次选区设备分配成功: 选中设备={}, 任务ID={}", selectedDeviceId, linkTask.getId()); + }else{ + log.info("重复选区: 检查首次选区时有效性并复用设备"); + // 检查首次选区是否已过期 + LocalDateTime firstSelectTime = linkTask.getFirstRegionSelectAt(); + long expireSeconds = systemConfigService.getFirstRegionExpireSeconds(); + LocalDateTime expireTime = firstSelectTime.plusSeconds(expireSeconds); + LocalDateTime now = LocalDateTime.now(); + + log.info("时效性检查: 首次选区时间={}, 过期时间={}, 当前时间={}, 过期秒数={}", + firstSelectTime, expireTime, now, expireSeconds); + + if (now.isAfter(expireTime)) { + log.error("链接过期检查失败: 首次选区已过期 firstSelectTime={}, expireTime={}, now={}", + firstSelectTime, expireTime, now); + + // 将链接状态设置为过期 + linkTask.setStatus("EXPIRED"); + linkTask.setExpireAt(now); // 设置过期时间戳 + linkTask.setUpdatedAt(now); + linkTaskMapper.updateById(linkTask); + log.info("链接状态已更新为EXPIRED: linkTaskId={}, 更新时间={}", linkTask.getId(), now); + + throw new RuntimeException("链接已过期,请重新获取"); + } + + selectedDeviceId = linkTask.getMachineId(); + + // 检查之前分配的设备是否被其他链接任务占用 + List occupiedTasks = linkTaskMapper.findByMachineIdAndStatus(selectedDeviceId, "USING"); + occupiedTasks.addAll(linkTaskMapper.findByMachineIdAndStatus(selectedDeviceId, "LOGGED_IN")); + + // 过滤掉当前链接任务本身 + occupiedTasks.removeIf(task -> task.getId().equals(linkTask.getId())); + + if (!occupiedTasks.isEmpty()) { + log.error("重复选区设备冲突: 之前分配的设备{}被其他链接任务占用 占用任务数{}", selectedDeviceId, occupiedTasks.size()); + for (LinkTask occupiedTask : occupiedTasks) { + log.error("占用设备{}的链接 codeNo={}, status={}, id={}", selectedDeviceId, occupiedTask.getCodeNo(), occupiedTask.getStatus(), occupiedTask.getId()); + } + throw new RuntimeException("设备被其他链接占用,请重新获取链接"); + } + + log.info("重复选区设备复用成功: 使用之前分配的设备{}, 首次选区时间={}", selectedDeviceId, firstSelectTime); + log.info("执行设备刷新操作: 设备={}", selectedDeviceId); + scriptClient.refresh(selectedDeviceId).block(); + log.info("设备刷新完成: 设备={}, 继续使用之前分配的设备", selectedDeviceId); + // 休眠5秒,确保设备状态稳定 + Thread.sleep(5000); + log.info("等待5秒完成,继续使用之前分配的设备 设备={}", selectedDeviceId); + } + + log.info("设备选择完成: 最终选中设备={}", selectedDeviceId); + + // 6. 检查该设备是否有之前的LOGGED_IN状态链接任务需要完成 + log.info("步骤6: 开始检查设备状态和历史任务"); + try { + log.info("检查设备历史任务 设备={}, 触发原因=选区请求", selectedDeviceId); + deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(selectedDeviceId, "选区请求"); + log.info("设备历史任务检查完成 设备={}", selectedDeviceId); + } catch (Exception e) { + log.warn("设备历史任务检查异常 设备={}, 错误={}, 继续选区流程", selectedDeviceId, e.getMessage()); + // 不影响选区流程,只记录警告日志 + } + + // 7. 调用保存总次数接口 + // log.info("步骤7: 开始保存总次数到脚本端"); + // try { + // log.info("保存总次数 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes()); + // scriptClient.saveTotalTimes(selectedDeviceId, linkBatch.getTimes()).block(); + // log.info("总次数保存成功 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes()); + // } catch (Exception e) { + // log.warn("总次数保存失败 设备={}, 次数={}, 错误={}, 继续后续流程", selectedDeviceId, linkBatch.getTimes(), e.getMessage()); + // // 不影响后续流程,只记录警告日志 + // } + + // 8. 调用脚本端选区,使用选中的设备 + log.info("步骤8: 开始调用脚本端选区"); + log.info("选区请求参数: 设备={}, 区域={}", selectedDeviceId, region); + + String selectResult; + try { + selectResult = scriptClient.selectRegion(selectedDeviceId, region).block(); + log.info("脚本端选区调用成功: 设备={}, 区域={}, 返回结果={}", selectedDeviceId, region, selectResult); + } catch (Exception e) { + log.error("脚本端选区调用失败: 设备={}, 区域={}, 错误={}", selectedDeviceId, region, e.getMessage()); + + // 释放设备分配(仅在首次选区时) + if (linkTask.getFirstRegionSelectAt() == null) { + log.info("脚本调用失败,释放设备分配: 设备={}, 任务ID={}", selectedDeviceId, linkTask.getId()); + deviceAllocationService.releaseDeviceAllocation(selectedDeviceId, linkTask.getId()); + } + + throw new RuntimeException("选区操作失败,请重试: " + e.getMessage()); + } + + // 11. 等待脚本端生成二维码(这里可以添加轮询逻辑) + // log.info("等待脚本端生成二维码,等待3秒.."); + // Thread.sleep(3000); + + // 9. 原子占用设备并更新任务状态 + log.info("步骤9: 原子占用设备并更新任务状态"); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime qrExpireAt = now.plusSeconds(60); // 60秒后过期 + + // 记录更新前的状态 + log.info("数据库更新前状态 id={}, status={}, region={}, machineId={}, firstRegionSelectAt={}", + linkTask.getId(), linkTask.getStatus(), linkTask.getRegion(), linkTask.getMachineId(), linkTask.getFirstRegionSelectAt()); + + String prev = linkTask.getStatus(); + int affected = linkTaskMapper.reserveDeviceIfFree(linkTask.getId(), region, selectedDeviceId, 60); + if (affected == 0) { + log.warn("原子占用失败:设备已被其他任务占用 device={},codeNo={}", selectedDeviceId, linkTask.getCodeNo()); + throw new RuntimeException("设备已被占用,请重试"); + } + if (linkTask.getFirstRegionSelectAt() == null) { + linkTask.setFirstRegionSelectAt(now); + try { linkTaskMapper.update(linkTask); } catch (Exception ignore) {} + } + // 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()); + + // 9.5. 验证设备分配是否存在冲突 + log.info("步骤9.5: 验证设备分配结果"); + if (!deviceAllocationService.validateDeviceAllocation(selectedDeviceId, linkTask.getId())) { + log.error("设备分配冲突检测失败,准备回滚操作 设备={}, 任务ID={}", selectedDeviceId, linkTask.getId()); + + // 回滚操作:释放设备分配和还原任务状态 + try { + deviceAllocationService.releaseDeviceAllocation(selectedDeviceId, linkTask.getId()); + + // 将任务状态回滚到NEW + linkTaskMapper.updateStatus(linkTask.getId(), "NEW"); + log.info("设备分配冲突回滚完成 设备={}, 任务ID={}", selectedDeviceId, linkTask.getId()); + } catch (Exception rollbackEx) { + log.error("设备分配回滚失败 设备={}, 任务ID={}, 错误: {}", + selectedDeviceId, linkTask.getId(), rollbackEx.getMessage()); + } + + throw new RuntimeException("设备分配冲突,请重试"); + } + log.info("设备分配验证通过 设备={}, 任务ID={}", selectedDeviceId, linkTask.getId()); + + // 10. 构建响应 + log.info("步骤10: 开始构建响应数据"); + SelectRegionResponse response = new SelectRegionResponse(true, "选区成功"); + String qrCodeUrl = scriptClient.getProxyQrCodeUrl(code); + response.setQrCodeUrl(qrCodeUrl); + response.setQrCreatedAt(now); + response.setQrExpireAt(qrExpireAt); + response.setStatus("USING"); + response.setQrDelaySeconds(10); // 客户端收到响应后,等待5秒再请求二维码 + response.setMecmachineId(selectedDeviceId); // 便于调试和维护 + log.info("响应构建完成: success=true, status=USING, qrCodeUrl={}, qrDelaySeconds=5, machineId={}", + qrCodeUrl, selectedDeviceId); + log.info("=== 选区操作完成 ==="); + + return response; + + } catch (Exception e) { + log.error("=== 选区操作失败 ==="); + log.error("错误详情: {}", e.getMessage(), e); + throw e; + } + } + + /** + * 轮询上号接口 + */ + public Mono pollLogin(String code) { + log.info("=== 开始轮询上号 ==="); + log.info("code: {}", code); + + return validatePollLoginRequest(code) + .flatMap(result -> { + if (result instanceof PollLoginResponse) { + // 如果验证返回响应对象,直接返回 + return Mono.just((PollLoginResponse) result); + } + // 否则继续处理登录状态检查 + return checkAndHandleLoginStatus((LinkTask) result); + }) + .doOnSuccess(response -> { + log.info("轮询上号完成: success={}, status={}", response.isSuccess(), response.getStatus()); + }) + .doOnError(error -> { + log.error("轮询上号失败: code={}, error={}", code, error.getMessage(), error); + }); + } /** * 验证轮询上号请求参数和链接状态 @@ -886,7 +934,7 @@ public class LinkStatusService { // 检查链接状态,只有USING状态才能轮询 if (!"USING".equals(linkTask.getStatus())) { - log.warn("链接状态不是USING,当前状态: {}", linkTask.getStatus()); + log.warn("链接状态不是USING,当前状态 {}", linkTask.getStatus()); return new PollLoginResponse(false, linkTask.getStatus()); } @@ -918,12 +966,12 @@ public class LinkStatusService { } // 调用脚本端检查上号状态 - log.info("调用脚本端检查上号状态: 代理code={}, 真实设备={}", linkTask.getCodeNo(), realDeviceId); + log.info("调用脚本端检查上号状态 代理code={}, 真实设备={}", linkTask.getCodeNo(), realDeviceId); return scriptClient.checkLoginStatus(realDeviceId) .map(loginResult -> processLoginResult(linkTask, realDeviceId, loginResult)) .onErrorResume(error -> { - log.warn("调用脚本端检查上号状态失败: codeNo={}, error={}", + log.warn("调用脚本端检查上号状态失败 codeNo={}, error={}", linkTask.getCodeNo(), error.getMessage()); // 脚本端出错时,返回当前状态,不影响轮询 return Mono.just(new PollLoginResponse(false, "USING")); @@ -934,7 +982,7 @@ public class LinkStatusService { * 处理登录检查结果 */ private PollLoginResponse processLoginResult(LinkTask linkTask, String deviceId, String loginResult) { - log.info("脚本端返回结果: {}", loginResult); + log.info("脚本端返回结果 {}", loginResult); // 检查是否已上号 if ("已上号".equals(loginResult) || "已登录".equals(loginResult)) { @@ -947,7 +995,7 @@ public class LinkStatusService { log.info("============================================="); } catch (Exception e) { - log.warn("保存总次数接口调用失败: {}", e.getMessage()); + log.warn("保存总次数接口调用失败 {}", e.getMessage()); // 不影响后续流程,只记录警告日志 } return handleSuccessfulLogin(linkTask, deviceId); @@ -986,13 +1034,13 @@ public class LinkStatusService { // 记录设备登录时间(用于完成检测的缓冲期判断) try { - // 这里需要注入 GameCompletionDetectionService,为了兼容性,暂时记录日志 + // 这里需要注入GameCompletionDetectionService,为了兼容性,暂时记录日志 log.info("设备{}登录成功,开始缓冲期保护", deviceId); } catch (Exception e) { log.warn("记录设备登录时间失败", e); } - log.info("状态更新完成: codeNo={}, status=LOGGED_IN", linkTask.getCodeNo()); + log.info("状态更新完成 codeNo={}, status=LOGGED_IN", linkTask.getCodeNo()); // 构建成功响应 PollLoginResponse response = new PollLoginResponse(true, "LOGGED_IN", "SECOND", @@ -1021,7 +1069,7 @@ public class LinkStatusService { // 查询链接任务 LinkTask linkTask = linkTaskMapper.findByCodeNo(code.trim()); if (linkTask == null) { - log.warn("链接任务不存在: code={}", code); + log.warn("链接任务不存在 code={}", code); return null; } @@ -1031,7 +1079,7 @@ public class LinkStatusService { return machineId; } catch (Exception e) { - log.error("根据code获取设备ID时发生异常: code={}, error={}", code, e.getMessage(), e); + log.error("根据code获取设备ID时发生异常 code={}, error={}", code, e.getMessage(), e); return null; } } @@ -1060,27 +1108,27 @@ public class LinkStatusService { // 2. 查询链接任务,获取machineId LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo.trim()); if (linkTask == null) { - log.error("链接任务不存在: codeNo={}", codeNo); + log.error("链接任务不存在 codeNo={}", codeNo); return TargetScoreResponse.error(codeNo, null, "链接不存在"); } String machineId = linkTask.getMachineId(); if (machineId == null || machineId.trim().isEmpty()) { - log.error("链接未关联设备: codeNo={}", codeNo); + log.error("链接未关联设备 codeNo={}", codeNo); return TargetScoreResponse.error(codeNo, null, "链接未关联设备"); } log.info("查询到设备ID: codeNo={}, machineId={}", codeNo, machineId); // 3. 调用脚本端获取目标分数 - log.info("调用脚本端获取目标分数: machineId={}", machineId); + log.info("调用脚本端获取目标分数 machineId={}", machineId); String targetScoreStr = scriptClient.getTargetScore(machineId).block(); - log.info("脚本端返回结果: machineId={}, result={}", machineId, targetScoreStr); + log.info("脚本端返回结果 machineId={}, result={}", machineId, targetScoreStr); // 4. 解析返回结果 if (targetScoreStr == null || targetScoreStr.trim().isEmpty()) { log.warn("脚本端返回空结果: machineId={}", machineId); - return TargetScoreResponse.error(codeNo, machineId, "网络繁忙,稍后再试"); + return TargetScoreResponse.error(codeNo, machineId, "网络拥堵,请稍后再试"); } String trimmedResult = targetScoreStr.trim(); @@ -1088,12 +1136,12 @@ public class LinkStatusService { // 检查是否为数字 try { Integer targetScore = Integer.parseInt(trimmedResult); - log.info("解析到数字目标分数: {}", targetScore); + log.info("解析到数字目标分数 {}", targetScore); // 5. 保存到数据库 linkTask.setCompletedPoints(targetScore); linkTask.setUpdatedAt(LocalDateTime.now()); - int updateResult = linkTaskMapper.update(linkTask); + int updateResult = linkTaskMapper.updateById(linkTask); // Fixed the assignment here if (updateResult > 0) { log.info("目标分数保存成功: codeNo={}, machineId={}, targetScore={}", @@ -1111,10 +1159,10 @@ public class LinkStatusService { if ("空的".equals(trimmedResult) || "空闲".equals(trimmedResult) || "已运行".equals(trimmedResult)) { log.info("设备状态为: {}", trimmedResult); - return TargetScoreResponse.error(codeNo, machineId, "网络繁忙,稍后再试"); + return TargetScoreResponse.error(codeNo, machineId, "网络拥堵,请稍后再试"); } else { - log.warn("未知的返回结果: {}", trimmedResult); - return TargetScoreResponse.error(codeNo, machineId, "网络繁忙,稍后再试"); + log.warn("未知的返回结果 {}", trimmedResult); + return TargetScoreResponse.error(codeNo, machineId, "网络拥堵,请稍后再试"); } } @@ -1125,3 +1173,6 @@ public class LinkStatusService { } } } + + + diff --git a/src/main/java/com/gameplatform/server/task/MachineCooldownCleanupTask.java b/src/main/java/com/gameplatform/server/task/MachineCooldownCleanupTask.java index da54abf..ff5c34f 100644 --- a/src/main/java/com/gameplatform/server/task/MachineCooldownCleanupTask.java +++ b/src/main/java/com/gameplatform/server/task/MachineCooldownCleanupTask.java @@ -1,6 +1,6 @@ package com.gameplatform.server.task; -import com.gameplatform.server.service.cooldown.MachineCooldownService; +import com.gameplatform.server.service.cooldown.MemoryMachineCooldownService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; @@ -14,9 +14,9 @@ import org.springframework.stereotype.Component; public class MachineCooldownCleanupTask { private static final Logger log = LoggerFactory.getLogger(MachineCooldownCleanupTask.class); - private final MachineCooldownService machineCooldownService; + private final MemoryMachineCooldownService machineCooldownService; - public MachineCooldownCleanupTask(MachineCooldownService machineCooldownService) { + public MachineCooldownCleanupTask(MemoryMachineCooldownService machineCooldownService) { this.machineCooldownService = machineCooldownService; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0cd7028..6754065 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,7 +17,7 @@ mybatis-plus: type-aliases-package: com.gameplatform.server.model.entity configuration: map-underscore-to-camel-case: true -# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 注释掉以关闭SQL日志 global-config: db-config: id-type: auto @@ -37,10 +37,28 @@ management: logging: level: root: info - com.gameplatform.server: info - com.baomidou.mybatisplus: info - org.apache.ibatis: info - com.zaxxer.hikari: 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 + # Mapper 与 SQL 调用降噪(屏蔽 MyBatis 的参数/SQL DEBUG) + com.gameplatform.server.mapper: warn + com.baomidou.mybatisplus: warn + org.apache.ibatis: warn + org.mybatis: warn + com.zaxxer.hikari: warn + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" + console: + enabled: true security: jwt: diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index fdaeccc..a82a25d 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -1,7 +1,22 @@ + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + UTF-8 + + + + + + + ${LOG_PATH}/audit-status.log @@ -23,6 +38,26 @@ - - + + + ${LOG_PATH}/server.log + + ${LOG_PATH}/server.%d{yyyy-MM-dd}.%i.log.gz + + 20MB + + 30 + 2GB + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + UTF-8 + + + + + + + + diff --git a/src/main/resources/mapper/agent/LinkTaskMapper.xml b/src/main/resources/mapper/agent/LinkTaskMapper.xml index b589f51..c3dd212 100644 --- a/src/main/resources/mapper/agent/LinkTaskMapper.xml +++ b/src/main/resources/mapper/agent/LinkTaskMapper.xml @@ -255,4 +255,25 @@ WHERE status = #{status} ORDER BY created_at ASC + + + + UPDATE link_task lt + LEFT JOIN ( + SELECT 1 as has_conflict + FROM link_task x + WHERE x.machine_id = #{deviceId} + AND x.status IN ('USING','LOGGED_IN') + AND x.id <> #{id} + ) conflict_check ON 1=1 + SET lt.status = 'USING', + lt.region = #{region}, + lt.machine_id = #{deviceId}, + lt.qr_created_at = NOW(), + lt.qr_expire_at = DATE_ADD(NOW(), INTERVAL #{qrExpireSeconds} SECOND), + lt.updated_at = NOW() + WHERE lt.id = #{id} + AND conflict_check.has_conflict IS NULL + + diff --git a/src/main/resources/mapper/cooldown/MachineCooldownMapper.xml b/src/main/resources/mapper/cooldown/MachineCooldownMapper.xml index f966fb9..7b76f25 100644 --- a/src/main/resources/mapper/cooldown/MachineCooldownMapper.xml +++ b/src/main/resources/mapper/cooldown/MachineCooldownMapper.xml @@ -44,10 +44,18 @@ - UPDATE machine_cooldown - SET status = 'EXPIRED', updated_at = NOW() - WHERE status = 'ACTIVE' - AND cooldown_end_time <= #{currentTime} + UPDATE machine_cooldown mc + LEFT JOIN ( + SELECT machine_id FROM ( + SELECT DISTINCT machine_id + FROM machine_cooldown + WHERE status = 'EXPIRED' + ) ex0 + ) ex ON ex.machine_id = mc.machine_id + SET mc.status = 'EXPIRED', mc.updated_at = NOW() + WHERE mc.status = 'ACTIVE' + AND mc.cooldown_end_time <= #{currentTime} + AND ex.machine_id IS NULL @@ -84,6 +92,19 @@ WHERE status = 'EXPIRED' AND updated_at < #{beforeTime} + + + + DELETE mc FROM machine_cooldown mc + JOIN ( + SELECT DISTINCT machine_id + FROM machine_cooldown + WHERE status = 'ACTIVE' + AND cooldown_end_time <= #{currentTime} + ) t ON t.machine_id = mc.machine_id + WHERE mc.status = 'EXPIRED' + +