feat: 优化设备冷却管理,增加原子设备占用逻辑和过期记录处理

This commit is contained in:
zyh
2025-09-13 10:46:52 +08:00
parent 86140b1294
commit 40479fa38e
16 changed files with 819 additions and 643 deletions

View File

@@ -303,3 +303,4 @@ WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
4. 说明当前系统状态 4. 说明当前系统状态
这样可以更快速地定位和解决问题。 这样可以更快速地定位和解决问题。

View File

@@ -96,3 +96,4 @@ CREATE TABLE `system_monitor` (
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统监控表' ROW_FORMAT = DYNAMIC; ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统监控表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -1,6 +1,8 @@
package com.gameplatform.server; package com.gameplatform.server;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
@@ -9,8 +11,13 @@ import org.springframework.scheduling.annotation.EnableScheduling;
@MapperScan("com.gameplatform.server.mapper") @MapperScan("com.gameplatform.server.mapper")
@EnableScheduling @EnableScheduling
public class GamePlatformServerApplication { public class GamePlatformServerApplication {
private static final Logger log = LoggerFactory.getLogger(GamePlatformServerApplication.class);
public static void main(String[] args) { public static void main(String[] args) {
log.info("=== 游戏平台服务器启动中 ===");
log.debug("Debug 日志级别已启用");
SpringApplication.run(GamePlatformServerApplication.class, args); SpringApplication.run(GamePlatformServerApplication.class, args);
log.info("=== 游戏平台服务器启动完成 ===");
} }
} }

View File

@@ -29,7 +29,7 @@ public class AuthController {
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public Mono<LoginResponse> login(@Valid @RequestBody LoginRequest req) { public Mono<LoginResponse> login(@Valid @RequestBody LoginRequest req) {
// Avoid logging raw usernames at info level // Avoid logging raw usernames at info level
log.debug("/api/auth/login called"); log.info("/api/auth/login called");
return authService.login(req); return authService.login(req);
} }

View File

@@ -15,8 +15,8 @@ import com.gameplatform.server.model.dto.link.UserLinkStatusResponse;
import com.gameplatform.server.model.dto.link.TargetScoreResponse; import com.gameplatform.server.model.dto.link.TargetScoreResponse;
import com.gameplatform.server.service.link.LinkGenerationService; import com.gameplatform.server.service.link.LinkGenerationService;
import com.gameplatform.server.service.link.LinkListService; 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.external.ScriptClient;
import com.gameplatform.server.service.link.LinkStatusService;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;

View File

@@ -115,4 +115,14 @@ public interface LinkTaskMapper extends BaseMapper<LinkTask> {
* 根据状态查询所有链接任务 * 根据状态查询所有链接任务
*/ */
List<LinkTask> findByStatus(@Param("status") String status); List<LinkTask> 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);
} }

View File

@@ -62,6 +62,12 @@ public interface MachineCooldownMapper extends BaseMapper<MachineCooldown> {
*/ */
int cleanupExpiredCooldowns(@Param("beforeTime") LocalDateTime beforeTime); 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<MachineCooldown> {
@Param("limit") int limit, @Param("limit") int limit,
@Param("offset") int offset); @Param("offset") int offset);
} }

View File

@@ -182,3 +182,4 @@ public class MachineCooldown {
'}'; '}';
} }
} }

View File

@@ -197,3 +197,4 @@ public class GameCompletionLog {
'}'; '}';
} }
} }

View File

@@ -3,7 +3,7 @@ package com.gameplatform.server.service.detection;
import com.gameplatform.server.mapper.agent.LinkTaskMapper; import com.gameplatform.server.mapper.agent.LinkTaskMapper;
import com.gameplatform.server.model.entity.agent.LinkTask; import com.gameplatform.server.model.entity.agent.LinkTask;
import com.gameplatform.server.model.entity.detection.GameCompletionLog; 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.detection.GameCompletionLogMapper;
import com.gameplatform.server.mapper.history.LinkTaskStatusHistoryMapper; import com.gameplatform.server.mapper.history.LinkTaskStatusHistoryMapper;
import com.gameplatform.server.model.entity.history.LinkTaskStatusHistory; 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 static final int COMPLETION_CONFIRMATION_INTERVAL_SECONDS = 10;
private final LinkTaskMapper linkTaskMapper; private final LinkTaskMapper linkTaskMapper;
private final MachineCooldownService machineCooldownService; private final MemoryMachineCooldownService machineCooldownService;
private final GameCompletionLogMapper gameCompletionLogMapper; private final GameCompletionLogMapper gameCompletionLogMapper;
private final LinkTaskStatusHistoryMapper statusHistoryMapper; private final LinkTaskStatusHistoryMapper statusHistoryMapper;
@@ -48,7 +48,7 @@ public class GameCompletionDetectionService {
private final ConcurrentMap<String, LocalDateTime> recentLogins = new ConcurrentHashMap<>(); private final ConcurrentMap<String, LocalDateTime> recentLogins = new ConcurrentHashMap<>();
public GameCompletionDetectionService(LinkTaskMapper linkTaskMapper, public GameCompletionDetectionService(LinkTaskMapper linkTaskMapper,
MachineCooldownService machineCooldownService, MemoryMachineCooldownService machineCooldownService,
GameCompletionLogMapper gameCompletionLogMapper, GameCompletionLogMapper gameCompletionLogMapper,
LinkTaskStatusHistoryMapper statusHistoryMapper) { LinkTaskStatusHistoryMapper statusHistoryMapper) {
this.linkTaskMapper = linkTaskMapper; this.linkTaskMapper = linkTaskMapper;
@@ -297,3 +297,4 @@ public class GameCompletionDetectionService {
LOW // 低置信度:不可信的状态变化 LOW // 低置信度:不可信的状态变化
} }
} }

