From aaee312662eb8444acf76be7dd8f85b04cd24876 Mon Sep 17 00:00:00 2001 From: zyh Date: Tue, 9 Sep 2025 20:16:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=EF=BC=8C=E9=81=BF=E5=85=8D=E5=9C=A8=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E5=92=8C=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E6=97=B6?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E6=95=8F=E6=84=9F=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/auth/AuthController.java | 3 +- .../controller/link/QrProxyController.java | 31 +++++++++---------- .../server/mapper/lock/LockMapper.java | 22 +++++++++++++ .../cooldown/MachineCooldownService.java | 27 +++++++++------- .../db_migration_unique_machine_cooldown.sql | 6 ++++ 5 files changed, 61 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/gameplatform/server/mapper/lock/LockMapper.java create mode 100644 src/main/resources/db_migration_unique_machine_cooldown.sql 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 5487ab0..1ac17b1 100644 --- a/src/main/java/com/gameplatform/server/controller/auth/AuthController.java +++ b/src/main/java/com/gameplatform/server/controller/auth/AuthController.java @@ -28,7 +28,8 @@ public class AuthController { @PostMapping("/login") @ResponseStatus(HttpStatus.OK) public Mono login(@Valid @RequestBody LoginRequest req) { - log.info("/api/auth/login called username={}", req.getUsername()); + // Avoid logging raw usernames at info level + log.debug("/api/auth/login called"); return authService.login(req); } diff --git a/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java b/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java index d770f4f..bfcedc2 100644 --- a/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java +++ b/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java @@ -58,7 +58,7 @@ public class QrProxyController { // 通过codeNo查询machineId String machineId = linkStatusService.getMechainIdByCode(codeNo); if (machineId == null) { - log.warn("无法找到codeNo对应的machineId: {}", codeNo); + log.warn("无法找到codeNo对应的machineId"); return createNotFoundResponseMono(); } @@ -70,11 +70,11 @@ public class QrProxyController { .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=qr.png") .body(bytes)) .onErrorResume(WebClientResponseException.NotFound.class, ex -> { - log.warn("图片不存在: path={}", path); + log.warn("图片不存在"); return createNotFoundResponseMono(); }) .onErrorResume(WebClientResponseException.class, ex -> { - log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); + log.warn("获取图片失败: status={}, error={}", ex.getStatusCode(), ex.getMessage()); return Mono.just(ResponseEntity.status(ex.getStatusCode()).build()); }); } @@ -86,7 +86,7 @@ public class QrProxyController { .flatMap(linkStatus -> { String machineId = linkStatus.getMachineId(); if (machineId == null) { - log.warn("无法找到codeNo对应的machineId: {}", codeNo); + log.warn("无法找到codeNo对应的machineId"); return createNotFoundResponseMono(); } @@ -98,16 +98,16 @@ public class QrProxyController { .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=homepage.png") .body(bytes)) .onErrorResume(WebClientResponseException.NotFound.class, ex -> { - log.warn("图片不存在: path={}", path); + log.warn("图片不存在"); return createNotFoundResponseMono(); }) .onErrorResume(WebClientResponseException.class, ex -> { - log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); + log.warn("获取图片失败: status={}, error={}", ex.getStatusCode(), ex.getMessage()); return Mono.just(ResponseEntity.status(ex.getStatusCode()).build()); }); }) .onErrorResume(Exception.class, ex -> { - log.error("获取链接状态失败: codeNo={}, error={}", codeNo, ex.getMessage()); + log.error("获取链接状态失败: {}", ex.getMessage()); return createInternalServerErrorResponseMono(); }); } @@ -119,7 +119,7 @@ public class QrProxyController { .flatMap(linkStatus -> { String machineId = linkStatus.getMachineId(); if (machineId == null) { - log.warn("无法找到codeNo对应的machineId: {}", codeNo); + log.warn("无法找到codeNo对应的machineId"); return createNotFoundResponseMono(); } @@ -131,16 +131,16 @@ public class QrProxyController { .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=first-reward.png") .body(bytes)) .onErrorResume(WebClientResponseException.NotFound.class, ex -> { - log.warn("图片不存在: path={}", path); + log.warn("图片不存在"); return createNotFoundResponseMono(); }) .onErrorResume(WebClientResponseException.class, ex -> { - log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); + log.warn("获取图片失败: status={}, error={}", ex.getStatusCode(), ex.getMessage()); return Mono.just(ResponseEntity.status(ex.getStatusCode()).build()); }); }) .onErrorResume(Exception.class, ex -> { - log.error("获取链接状态失败: codeNo={}, error={}", codeNo, ex.getMessage()); + log.error("获取链接状态失败: {}", ex.getMessage()); return createInternalServerErrorResponseMono(); }); } @@ -152,7 +152,7 @@ public class QrProxyController { .flatMap(linkStatus -> { String machineId = linkStatus.getMachineId(); if (machineId == null) { - log.warn("无法找到codeNo对应的machineId: {}", codeNo); + log.warn("无法找到codeNo对应的machineId"); return createNotFoundResponseMono(); } @@ -164,16 +164,16 @@ public class QrProxyController { .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=mid-reward.png") .body(bytes)) .onErrorResume(WebClientResponseException.NotFound.class, ex -> { - log.warn("图片不存在: path={}", path); + log.warn("图片不存在"); return createNotFoundResponseMono(); }) .onErrorResume(WebClientResponseException.class, ex -> { - log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); + log.warn("获取图片失败: status={}, error={}", ex.getStatusCode(), ex.getMessage()); return Mono.just(ResponseEntity.status(ex.getStatusCode()).build()); }); }) .onErrorResume(Exception.class, ex -> { - log.error("获取链接状态失败: codeNo={}, error={}", codeNo, ex.getMessage()); + log.error("获取链接状态失败: {}", ex.getMessage()); return createInternalServerErrorResponseMono(); }); } @@ -315,4 +315,3 @@ public class QrProxyController { } } - diff --git a/src/main/java/com/gameplatform/server/mapper/lock/LockMapper.java b/src/main/java/com/gameplatform/server/mapper/lock/LockMapper.java new file mode 100644 index 0000000..e3824c3 --- /dev/null +++ b/src/main/java/com/gameplatform/server/mapper/lock/LockMapper.java @@ -0,0 +1,22 @@ +package com.gameplatform.server.mapper.lock; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface LockMapper { + + /** + * Try to acquire a named MySQL advisory lock. Returns 1 if acquired, 0 if timeout, NULL on error. + */ + @Select("SELECT GET_LOCK(#{name}, #{timeoutSeconds})") + Integer getLock(@Param("name") String name, @Param("timeoutSeconds") int timeoutSeconds); + + /** + * Release the named advisory lock. Returns 1 if released, 0 if not held, NULL on error. + */ + @Select("SELECT RELEASE_LOCK(#{name})") + Integer releaseLock(@Param("name") String name); +} + diff --git a/src/main/java/com/gameplatform/server/service/cooldown/MachineCooldownService.java b/src/main/java/com/gameplatform/server/service/cooldown/MachineCooldownService.java index baa51be..1595ba5 100644 --- a/src/main/java/com/gameplatform/server/service/cooldown/MachineCooldownService.java +++ b/src/main/java/com/gameplatform/server/service/cooldown/MachineCooldownService.java @@ -124,7 +124,12 @@ public class MachineCooldownService { */ @Transactional public void addMachineToCooldown(String machineId, String reason) { - addMachineToCooldown(machineId, reason, null); + try { + addMachineToCooldown(machineId, reason, null); + } catch (RuntimeException ex) { + // 当上层已完成占用(或并发占用导致唯一键冲突)时,吞掉异常以便上层逻辑继续 + log.warn("添加冷却记录失败或已存在,machineId={},原因={}(忽略)", machineId, ex.getMessage()); + } } /** @@ -144,24 +149,24 @@ public class MachineCooldownService { LocalDateTime cooldownEndTime = now.plusMinutes(COOLDOWN_MINUTES); try { - // 先移除该设备现有的活跃冷却记录 + // 先移除该设备现有的活跃冷却记录(若存在非并发场景下的残留) machineCooldownMapper.removeMachineCooldown(machineId); - - // 创建新的冷却记录 + + // 创建新的冷却记录(依赖数据库唯一约束防止并发重复占用) MachineCooldown cooldown = new MachineCooldown( machineId, now, cooldownEndTime, reason, linkTaskId); machineCooldownMapper.insert(cooldown); - + // 更新缓存 machineCooldownCache.put(machineId, now); - - log.info("机器{}已加入冷却队列,原因:{},冷却时间:{}分钟,冷却结束时间:{},关联任务:{}", + + log.info("机器{}已加入冷却队列,原因:{},冷却时间:{}分钟,冷却结束时间:{},关联任务:{}", machineId, reason, COOLDOWN_MINUTES, cooldownEndTime, linkTaskId); - + } catch (Exception e) { log.error("将机器{}加入冷却队列失败:{}", machineId, e.getMessage(), e); - // 即使数据库操作失败,也要更新缓存作为备用 - machineCooldownCache.put(machineId, now); + // 抛出异常,交由上层流程回滚/重试,避免同一设备被并发占用 + throw new RuntimeException("设备正忙或分配冲突,请稍后重试"); } } @@ -290,4 +295,4 @@ public class MachineCooldownService { return List.of(); } } -} \ No newline at end of file +} diff --git a/src/main/resources/db_migration_unique_machine_cooldown.sql b/src/main/resources/db_migration_unique_machine_cooldown.sql new file mode 100644 index 0000000..af7041b --- /dev/null +++ b/src/main/resources/db_migration_unique_machine_cooldown.sql @@ -0,0 +1,6 @@ +-- Add a unique constraint to ensure a device has at most one ACTIVE cooldown record at a time. +-- This helps prevent concurrent assignments of the same machine to multiple links. + +ALTER TABLE machine_cooldown + ADD UNIQUE KEY ux_machine_cooldown_active (machine_id, status); +