feat: 优化设备冷却管理,增加原子设备占用逻辑和过期记录处理
This commit is contained in:
@@ -303,3 +303,4 @@ WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
|
||||
4. 说明当前系统状态
|
||||
|
||||
这样可以更快速地定位和解决问题。
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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("=== 游戏平台服务器启动完成 ===");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ public class AuthController {
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public Mono<LoginResponse> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -115,4 +115,14 @@ public interface LinkTaskMapper extends BaseMapper<LinkTask> {
|
||||
* 根据状态查询所有链接任务
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,12 @@ public interface MachineCooldownMapper extends BaseMapper<MachineCooldown> {
|
||||
*/
|
||||
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("offset") int offset);
|
||||
}
|
||||
|
||||
|
||||
@@ -182,3 +182,4 @@ public class MachineCooldown {
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,3 +197,4 @@ public class GameCompletionLog {
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, LocalDateTime> 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 // 低置信度:不可信的状态变化
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, String> 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;
|
||||
@@ -174,7 +175,7 @@ public class LinkStatusService {
|
||||
// 1. 查询链接任务
|
||||
LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo);
|
||||
if (linkTask == null) {
|
||||
log.error("链接任务不存在: codeNo={}", codeNo);
|
||||
log.error("链接任务不存在 codeNo={}", codeNo);
|
||||
throw new IllegalArgumentException("链接不存在");
|
||||
}
|
||||
|
||||
@@ -209,9 +210,9 @@ 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());
|
||||
// 即使脚本端调用失败,我们仍然继续更新数据库状态
|
||||
// 这样可以确保用户能够看到退单状态,避免重复退单
|
||||
}
|
||||
@@ -224,9 +225,9 @@ public class LinkStatusService {
|
||||
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("更新链接状态失败");
|
||||
}
|
||||
|
||||
@@ -251,7 +252,7 @@ public class LinkStatusService {
|
||||
// 首先检查链接是否存在且属于该用户
|
||||
LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo);
|
||||
if (linkTask == null) {
|
||||
log.warn("链接不存在: codeNo={}", codeNo);
|
||||
log.warn("链接不存在 codeNo={}", codeNo);
|
||||
throw new IllegalArgumentException("链接不存在");
|
||||
}
|
||||
|
||||
@@ -362,6 +363,7 @@ public class LinkStatusService {
|
||||
}).subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 批量删除链接(确保用户只能删除自己的链接)
|
||||
*/
|
||||
@@ -458,7 +460,7 @@ public class LinkStatusService {
|
||||
}
|
||||
|
||||
if (linkTask == null) {
|
||||
log.error("链接任务不存在: linkId={}, codeNo={}", linkId, codeNo);
|
||||
log.error("链接任务不存在 linkId={}, codeNo={}", linkId, codeNo);
|
||||
throw new IllegalArgumentException("链接不存在");
|
||||
}
|
||||
|
||||
@@ -486,7 +488,7 @@ public class LinkStatusService {
|
||||
log.warn("登录状态检测失败: {}", error.getMessage());
|
||||
})
|
||||
.onErrorResume(error -> {
|
||||
// 检测失败不影响后续流程,继续返回状态
|
||||
// 检测失败不影响后续流程,继续返回当前状态
|
||||
log.warn("检测失败,继续返回当前状态");
|
||||
return Mono.empty();
|
||||
})
|
||||
@@ -515,7 +517,7 @@ public class LinkStatusService {
|
||||
}
|
||||
|
||||
if (linkTask == null) {
|
||||
log.error("链接任务不存在: linkId={}, codeNo={}", linkId, codeNo);
|
||||
log.error("链接任务不存在 linkId={}, codeNo={}", linkId, codeNo);
|
||||
throw new IllegalArgumentException("链接不存在");
|
||||
}
|
||||
|
||||
@@ -529,11 +531,19 @@ public class LinkStatusService {
|
||||
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);
|
||||
|
||||
UserLinkStatusResponse response = new UserLinkStatusResponse();
|
||||
response.setStatus("EXPIRED");
|
||||
return response;
|
||||
} catch (Exception ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查 USING 状态的 10 分钟过期逻辑
|
||||
@@ -553,7 +563,19 @@ public class LinkStatusService {
|
||||
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");
|
||||
@@ -564,7 +586,7 @@ public class LinkStatusService {
|
||||
log.info("链接状态是 USING,执行自动刷新");
|
||||
// performAutoRefresh(linkTask);
|
||||
} else if ("LOGGED_IN".equals(linkTask.getStatus()) || "COMPLETED".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) {
|
||||
// 已上号、已完成或已退款状态,不需要刷新
|
||||
// 已上号、已完成或已退单状态,不需要刷新
|
||||
log.info("链接状态为 {},不需要刷新", linkTask.getStatus());
|
||||
}
|
||||
|
||||
@@ -584,7 +606,6 @@ public class LinkStatusService {
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 构建用户端状态响应
|
||||
*/
|
||||
@@ -620,7 +641,7 @@ public class LinkStatusService {
|
||||
throw new IllegalArgumentException("code不能为空");
|
||||
}
|
||||
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");
|
||||
}
|
||||
log.info("参数验证通过: code={}, region={}", code.trim(), region);
|
||||
@@ -649,10 +670,10 @@ public class LinkStatusService {
|
||||
// 4. 检查链接状态,只有NEW或USING状态才能选区
|
||||
log.info("步骤4: 开始检查链接状态");
|
||||
if (!"NEW".equals(linkTask.getStatus()) && !"USING".equals(linkTask.getStatus())) {
|
||||
log.error("链接状态检查失败: 当前状态={}, 只允许NEW或USING状态进行选区操作", linkTask.getStatus());
|
||||
log.error("链接状态检查失败 当前状态{}, 只允许NEW或USING状态进行选区操作", linkTask.getStatus());
|
||||
throw new IllegalArgumentException("链接状态不正确,只有新建或使用中状态的链接才能选区");
|
||||
}
|
||||
log.info("链接状态检查通过: 当前状态={}, 允许进行选区操作", linkTask.getStatus());
|
||||
log.info("链接状态检查通过: 当前状态{}, 允许进行选区操作", linkTask.getStatus());
|
||||
|
||||
|
||||
// 5. 如果need_refresh=true,检查是否已等待10秒
|
||||
@@ -661,7 +682,7 @@ public class LinkStatusService {
|
||||
// long secondsSinceRefresh = ChronoUnit.SECONDS.between(linkTask.getRefreshTime(), now);
|
||||
// if (secondsSinceRefresh < 10) {
|
||||
// long waitTime = 10 - secondsSinceRefresh;
|
||||
// log.error("刷新后需要等待,剩余等待时间: {}秒", waitTime);
|
||||
// log.error("刷新后需要等待,剩余等待时间: {}秒, waitTime);
|
||||
// SelectRegionResponse response = new SelectRegionResponse(false, "刷新后需要等待" + waitTime + "秒才能选区");
|
||||
// return response;
|
||||
// }
|
||||
@@ -676,44 +697,26 @@ public class LinkStatusService {
|
||||
|
||||
// 检查是否有空闲设备
|
||||
if (deviceStatus.getAvailableCount() == 0) {
|
||||
log.error("设备分配失败: 当前没有空闲设备可用, 总设备数={}, 空闲设备数={}",
|
||||
log.error("设备分配失败: 当前没有空闲设备可用, 总设备数={}, 空闲设备数{}",
|
||||
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount());
|
||||
throw new RuntimeException("当前没有空闲设备,请稍后再试");
|
||||
}
|
||||
|
||||
log.info("空闲设备检查完成: 总设备数={}, 空闲设备数={}, 空闲设备列表={}",
|
||||
log.info("空闲设备检查完成 总设备数={}, 空闲设备数{}, 空闲设备列表={}",
|
||||
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices());
|
||||
|
||||
// 过滤掉冷却期内的设备
|
||||
// 使用新的设备分配服务进行原子设备分配
|
||||
List<String> availableDevices = deviceStatus.getAvailableDevices();
|
||||
List<String> nonCooldownDevices = new ArrayList<>();
|
||||
selectedDeviceId = deviceAllocationService.allocateDevice(availableDevices, linkTask.getId(), "首次选区");
|
||||
|
||||
for (String deviceId : availableDevices) {
|
||||
if (!machineCooldownService.isMachineInCooldown(deviceId)) {
|
||||
nonCooldownDevices.add(deviceId);
|
||||
if (selectedDeviceId == null) {
|
||||
log.error("设备分配失败: 所有空闲设备都在冷却期内或被占用 总空闲设备数={}", availableDevices.size());
|
||||
throw new RuntimeException("所有设备都在冷却期内或被占用,请稍后再试");
|
||||
}
|
||||
|
||||
log.info("首次选区设备分配成功: 选中设备={}, 任务ID={}", selectedDeviceId, linkTask.getId());
|
||||
}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("重复选区: 检查首次选区时效性并复用设备");
|
||||
log.info("重复选区: 检查首次选区时有效性并复用设备");
|
||||
// 检查首次选区是否已过期
|
||||
LocalDateTime firstSelectTime = linkTask.getFirstRegionSelectAt();
|
||||
long expireSeconds = systemConfigService.getFirstRegionExpireSeconds();
|
||||
@@ -724,7 +727,7 @@ public class LinkStatusService {
|
||||
firstSelectTime, expireTime, now, expireSeconds);
|
||||
|
||||
if (now.isAfter(expireTime)) {
|
||||
log.error("链接过期检查失败: 首次选区已过期, firstSelectTime={}, expireTime={}, now={}",
|
||||
log.error("链接过期检查失败: 首次选区已过期 firstSelectTime={}, expireTime={}, now={}",
|
||||
firstSelectTime, expireTime, now);
|
||||
|
||||
// 将链接状态设置为过期
|
||||
@@ -738,13 +741,29 @@ public class LinkStatusService {
|
||||
}
|
||||
|
||||
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);
|
||||
scriptClient.refresh(selectedDeviceId).block();
|
||||
log.info("设备刷新完成: 设备={}, 继续使用之前分配的设备", selectedDeviceId);
|
||||
// 休眠5秒,确保设备状态稳定
|
||||
Thread.sleep(11000);
|
||||
log.info("等待5秒完成,继续使用之前分配的设备: 设备={}", selectedDeviceId);
|
||||
Thread.sleep(5000);
|
||||
log.info("等待5秒完成,继续使用之前分配的设备 设备={}", selectedDeviceId);
|
||||
}
|
||||
|
||||
log.info("设备选择完成: 最终选中设备={}", selectedDeviceId);
|
||||
@@ -752,59 +771,68 @@ public class LinkStatusService {
|
||||
// 6. 检查该设备是否有之前的LOGGED_IN状态链接任务需要完成
|
||||
log.info("步骤6: 开始检查设备状态和历史任务");
|
||||
try {
|
||||
log.info("检查设备历史任务: 设备={}, 触发原因=选区请求", selectedDeviceId);
|
||||
log.info("检查设备历史任务 设备={}, 触发原因=选区请求", selectedDeviceId);
|
||||
deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(selectedDeviceId, "选区请求");
|
||||
log.info("设备历史任务检查完成: 设备={}", selectedDeviceId);
|
||||
log.info("设备历史任务检查完成 设备={}", selectedDeviceId);
|
||||
} catch (Exception e) {
|
||||
log.warn("设备历史任务检查异常: 设备={}, 错误={}, 继续选区流程", selectedDeviceId, e.getMessage());
|
||||
log.warn("设备历史任务检查异常 设备={}, 错误={}, 继续选区流程", selectedDeviceId, e.getMessage());
|
||||
// 不影响选区流程,只记录警告日志
|
||||
}
|
||||
|
||||
// 7. 调用保存总次数接口
|
||||
// log.info("步骤7: 开始保存总次数到脚本端");
|
||||
// try {
|
||||
// log.info("保存总次数: 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes());
|
||||
// log.info("保存总次数 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes());
|
||||
// scriptClient.saveTotalTimes(selectedDeviceId, linkBatch.getTimes()).block();
|
||||
// log.info("总次数保存成功: 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes());
|
||||
// log.info("总次数保存成功 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes());
|
||||
// } catch (Exception e) {
|
||||
// log.warn("总次数保存失败: 设备={}, 次数={}, 错误={}, 继续后续流程", selectedDeviceId, linkBatch.getTimes(), e.getMessage());
|
||||
// log.warn("总次数保存失败 设备={}, 次数={}, 错误={}, 继续后续流程", selectedDeviceId, linkBatch.getTimes(), e.getMessage());
|
||||
// // 不影响后续流程,只记录警告日志
|
||||
// }
|
||||
|
||||
// 8. 调用脚本端选区,使用选中的设备
|
||||
log.info("步骤8: 开始调用脚本端选区");
|
||||
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);
|
||||
} 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秒...");
|
||||
// log.info("等待脚本端生成二维码,等待3秒..");
|
||||
// Thread.sleep(3000);
|
||||
|
||||
// 9. 更新数据库状态为USING,保存设备信息
|
||||
log.info("步骤9: 开始更新数据库状态");
|
||||
// 9. 原子占用设备并更新任务状态
|
||||
log.info("步骤9: 原子占用设备并更新任务状态");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
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());
|
||||
|
||||
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);
|
||||
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) {}
|
||||
}
|
||||
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);
|
||||
@@ -814,9 +842,30 @@ public class LinkStatusService {
|
||||
));
|
||||
}
|
||||
} 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());
|
||||
|
||||
// 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, "选区成功");
|
||||
@@ -827,7 +876,6 @@ public class LinkStatusService {
|
||||
response.setStatus("USING");
|
||||
response.setQrDelaySeconds(10); // 客户端收到响应后,等待5秒再请求二维码
|
||||
response.setMecmachineId(selectedDeviceId); // 便于调试和维护
|
||||
|
||||
log.info("响应构建完成: success=true, status=USING, qrCodeUrl={}, qrDelaySeconds=5, machineId={}",
|
||||
qrCodeUrl, selectedDeviceId);
|
||||
log.info("=== 选区操作完成 ===");
|
||||
@@ -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);
|
||||
@@ -992,7 +1040,7 @@ public class LinkStatusService {
|
||||
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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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"/>
|
||||
|
||||
<!-- 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">
|
||||
<file>${LOG_PATH}/audit-status.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
@@ -23,6 +38,26 @@
|
||||
<appender-ref ref="AUDIT-FILE"/>
|
||||
</logger>
|
||||
|
||||
<!-- Let Spring Boot default console/file config handle others -->
|
||||
</configuration>
|
||||
<!-- General rolling file appender to mirror console output -->
|
||||
<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>
|
||||
|
||||
@@ -255,4 +255,25 @@
|
||||
WHERE status = #{status}
|
||||
ORDER BY created_at ASC
|
||||
</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 <> #{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>
|
||||
|
||||
@@ -44,10 +44,18 @@
|
||||
|
||||
<!-- 批量更新过期的冷却记录状态 -->
|
||||
<update id="batchUpdateExpiredCooldowns">
|
||||
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
|
||||
</update>
|
||||
|
||||
<!-- 手动移除设备的冷却状态 -->
|
||||
@@ -85,6 +93,19 @@
|
||||
AND updated_at < #{beforeTime}
|
||||
</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 <= #{currentTime}
|
||||
) t ON t.machine_id = mc.machine_id
|
||||
WHERE mc.status = 'EXPIRED'
|
||||
</delete>
|
||||
|
||||
<!-- 获取指定设备的冷却历史记录 -->
|
||||
<select id="getCooldownHistory" resultMap="MachineCooldownMap">
|
||||
SELECT * FROM machine_cooldown
|
||||
@@ -94,3 +115,4 @@
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user