View File

@@ -16,7 +16,7 @@ import com.gameplatform.server.service.external.ScriptClient;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import com.gameplatform.server.service.device.DeviceStatusCheckService; import com.gameplatform.server.service.device.DeviceStatusCheckService;
import com.gameplatform.server.service.admin.SystemConfigService; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -42,13 +42,13 @@ public class LinkStatusService {
private final ScriptClient scriptClient; private final ScriptClient scriptClient;
private final DeviceStatusCheckService deviceStatusCheckService; private final DeviceStatusCheckService deviceStatusCheckService;
private final SystemConfigService systemConfigService; private final SystemConfigService systemConfigService;
private final MachineCooldownService machineCooldownService; private final MemoryMachineCooldownService machineCooldownService;
private final DeviceAllocationService deviceAllocationService;
@Autowired(required = false) @Autowired(required = false)
private com.gameplatform.server.service.detection.GameCompletionDetectionService completionDetectionService; private com.gameplatform.server.service.detection.GameCompletionDetectionService completionDetectionService;
@Autowired(required = false) @Autowired(required = false)
private com.gameplatform.server.mapper.history.LinkTaskStatusHistoryMapper statusHistoryMapper; private com.gameplatform.server.mapper.history.LinkTaskStatusHistoryMapper statusHistoryMapper;
// 状态描述映射 // 状态描述映射
private static final Map<String, String> STATUS_DESC_MAP = new HashMap<>(); private static final Map<String, String> STATUS_DESC_MAP = new HashMap<>();
static { static {
@@ -62,13 +62,14 @@ public class LinkStatusService {
public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper, public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper,
ScriptClient scriptClient, ScriptClient scriptClient,
DeviceStatusCheckService deviceStatusCheckService, SystemConfigService systemConfigService, MachineCooldownService machineCooldownService) { DeviceStatusCheckService deviceStatusCheckService, SystemConfigService systemConfigService, MemoryMachineCooldownService machineCooldownService, DeviceAllocationService deviceAllocationService) {
this.linkTaskMapper = linkTaskMapper; this.linkTaskMapper = linkTaskMapper;
this.linkBatchMapper = linkBatchMapper; this.linkBatchMapper = linkBatchMapper;
this.scriptClient = scriptClient; this.scriptClient = scriptClient;
this.deviceStatusCheckService = deviceStatusCheckService; this.deviceStatusCheckService = deviceStatusCheckService;
this.systemConfigService = systemConfigService; this.systemConfigService = systemConfigService;
this.machineCooldownService = machineCooldownService; this.machineCooldownService = machineCooldownService;
this.deviceAllocationService = deviceAllocationService;
} }
/** /**
@@ -89,13 +90,13 @@ public class LinkStatusService {
// 查询链接任务 // 查询链接任务
LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo); LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo);
if (linkTask == null) { if (linkTask == null) {
throw new IllegalArgumentException("链接不存在: " + codeNo); throw new IllegalArgumentException("链接不存在 " + codeNo);
} }
// 查询批次信息 // 查询批次信息
LinkBatch linkBatch = linkBatchMapper.findById(linkTask.getBatchId()); LinkBatch linkBatch = linkBatchMapper.findById(linkTask.getBatchId());
if (linkBatch == null) { 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); response.setRemainingSeconds(0L);
} }
log.debug("链接状态查询完成: codeNo={}, status={}, isExpired={}, remainingSeconds={}", log.debug("链接状态查询完成 codeNo={}, status={}, isExpired={}, remainingSeconds={}",
codeNo, linkTask.getStatus(), isExpired, response.getRemainingSeconds()); codeNo, linkTask.getStatus(), isExpired, response.getRemainingSeconds());
return response; return response;
@@ -174,7 +175,7 @@ public class LinkStatusService {
// 1. 查询链接任务 // 1. 查询链接任务
LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo); LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo);
if (linkTask == null) { if (linkTask == null) {
log.error("链接任务不存在: codeNo={}", codeNo); log.error("链接任务不存在 codeNo={}", codeNo);
throw new IllegalArgumentException("链接不存在"); throw new IllegalArgumentException("链接不存在");
} }
@@ -209,9 +210,9 @@ public class LinkStatusService {
try { try {
// 同步调用脚本端退单接口 // 同步调用脚本端退单接口
String refundResult = scriptClient.refundOrder(machineId).block(); String refundResult = scriptClient.refundOrder(machineId).block();
log.info("脚本端退单接口调用成功: 设备={}, 结果={}", machineId, refundResult); log.info("脚本端退单接口调用成功, 设备={}, 结果={}", machineId, refundResult);
} catch (Exception e) { } catch (Exception e) {
log.error("脚本端退单接口调用失败: 设备={}, 错误={}", machineId, e.getMessage()); log.error("脚本端退单接口调用失败, 设备={}, 错误={}", machineId, e.getMessage());
// 即使脚本端调用失败,我们仍然继续更新数据库状态 // 即使脚本端调用失败,我们仍然继续更新数据库状态
// 这样可以确保用户能够看到退单状态,避免重复退单 // 这样可以确保用户能够看到退单状态,避免重复退单
} }
@@ -224,9 +225,9 @@ public class LinkStatusService {
linkTask.setRefundAt(LocalDateTime.now()); linkTask.setRefundAt(LocalDateTime.now());
linkTask.setUpdatedAt(LocalDateTime.now()); linkTask.setUpdatedAt(LocalDateTime.now());
int updateResult = linkTaskMapper.update(linkTask); int updateResult = linkTaskMapper.updateById(linkTask);
if (updateResult <= 0) { if (updateResult <= 0) {
log.error("更新链接状态失败: codeNo={}", codeNo); log.error("更新链接状态失败 codeNo={}", codeNo);
throw new RuntimeException("更新链接状态失败"); throw new RuntimeException("更新链接状态失败");
} }
@@ -251,7 +252,7 @@ public class LinkStatusService {
// 首先检查链接是否存在且属于该用户 // 首先检查链接是否存在且属于该用户
LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo); LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo);
if (linkTask == null) { if (linkTask == null) {
log.warn("链接不存在: codeNo={}", codeNo); log.warn("链接不存在 codeNo={}", codeNo);
throw new IllegalArgumentException("链接不存在"); throw new IllegalArgumentException("链接不存在");
} }
@@ -362,6 +363,7 @@ public class LinkStatusService {
}).subscribeOn(Schedulers.boundedElastic()); }).subscribeOn(Schedulers.boundedElastic());
} }
/** /**
* 批量删除链接(确保用户只能删除自己的链接) * 批量删除链接(确保用户只能删除自己的链接)
*/ */
@@ -458,7 +460,7 @@ public class LinkStatusService {
} }
if (linkTask == null) { if (linkTask == null) {
log.error("链接任务不存在: linkId={}, codeNo={}", linkId, codeNo); log.error("链接任务不存在 linkId={}, codeNo={}", linkId, codeNo);
throw new IllegalArgumentException("链接不存在"); throw new IllegalArgumentException("链接不存在");
} }
@@ -486,7 +488,7 @@ public class LinkStatusService {
log.warn("登录状态检测失败: {}", error.getMessage()); log.warn("登录状态检测失败: {}", error.getMessage());
}) })
.onErrorResume(error -> { .onErrorResume(error -> {
// 检测失败不影响后续流程,继续返回状态 // 检测失败不影响后续流程,继续返回当前状态
log.warn("检测失败,继续返回当前状态"); log.warn("检测失败,继续返回当前状态");
return Mono.empty(); return Mono.empty();
}) })
@@ -515,7 +517,7 @@ public class LinkStatusService {
} }
if (linkTask == null) { if (linkTask == null) {
log.error("链接任务不存在: linkId={}, codeNo={}", linkId, codeNo); log.error("链接任务不存在 linkId={}, codeNo={}", linkId, codeNo);
throw new IllegalArgumentException("链接不存在"); throw new IllegalArgumentException("链接不存在");
} }
@@ -529,11 +531,19 @@ public class LinkStatusService {
linkTask.setStatus("EXPIRED"); linkTask.setStatus("EXPIRED");
linkTask.setExpireAt(now); // 设置过期时间戳 linkTask.setExpireAt(now); // 设置过期时间戳
linkTask.setUpdatedAt(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); linkTaskMapper.update(linkTask);
} catch (Exception ignore) {}
UserLinkStatusResponse response = new UserLinkStatusResponse(); }
response.setStatus("EXPIRED");
return response;
} }
// 3. 检查 USING 状态的 10 分钟过期逻辑 // 3. 检查 USING 状态的 10 分钟过期逻辑
@@ -553,7 +563,19 @@ public class LinkStatusService {
linkTask.setStatus("EXPIRED"); linkTask.setStatus("EXPIRED");
linkTask.setExpireAt(now); // 设置过期时间戳 linkTask.setExpireAt(now); // 设置过期时间戳
linkTask.setUpdatedAt(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); linkTaskMapper.update(linkTask);
} catch (Exception ignore) {}
}
UserLinkStatusResponse response = new UserLinkStatusResponse(); UserLinkStatusResponse response = new UserLinkStatusResponse();
response.setStatus("EXPIRED"); response.setStatus("EXPIRED");
@@ -564,7 +586,7 @@ public class LinkStatusService {
log.info("链接状态是 USING执行自动刷新"); log.info("链接状态是 USING执行自动刷新");
// performAutoRefresh(linkTask); // performAutoRefresh(linkTask);
} else if ("LOGGED_IN".equals(linkTask.getStatus()) || "COMPLETED".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) { } else if ("LOGGED_IN".equals(linkTask.getStatus()) || "COMPLETED".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) {
// 已上号、已完成或已退状态,不需要刷新 // 已上号、已完成或已退状态,不需要刷新
log.info("链接状态为 {},不需要刷新", linkTask.getStatus()); log.info("链接状态为 {},不需要刷新", linkTask.getStatus());
} }
@@ -584,7 +606,6 @@ public class LinkStatusService {
/** /**
* 构建用户端状态响应 * 构建用户端状态响应
*/ */
@@ -620,7 +641,7 @@ public class LinkStatusService {
throw new IllegalArgumentException("code不能为空"); throw new IllegalArgumentException("code不能为空");
} }
if (region == null || (!region.equals("Q") && !region.equals("V"))) { if (region == null || (!region.equals("Q") && !region.equals("V"))) {
log.error("参数验证失败: region只能是Q或V, 当前值={}", region); log.error("参数验证失败: region只能是Q或V, 当前值{}", region);
throw new IllegalArgumentException("region只能是Q或V"); throw new IllegalArgumentException("region只能是Q或V");
} }
log.info("参数验证通过: code={}, region={}", code.trim(), region); log.info("参数验证通过: code={}, region={}", code.trim(), region);
@@ -649,10 +670,10 @@ public class LinkStatusService {
// 4. 检查链接状态只有NEW或USING状态才能选区 // 4. 检查链接状态只有NEW或USING状态才能选区
log.info("步骤4: 开始检查链接状态"); log.info("步骤4: 开始检查链接状态");
if (!"NEW".equals(linkTask.getStatus()) && !"USING".equals(linkTask.getStatus())) { if (!"NEW".equals(linkTask.getStatus()) && !"USING".equals(linkTask.getStatus())) {
log.error("链接状态检查失败: 当前状态={}, 只允许NEW或USING状态进行选区操作", linkTask.getStatus()); log.error("链接状态检查失败 当前状态{}, 只允许NEW或USING状态进行选区操作", linkTask.getStatus());
throw new IllegalArgumentException("链接状态不正确,只有新建或使用中状态的链接才能选区"); throw new IllegalArgumentException("链接状态不正确,只有新建或使用中状态的链接才能选区");
} }
log.info("链接状态检查通过: 当前状态={}, 允许进行选区操作", linkTask.getStatus()); log.info("链接状态检查通过: 当前状态{}, 允许进行选区操作", linkTask.getStatus());
// 5. 如果need_refresh=true检查是否已等待10秒 // 5. 如果need_refresh=true检查是否已等待10秒
@@ -661,7 +682,7 @@ public class LinkStatusService {
// long secondsSinceRefresh = ChronoUnit.SECONDS.between(linkTask.getRefreshTime(), now); // long secondsSinceRefresh = ChronoUnit.SECONDS.between(linkTask.getRefreshTime(), now);
// if (secondsSinceRefresh < 10) { // if (secondsSinceRefresh < 10) {
// long waitTime = 10 - secondsSinceRefresh; // long waitTime = 10 - secondsSinceRefresh;
// log.error("刷新后需要等待,剩余等待时间: {}秒", waitTime); // log.error("刷新后需要等待,剩余等待时间: {}秒, waitTime);
// SelectRegionResponse response = new SelectRegionResponse(false, "刷新后需要等待" + waitTime + "秒才能选区"); // SelectRegionResponse response = new SelectRegionResponse(false, "刷新后需要等待" + waitTime + "秒才能选区");
// return response; // return response;
// } // }
@@ -676,44 +697,26 @@ public class LinkStatusService {
// 检查是否有空闲设备 // 检查是否有空闲设备
if (deviceStatus.getAvailableCount() == 0) { if (deviceStatus.getAvailableCount() == 0) {
log.error("设备分配失败: 当前没有空闲设备可用, 总设备数={}, 空闲设备数={}", log.error("设备分配失败: 当前没有空闲设备可用, 总设备数={}, 空闲设备数{}",
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount()); deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount());
throw new RuntimeException("当前没有空闲设备,请稍后再试"); throw new RuntimeException("当前没有空闲设备,请稍后再试");
} }
log.info("空闲设备检查完成: 总设备数={}, 空闲设备数={}, 空闲设备列表={}", log.info("空闲设备检查完成 总设备数={}, 空闲设备数{}, 空闲设备列表={}",
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices()); deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices());
// 过滤掉冷却期内的设备 // 使用新的设备分配服务进行原子设备分配
List<String> availableDevices = deviceStatus.getAvailableDevices(); List<String> availableDevices = deviceStatus.getAvailableDevices();
List<String> nonCooldownDevices = new ArrayList<>(); selectedDeviceId = deviceAllocationService.allocateDevice(availableDevices, linkTask.getId(), "首次选区");
for (String deviceId : availableDevices) { if (selectedDeviceId == null) {
if (!machineCooldownService.isMachineInCooldown(deviceId)) { log.error("设备分配失败: 所有空闲设备都在冷却期内或被占用 总空闲设备数={}", availableDevices.size());
nonCooldownDevices.add(deviceId); throw new RuntimeException("所有设备都在冷却期内或被占用,请稍后再试");
}
log.info("首次选区设备分配成功: 选中设备={}, 任务ID={}", selectedDeviceId, linkTask.getId());
}else{ }else{
long remainingMinutes = machineCooldownService.getRemainingCooldownMinutes(deviceId); log.info("重复选区: 检查首次选区时有效性并复用设备");
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(); LocalDateTime firstSelectTime = linkTask.getFirstRegionSelectAt();
long expireSeconds = systemConfigService.getFirstRegionExpireSeconds(); long expireSeconds = systemConfigService.getFirstRegionExpireSeconds();
@@ -724,7 +727,7 @@ public class LinkStatusService {
firstSelectTime, expireTime, now, expireSeconds); firstSelectTime, expireTime, now, expireSeconds);
if (now.isAfter(expireTime)) { if (now.isAfter(expireTime)) {
log.error("链接过期检查失败: 首次选区已过期, firstSelectTime={}, expireTime={}, now={}", log.error("链接过期检查失败: 首次选区已过期 firstSelectTime={}, expireTime={}, now={}",
firstSelectTime, expireTime, now); firstSelectTime, expireTime, now);
// 将链接状态设置为过期 // 将链接状态设置为过期
@@ -738,13 +741,29 @@ public class LinkStatusService {
} }
selectedDeviceId = linkTask.getMachineId(); selectedDeviceId = linkTask.getMachineId();
log.info("重复选区设备复用成功: 使用之前分配的设备={}, 首次选区时间={}", selectedDeviceId, firstSelectTime);
// 检查之前分配的设备是否被其他链接任务占用
List<LinkTask> 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); log.info("执行设备刷新操作: 设备={}", selectedDeviceId);
scriptClient.refresh(selectedDeviceId).block(); scriptClient.refresh(selectedDeviceId).block();
log.info("设备刷新完成: 设备={}, 继续使用之前分配的设备", selectedDeviceId); log.info("设备刷新完成: 设备={}, 继续使用之前分配的设备", selectedDeviceId);
// 休眠5秒确保设备状态稳定 // 休眠5秒确保设备状态稳定
Thread.sleep(11000); Thread.sleep(5000);
log.info("等待5秒完成继续使用之前分配的设备: 设备={}", selectedDeviceId); log.info("等待5秒完成继续使用之前分配的设备 设备={}", selectedDeviceId);
} }
log.info("设备选择完成: 最终选中设备={}", selectedDeviceId); log.info("设备选择完成: 最终选中设备={}", selectedDeviceId);
@@ -752,59 +771,68 @@ public class LinkStatusService {
// 6. 检查该设备是否有之前的LOGGED_IN状态链接任务需要完成 // 6. 检查该设备是否有之前的LOGGED_IN状态链接任务需要完成
log.info("步骤6: 开始检查设备状态和历史任务"); log.info("步骤6: 开始检查设备状态和历史任务");
try { try {
log.info("检查设备历史任务: 设备={}, 触发原因=选区请求", selectedDeviceId); log.info("检查设备历史任务 设备={}, 触发原因=选区请求", selectedDeviceId);
deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(selectedDeviceId, "选区请求"); deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(selectedDeviceId, "选区请求");
log.info("设备历史任务检查完成: 设备={}", selectedDeviceId); log.info("设备历史任务检查完成 设备={}", selectedDeviceId);
} catch (Exception e) { } catch (Exception e) {
log.warn("设备历史任务检查异常: 设备={}, 错误={}, 继续选区流程", selectedDeviceId, e.getMessage()); log.warn("设备历史任务检查异常 设备={}, 错误={}, 继续选区流程", selectedDeviceId, e.getMessage());
// 不影响选区流程,只记录警告日志 // 不影响选区流程,只记录警告日志
} }
// 7. 调用保存总次数接口 // 7. 调用保存总次数接口
// log.info("步骤7: 开始保存总次数到脚本端"); // log.info("步骤7: 开始保存总次数到脚本端");
// try { // try {
// log.info("保存总次数: 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes()); // log.info("保存总次数 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes());
// scriptClient.saveTotalTimes(selectedDeviceId, linkBatch.getTimes()).block(); // scriptClient.saveTotalTimes(selectedDeviceId, linkBatch.getTimes()).block();
// log.info("总次数保存成功: 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes()); // log.info("总次数保存成功 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes());
// } catch (Exception e) { // } catch (Exception e) {
// log.warn("总次数保存失败: 设备={}, 次数={}, 错误={}, 继续后续流程", selectedDeviceId, linkBatch.getTimes(), e.getMessage()); // log.warn("总次数保存失败 设备={}, 次数={}, 错误={}, 继续后续流程", selectedDeviceId, linkBatch.getTimes(), e.getMessage());
// // 不影响后续流程,只记录警告日志 // // 不影响后续流程,只记录警告日志
// } // }
// 8. 调用脚本端选区,使用选中的设备 // 8. 调用脚本端选区,使用选中的设备
log.info("步骤8: 开始调用脚本端选区"); log.info("步骤8: 开始调用脚本端选区");
log.info("选区请求参数: 设备={}, 区域={}", selectedDeviceId, region); log.info("选区请求参数: 设备={}, 区域={}", selectedDeviceId, region);
String selectResult = scriptClient.selectRegion(selectedDeviceId, region).block();
String selectResult;
try {
selectResult = scriptClient.selectRegion(selectedDeviceId, region).block();
log.info("脚本端选区调用成功: 设备={}, 区域={}, 返回结果={}", selectedDeviceId, region, selectResult); 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. 等待脚本端生成二维码(这里可以添加轮询逻辑) // 11. 等待脚本端生成二维码(这里可以添加轮询逻辑)
// log.info("等待脚本端生成二维码等待3秒..."); // log.info("等待脚本端生成二维码等待3秒..");
// Thread.sleep(3000); // Thread.sleep(3000);
// 9. 更新数据库状态为USING保存设备信息 // 9. 原子占用设备并更新任务状态
log.info("步骤9: 开始更新数据库状态"); log.info("步骤9: 原子占用设备并更新任务状态");
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
LocalDateTime qrExpireAt = now.plusSeconds(60); // 60秒后过期 LocalDateTime qrExpireAt = now.plusSeconds(60); // 60秒后过期
// 记录更新前的状态 // 记录更新前的状态
log.info("数据库更新前状态: id={}, status={}, region={}, machineId={}, firstRegionSelectAt={}", log.info("数据库更新前状态 id={}, status={}, region={}, machineId={}, firstRegionSelectAt={}",
linkTask.getId(), linkTask.getStatus(), linkTask.getRegion(), linkTask.getMachineId(), linkTask.getFirstRegionSelectAt()); linkTask.getId(), linkTask.getStatus(), linkTask.getRegion(), linkTask.getMachineId(), linkTask.getFirstRegionSelectAt());
String prev = linkTask.getStatus(); String prev = linkTask.getStatus();
linkTask.setStatus("USING"); int affected = linkTaskMapper.reserveDeviceIfFree(linkTask.getId(), region, selectedDeviceId, 60);
linkTask.setRegion(region); if (affected == 0) {
linkTask.setCodeNo(code); log.warn("原子占用失败:设备已被其他任务占用 device={}codeNo={}", selectedDeviceId, linkTask.getCodeNo());
linkTask.setQrCreatedAt(now); throw new RuntimeException("设备已被占用,请重试");
linkTask.setQrExpireAt(qrExpireAt); }
if (linkTask.getFirstRegionSelectAt() == null) { if (linkTask.getFirstRegionSelectAt() == null) {
linkTask.setFirstRegionSelectAt(now); // 只在首次选区时记录 linkTask.setFirstRegionSelectAt(now);
log.info("首次选区: 记录首次选区时间={}", now); try { linkTaskMapper.update(linkTask); } catch (Exception ignore) {}
} }
linkTask.setNeedRefresh(false);
linkTask.setUpdatedAt(now);
linkTask.setMachineId(selectedDeviceId);
linkTaskMapper.update(linkTask);
// Audit and history for USING transition // Audit and history for USING transition
try { try {
com.gameplatform.server.util.AuditLogger.info("StatusTransition: codeNo={}, device={}, {}->USING, source=REGION_SELECT", linkTask.getCodeNo(), selectedDeviceId, prev); com.gameplatform.server.util.AuditLogger.info("StatusTransition: codeNo={}, device={}, {}->USING, source=REGION_SELECT", linkTask.getCodeNo(), selectedDeviceId, prev);
@@ -814,9 +842,30 @@ public class LinkStatusService {
)); ));
} }
} catch (Exception ignore) {} } catch (Exception ignore) {}
log.info("数据库更新成功: id={}, status=USING, region={}, machineId={}, qrCreatedAt={}, qrExpireAt={}, firstRegionSelectAt={}", log.info("数据库更新成功 id={}, status=USING, region={}, machineId={}, qrCreatedAt={}, qrExpireAt={}, firstRegionSelectAt={}",
linkTask.getId(), region, selectedDeviceId, now, qrExpireAt, linkTask.getFirstRegionSelectAt()); 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. 构建响应 // 10. 构建响应
log.info("步骤10: 开始构建响应数据"); log.info("步骤10: 开始构建响应数据");
SelectRegionResponse response = new SelectRegionResponse(true, "选区成功"); SelectRegionResponse response = new SelectRegionResponse(true, "选区成功");
@@ -827,7 +876,6 @@ public class LinkStatusService {
response.setStatus("USING"); response.setStatus("USING");
response.setQrDelaySeconds(10); // 客户端收到响应后等待5秒再请求二维码 response.setQrDelaySeconds(10); // 客户端收到响应后等待5秒再请求二维码
response.setMecmachineId(selectedDeviceId); // 便于调试和维护 response.setMecmachineId(selectedDeviceId); // 便于调试和维护
log.info("响应构建完成: success=true, status=USING, qrCodeUrl={}, qrDelaySeconds=5, machineId={}", log.info("响应构建完成: success=true, status=USING, qrCodeUrl={}, qrDelaySeconds=5, machineId={}",
qrCodeUrl, selectedDeviceId); qrCodeUrl, selectedDeviceId);
log.info("=== 选区操作完成 ==="); log.info("=== 选区操作完成 ===");
@@ -886,7 +934,7 @@ public class LinkStatusService {
// 检查链接状态只有USING状态才能轮询 // 检查链接状态只有USING状态才能轮询
if (!"USING".equals(linkTask.getStatus())) { if (!"USING".equals(linkTask.getStatus())) {
log.warn("链接状态不是USING当前状态: {}", linkTask.getStatus()); log.warn("链接状态不是USING当前状态 {}", linkTask.getStatus());
return new PollLoginResponse(false, 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) return scriptClient.checkLoginStatus(realDeviceId)
.map(loginResult -> processLoginResult(linkTask, realDeviceId, loginResult)) .map(loginResult -> processLoginResult(linkTask, realDeviceId, loginResult))
.onErrorResume(error -> { .onErrorResume(error -> {
log.warn("调用脚本端检查上号状态失败: codeNo={}, error={}", log.warn("调用脚本端检查上号状态失败 codeNo={}, error={}",
linkTask.getCodeNo(), error.getMessage()); linkTask.getCodeNo(), error.getMessage());
// 脚本端出错时,返回当前状态,不影响轮询 // 脚本端出错时,返回当前状态,不影响轮询
return Mono.just(new PollLoginResponse(false, "USING")); return Mono.just(new PollLoginResponse(false, "USING"));
@@ -934,7 +982,7 @@ public class LinkStatusService {
* 处理登录检查结果 * 处理登录检查结果
*/ */
private PollLoginResponse processLoginResult(LinkTask linkTask, String deviceId, String loginResult) { private PollLoginResponse processLoginResult(LinkTask linkTask, String deviceId, String loginResult) {
log.info("脚本端返回结果: {}", loginResult); log.info("脚本端返回结果 {}", loginResult);
// 检查是否已上号 // 检查是否已上号
if ("已上号".equals(loginResult) || "已登录".equals(loginResult)) { if ("已上号".equals(loginResult) || "已登录".equals(loginResult)) {
@@ -947,7 +995,7 @@ public class LinkStatusService {
log.info("============================================="); log.info("=============================================");
} catch (Exception e) { } catch (Exception e) {
log.warn("保存总次数接口调用失败: {}", e.getMessage()); log.warn("保存总次数接口调用失败 {}", e.getMessage());
// 不影响后续流程,只记录警告日志 // 不影响后续流程,只记录警告日志
} }
return handleSuccessfulLogin(linkTask, deviceId); return handleSuccessfulLogin(linkTask, deviceId);
@@ -992,7 +1040,7 @@ public class LinkStatusService {
log.warn("记录设备登录时间失败", 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", PollLoginResponse response = new PollLoginResponse(true, "LOGGED_IN", "SECOND",
@@ -1021,7 +1069,7 @@ public class LinkStatusService {
// 查询链接任务 // 查询链接任务
LinkTask linkTask = linkTaskMapper.findByCodeNo(code.trim()); LinkTask linkTask = linkTaskMapper.findByCodeNo(code.trim());
if (linkTask == null) { if (linkTask == null) {
log.warn("链接任务不存在: code={}", code); log.warn("链接任务不存在 code={}", code);
return null; return null;
} }
@@ -1031,7 +1079,7 @@ public class LinkStatusService {
return machineId; return machineId;
} catch (Exception e) { } 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; return null;
} }
} }
@@ -1060,27 +1108,27 @@ public class LinkStatusService {
// 2. 查询链接任务获取machineId // 2. 查询链接任务获取machineId
LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo.trim()); LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo.trim());
if (linkTask == null) { if (linkTask == null) {
log.error("链接任务不存在: codeNo={}", codeNo); log.error("链接任务不存在 codeNo={}", codeNo);
return TargetScoreResponse.error(codeNo, null, "链接不存在"); return TargetScoreResponse.error(codeNo, null, "链接不存在");
} }
String machineId = linkTask.getMachineId(); String machineId = linkTask.getMachineId();
if (machineId == null || machineId.trim().isEmpty()) { if (machineId == null || machineId.trim().isEmpty()) {
log.error("链接未关联设备: codeNo={}", codeNo); log.error("链接未关联设备 codeNo={}", codeNo);
return TargetScoreResponse.error(codeNo, null, "链接未关联设备"); return TargetScoreResponse.error(codeNo, null, "链接未关联设备");
} }
log.info("查询到设备ID: codeNo={}, machineId={}", codeNo, machineId); log.info("查询到设备ID: codeNo={}, machineId={}", codeNo, machineId);
// 3. 调用脚本端获取目标分数 // 3. 调用脚本端获取目标分数
log.info("调用脚本端获取目标分数: machineId={}", machineId); log.info("调用脚本端获取目标分数 machineId={}", machineId);
String targetScoreStr = scriptClient.getTargetScore(machineId).block(); String targetScoreStr = scriptClient.getTargetScore(machineId).block();
log.info("脚本端返回结果: machineId={}, result={}", machineId, targetScoreStr); log.info("脚本端返回结果 machineId={}, result={}", machineId, targetScoreStr);
// 4. 解析返回结果 // 4. 解析返回结果
if (targetScoreStr == null || targetScoreStr.trim().isEmpty()) { if (targetScoreStr == null || targetScoreStr.trim().isEmpty()) {
log.warn("脚本端返回空结果: machineId={}", machineId); log.warn("脚本端返回空结果: machineId={}", machineId);
return TargetScoreResponse.error(codeNo, machineId, "网络繁忙,稍后再试"); return TargetScoreResponse.error(codeNo, machineId, "网络拥堵,请稍后再试");
} }
String trimmedResult = targetScoreStr.trim(); String trimmedResult = targetScoreStr.trim();
@@ -1088,12 +1136,12 @@ public class LinkStatusService {
// 检查是否为数字 // 检查是否为数字
try { try {
Integer targetScore = Integer.parseInt(trimmedResult); Integer targetScore = Integer.parseInt(trimmedResult);
log.info("解析到数字目标分数: {}", targetScore); log.info("解析到数字目标分数 {}", targetScore);
// 5. 保存到数据库 // 5. 保存到数据库
linkTask.setCompletedPoints(targetScore); linkTask.setCompletedPoints(targetScore);
linkTask.setUpdatedAt(LocalDateTime.now()); linkTask.setUpdatedAt(LocalDateTime.now());
int updateResult = linkTaskMapper.update(linkTask); int updateResult = linkTaskMapper.updateById(linkTask); // Fixed the assignment here
if (updateResult > 0) { if (updateResult > 0) {
log.info("目标分数保存成功: codeNo={}, machineId={}, targetScore={}", log.info("目标分数保存成功: codeNo={}, machineId={}, targetScore={}",
@@ -1111,10 +1159,10 @@ public class LinkStatusService {
if ("空的".equals(trimmedResult) || "空闲".equals(trimmedResult) || "已运行".equals(trimmedResult)) { if ("空的".equals(trimmedResult) || "空闲".equals(trimmedResult) || "已运行".equals(trimmedResult)) {
log.info("设备状态为: {}", trimmedResult); log.info("设备状态为: {}", trimmedResult);
return TargetScoreResponse.error(codeNo, machineId, "网络繁忙,稍后再试"); return TargetScoreResponse.error(codeNo, machineId, "网络拥堵,请稍后再试");
} else { } else {
log.warn("未知的返回结果: {}", trimmedResult); log.warn("未知的返回结果 {}", trimmedResult);
return TargetScoreResponse.error(codeNo, machineId, "网络繁忙,稍后再试"); return TargetScoreResponse.error(codeNo, machineId, "网络拥堵,请稍后再试");
} }
} }
@@ -1125,3 +1173,6 @@ public class LinkStatusService {
} }
} }
} }

View File

@@ -1,6 +1,6 @@
package com.gameplatform.server.task; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
@@ -14,9 +14,9 @@ import org.springframework.stereotype.Component;
public class MachineCooldownCleanupTask { public class MachineCooldownCleanupTask {
private static final Logger log = LoggerFactory.getLogger(MachineCooldownCleanupTask.class); 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; this.machineCooldownService = machineCooldownService;
} }

View File

@@ -17,7 +17,7 @@ mybatis-plus:
type-aliases-package: com.gameplatform.server.model.entity type-aliases-package: com.gameplatform.server.model.entity
configuration: configuration:
map-underscore-to-camel-case: true 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: global-config:
db-config: db-config:
id-type: auto id-type: auto
@@ -37,10 +37,28 @@ management:
logging: logging:
level: level:
root: info root: info
com.gameplatform.server: info com.gameplatform.server: debug # 保持整体调试
com.baomidou.mybatisplus: info # 仅保留设备解析最终汇总INFO其余降级
org.apache.ibatis: info com.gameplatform.server.service.device.DeviceStatusService: info
com.zaxxer.hikari: 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: security:
jwt: jwt:

View File

@@ -1,7 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true"> <configuration scan="true">
<!-- Include Spring Boot defaults -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- Explicit console appender with UTF-8 to avoid garbled output on Windows -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<property name="LOG_PATH" value="logs"/> <property name="LOG_PATH" value="logs"/>
<!-- Optional: include file appender if you also want general file logs -->
<!-- <include resource="org/springframework/boot/logging/logback/file-appender.xml"/> -->
<!-- Audit appender for status transitions -->
<appender name="AUDIT-FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="AUDIT-FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/audit-status.log</file> <file>${LOG_PATH}/audit-status.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
@@ -23,6 +38,26 @@
<appender-ref ref="AUDIT-FILE"/> <appender-ref ref="AUDIT-FILE"/>
</logger> </logger>
<!-- Let Spring Boot default console/file config handle others --> <!-- General rolling file appender to mirror console output -->
</configuration> <appender name="GENERAL-FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/server.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/server.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>20MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- Root logger: send to console and general file -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="GENERAL-FILE"/>
</root>
</configuration>

View File

@@ -255,4 +255,25 @@
WHERE status = #{status} WHERE status = #{status}
ORDER BY created_at ASC ORDER BY created_at ASC
</select> </select>
<!-- 原子占用设备,避免并发下同一设备被多个链接占用 -->
<update id="reserveDeviceIfFree">
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 &lt;&gt; #{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
</update>
</mapper> </mapper>

View File

@@ -44,10 +44,18 @@
<!-- 批量更新过期的冷却记录状态 --> <!-- 批量更新过期的冷却记录状态 -->
<update id="batchUpdateExpiredCooldowns"> <update id="batchUpdateExpiredCooldowns">
UPDATE machine_cooldown UPDATE machine_cooldown mc
SET status = 'EXPIRED', updated_at = NOW() LEFT JOIN (
WHERE status = 'ACTIVE' SELECT machine_id FROM (
AND cooldown_end_time &lt;= #{currentTime} 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 &lt;= #{currentTime}
AND ex.machine_id IS NULL
</update> </update>
<!-- 手动移除设备的冷却状态 --> <!-- 手动移除设备的冷却状态 -->
@@ -85,6 +93,19 @@
AND updated_at &lt; #{beforeTime} AND updated_at &lt; #{beforeTime}
</delete> </delete>
<!-- 在批量将 ACTIVE 更新为 EXPIRED 前,
先删除这些设备已存在的 EXPIRED 记录,避免 (machine_id,status) 唯一键冲突 -->
<delete id="deleteExistingExpiredForMachinesToExpire">
DELETE mc FROM machine_cooldown mc
JOIN (
SELECT DISTINCT machine_id
FROM machine_cooldown
WHERE status = 'ACTIVE'
AND cooldown_end_time &lt;= #{currentTime}
) t ON t.machine_id = mc.machine_id
WHERE mc.status = 'EXPIRED'
</delete>
<!-- 获取指定设备的冷却历史记录 --> <!-- 获取指定设备的冷却历史记录 -->
<select id="getCooldownHistory" resultMap="MachineCooldownMap"> <select id="getCooldownHistory" resultMap="MachineCooldownMap">
SELECT * FROM machine_cooldown SELECT * FROM machine_cooldown
@@ -94,3 +115,4 @@
</select> </select>
</mapper> </mapper>