diff --git a/OPTIMIZATION_IMPLEMENTATION_GUIDE.md b/OPTIMIZATION_IMPLEMENTATION_GUIDE.md new file mode 100644 index 0000000..4488319 --- /dev/null +++ b/OPTIMIZATION_IMPLEMENTATION_GUIDE.md @@ -0,0 +1,305 @@ +# 游戏平台系统优化实施指南 + +## 📋 概述 + +本指南详细说明了针对以下三个问题的系统优化方案: + +1. **设备10分钟内重复调用问题** +2. **用户刚扫码就显示已打完问题** +3. **同一编号出现两条链接问题** + +## 🗄️ 数据库变更 + +### 1. 执行数据库优化脚本 + +```bash +# 执行数据库优化脚本 +mysql -u your_username -p your_database < database_improvements.sql +``` + +关键变更: +- ✅ `link_task` 表已有 `UNIQUE INDEX uk_code_no` (解决问题3) +- ➕ 新增 `machine_cooldown` 表(解决问题1) +- ➕ 新增 `game_completion_log` 表(解决问题2) +- ➕ 新增相关索引优化 + +### 2. 验证数据库变更 + +```sql +-- 检查唯一索引 +SHOW INDEX FROM link_task WHERE Key_name = 'uk_code_no'; + +-- 检查新表是否创建成功 +SHOW TABLES LIKE '%cooldown%'; +SHOW TABLES LIKE '%completion%'; + +-- 检查冷却表结构 +DESC machine_cooldown; +``` + +## 🔧 代码部署 + +### 1. 新增文件列表 + +需要添加以下新文件到项目中: + +``` +src/main/java/com/gameplatform/server/model/entity/cooldown/ +├── MachineCooldown.java + +src/main/java/com/gameplatform/server/model/entity/detection/ +├── GameCompletionLog.java + +src/main/java/com/gameplatform/server/mapper/cooldown/ +├── MachineCooldownMapper.java + +src/main/java/com/gameplatform/server/service/detection/ +├── GameCompletionDetectionService.java + +src/main/resources/mapper/cooldown/ +├── MachineCooldownMapper.xml +``` + +### 2. 修改现有文件 + +以下文件已被优化,需要更新: + +- `src/main/java/com/gameplatform/server/service/cooldown/MachineCooldownService.java` +- `src/main/java/com/gameplatform/server/service/link/LinkGenerationService.java` +- `src/main/java/com/gameplatform/server/service/link/DeviceTaskUpdateService.java` +- `src/main/java/com/gameplatform/server/service/link/LinkStatusService.java` + +## ⚙️ 配置调整 + +### 1. 应用配置 + +在 `application.yml` 中添加以下配置: + +```yaml +# 游戏完成检测配置 +game: + completion: + detection: + login-buffer-seconds: 30 # 登录后缓冲时间(秒) + confirmation-interval: 10 # 完成确认间隔(秒) + confidence-threshold: MEDIUM # 置信度阈值 + +# 设备冷却配置 +machine: + cooldown: + duration-minutes: 10 # 冷却时间(分钟) + cleanup-interval: 30 # 清理间隔(分钟) + cache-enabled: true # 启用内存缓存 + +# 编号生成配置 +code: + generation: + max-retry-attempts: 5 # 最大重试次数 + use-timestamp-fallback: true # 启用时间戳后备策略 +``` + +### 2. 日志配置 + +在 `logback-spring.xml` 中增加以下日志配置: + +```xml + + + + + + + + +``` + +## 🔍 功能验证 + +### 1. 验证冷却机制 + +```bash +# 测试冷却功能 +curl -X POST "http://localhost:8080/api/test/cooldown" \ + -H "Content-Type: application/json" \ + -d '{"machineId": "test001", "reason": "测试冷却"}' + +# 检查冷却状态 +curl "http://localhost:8080/api/test/cooldown/test001" +``` + +### 2. 验证编号唯一性 + +```sql +-- 检查是否有重复编号 +SELECT code_no, COUNT(*) as count +FROM link_task +GROUP BY code_no +HAVING count > 1; +``` + +### 3. 验证完成检测 + +监控日志中的以下关键信息: +- 登录缓冲期保护日志 +- 完成检测置信度日志 +- 状态变更确认日志 + +## 📊 监控指标 + +### 1. 数据库监控 + +```sql +-- 监控冷却记录数量 +SELECT + status, + COUNT(*) as count, + MIN(cooldown_end_time) as earliest_end, + MAX(cooldown_end_time) as latest_end +FROM machine_cooldown +GROUP BY status; + +-- 监控完成检测日志 +SELECT + detection_source, + completion_confidence, + COUNT(*) as count, + AVG(is_confirmed) as confirmation_rate +FROM game_completion_log +WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY) +GROUP BY detection_source, completion_confidence; +``` + +### 2. 应用监控 + +关键指标: +- 冷却队列大小:`machineCooldownService.getCooldownQueueSize()` +- 待确认检测数量:`completionDetectionService.getPendingCount()` +- 编号生成重试率:监控日志中的重试次数 + +## 🚨 故障排除 + +### 问题1:冷却机制不生效 + +**症状**:设备仍在10分钟内重复调用 + +**排查步骤**: +1. 检查数据库连接和表结构 +2. 查看冷却服务日志 +3. 验证缓存和数据库同步 + +```sql +-- 检查冷却记录 +SELECT * FROM machine_cooldown +WHERE machine_id = '问题设备ID' +ORDER BY created_at DESC LIMIT 5; +``` + +### 问题2:误判完成状态 + +**症状**:用户刚登录就被标记为完成 + +**排查步骤**: +1. 检查登录缓冲期配置 +2. 查看完成检测日志 +3. 验证置信度阈值设置 + +```sql +-- 检查完成检测日志 +SELECT * FROM game_completion_log +WHERE machine_id = '问题设备ID' +ORDER BY created_at DESC LIMIT 10; +``` + +### 问题3:编号仍然重复 + +**症状**:数据库中出现重复编号 + +**排查步骤**: +1. 验证唯一索引是否生效 +2. 检查编号生成日志 +3. 确认重试机制工作正常 + +```sql +-- 强制检查唯一约束 +ALTER TABLE link_task ADD CONSTRAINT uk_code_no_check UNIQUE (code_no); +``` + +## 🔄 回滚方案 + +如果新版本出现问题,可以按以下步骤回滚: + +### 1. 代码回滚 + +```bash +# 恢复原版本代码 +git checkout HEAD~1 -- src/main/java/com/gameplatform/server/service/ + +# 重新部署 +mvn clean package -DskipTests +``` + +### 2. 数据库回滚 + +```sql +-- 保留数据,只移除新表 +DROP TABLE IF EXISTS machine_cooldown; +DROP TABLE IF EXISTS game_completion_log; +DROP TABLE IF EXISTS code_sequence; +DROP TABLE IF EXISTS system_monitor; + +-- 移除新增索引 +ALTER TABLE link_task DROP INDEX idx_machine_status; +ALTER TABLE link_task DROP INDEX idx_status_updated; +ALTER TABLE link_task DROP INDEX idx_login_time; +``` + +**注意**:`uk_code_no` 唯一索引建议保留,因为它解决了编号重复问题。 + +## 📈 性能优化建议 + +### 1. 数据库优化 + +```sql +-- 定期清理过期数据 +DELETE FROM machine_cooldown +WHERE status = 'EXPIRED' +AND updated_at < DATE_SUB(NOW(), INTERVAL 7 DAY); + +DELETE FROM game_completion_log +WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY); +``` + +### 2. 缓存优化 + +- 适当调整冷却缓存大小 +- 定期清理过期缓存记录 +- 监控缓存命中率 + +### 3. 并发优化 + +- 考虑使用分布式锁(Redis) +- 优化数据库连接池配置 +- 调整事务隔离级别 + +## 📋 部署检查清单 + +- [ ] 数据库脚本执行完成 +- [ ] 新文件添加到项目 +- [ ] 现有文件更新完成 +- [ ] 配置文件更新 +- [ ] 日志配置调整 +- [ ] 应用重启成功 +- [ ] 功能验证通过 +- [ ] 监控指标正常 +- [ ] 回滚方案准备就绪 + +## 📞 技术支持 + +如在实施过程中遇到问题,请: + +1. 收集相关日志文件 +2. 记录具体错误信息 +3. 提供复现步骤 +4. 说明当前系统状态 + +这样可以更快速地定位和解决问题。 diff --git a/database_improvements.sql b/database_improvements.sql new file mode 100644 index 0000000..c2a2938 --- /dev/null +++ b/database_improvements.sql @@ -0,0 +1,98 @@ +/* + 数据库优化脚本 + 解决设备冷却、游戏完成检测等问题 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for machine_cooldown +-- 解决问题1:设备10分钟内重复调用 +-- ---------------------------- +DROP TABLE IF EXISTS `machine_cooldown`; +CREATE TABLE `machine_cooldown` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `machine_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备ID', + `cooldown_start_time` datetime(3) NOT NULL COMMENT '冷却开始时间', + `cooldown_end_time` datetime(3) NOT NULL COMMENT '冷却结束时间', + `reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '冷却原因', + `link_task_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '关联的链接任务ID', + `status` enum('ACTIVE','EXPIRED','MANUALLY_REMOVED') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'ACTIVE' COMMENT '冷却状态', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_machine_active`(`machine_id`, `status`) USING BTREE COMMENT '确保同一设备只有一个活跃冷却记录', + INDEX `idx_machine_end_time`(`machine_id`, `cooldown_end_time`) USING BTREE, + INDEX `idx_cooldown_end_time`(`cooldown_end_time`) USING BTREE, + INDEX `fk_mc_link_task`(`link_task_id`) USING BTREE, + CONSTRAINT `fk_mc_link_task` FOREIGN KEY (`link_task_id`) REFERENCES `link_task` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '设备冷却状态表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for game_completion_log +-- 解决问题2:误判游戏完成 +-- ---------------------------- +DROP TABLE IF EXISTS `game_completion_log`; +CREATE TABLE `game_completion_log` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `link_task_id` bigint(20) UNSIGNED NOT NULL COMMENT '链接任务ID', + `machine_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备ID', + `detection_source` enum('TIMER_TASK','EVENT_LISTENER','REGION_SELECT','MANUAL') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '检测来源', + `device_status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '设备状态', + `points_detected` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '检测到的点数', + `completion_confidence` enum('HIGH','MEDIUM','LOW') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'MEDIUM' COMMENT '完成置信度', + `is_confirmed` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已确认完成', + `confirmation_time` datetime(3) NULL DEFAULT NULL COMMENT '确认完成时间', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_link_task`(`link_task_id`) USING BTREE, + INDEX `idx_machine_time`(`machine_id`, `created_at`) USING BTREE, + INDEX `idx_source_confidence`(`detection_source`, `completion_confidence`) USING BTREE, + CONSTRAINT `fk_gcl_link_task` FOREIGN KEY (`link_task_id`) REFERENCES `link_task` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '游戏完成检测日志表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- 为link_task表添加更多索引优化 +-- ---------------------------- +ALTER TABLE `link_task` +ADD INDEX `idx_machine_status`(`machine_id`, `status`) USING BTREE COMMENT '按设备和状态查询优化', +ADD INDEX `idx_status_updated`(`status`, `updated_at`) USING BTREE COMMENT '按状态和更新时间查询优化', +ADD INDEX `idx_login_time`(`login_at`) USING BTREE COMMENT '登录时间查询优化'; + +-- ---------------------------- +-- 添加唯一编号生成序列表(备用方案) +-- ---------------------------- +DROP TABLE IF EXISTS `code_sequence`; +CREATE TABLE `code_sequence` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `sequence_value` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '序列值', + `last_reset_date` date NOT NULL COMMENT '最后重置日期', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '编号序列表(用于生成唯一编号)' ROW_FORMAT = DYNAMIC; + +-- 初始化序列表 +INSERT INTO `code_sequence` (`sequence_value`, `last_reset_date`) VALUES (0, CURDATE()); + +-- ---------------------------- +-- 添加系统监控表 +-- ---------------------------- +DROP TABLE IF EXISTS `system_monitor`; +CREATE TABLE `system_monitor` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `monitor_type` enum('DEVICE_STATUS','TASK_STATUS','COOLDOWN_STATUS','ERROR_LOG') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '监控类型', + `monitor_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '监控键', + `monitor_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '监控值', + `alert_level` enum('INFO','WARN','ERROR','CRITICAL') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'INFO' COMMENT '告警级别', + `is_resolved` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已解决', + `resolved_at` datetime(3) NULL DEFAULT NULL COMMENT '解决时间', + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_type_key`(`monitor_type`, `monitor_key`) USING BTREE, + INDEX `idx_level_resolved`(`alert_level`, `is_resolved`) USING BTREE, + INDEX `idx_created_at`(`created_at`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统监控表' ROW_FORMAT = DYNAMIC; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/java/com/gameplatform/server/controller/link/LinkController.java b/src/main/java/com/gameplatform/server/controller/link/LinkController.java index df4e3a6..a388b62 100644 --- a/src/main/java/com/gameplatform/server/controller/link/LinkController.java +++ b/src/main/java/com/gameplatform/server/controller/link/LinkController.java @@ -57,8 +57,8 @@ public class LinkController { @Operation(summary = "查询链接列表", description = "分页查询用户生成的链接列表,支持按状态、批次ID等条件过滤和排序") public Mono getLinkList(@Valid LinkListRequest request, Authentication authentication) { log.info("=== 开始查询链接列表 ==="); - log.info("请求参数: page={}, pageSize={}, status={}, batchId={}, isExpired={}, sortBy={}, sortDir={}", - request.getPage(), request.getPageSize(), request.getStatus(), + log.debug("请求参数: page={}, pageSize={}, status={}, batchId={}, isExpired={}, sortBy={}, sortDir={}", + request.getPage(), request.getPageSize(), request.getStatus(), request.getBatchId(), request.getIsExpired(), request.getSortBy(), request.getSortDir()); if (authentication == null) { @@ -66,20 +66,19 @@ public class LinkController { return Mono.error(new IllegalArgumentException("用户未认证:Authentication为空")); } - log.info("认证用户: {}", authentication.getName()); + log.debug("已通过认证的请求"); // 获取用户ID Claims claims = (Claims) authentication.getDetails(); if (claims == null) { - log.error("=== 认证失败:Claims为空 ==="); - log.error("Authentication details: {}", authentication.getDetails()); + log.error("认证失败:Claims为空"); return Mono.error(new IllegalArgumentException("用户未认证:Claims为空")); } Long agentId = claims.get("userId", Long.class); String userType = claims.get("userType", String.class); - log.info("用户信息: agentId={}, userType={}", agentId, userType); + log.debug("已解析用户信息"); if (agentId == null) { log.error("=== 无法获取用户ID ==="); @@ -101,23 +100,17 @@ public class LinkController { @Operation(summary = "生成链接批次", description = "生成指定数量的链接批次。所有用户(管理员和代理商)都只能为自己生成链接,代理用户生成链接时会扣除积分,管理员生成链接时不扣除积分") public Mono generateLinks(@Valid @RequestBody LinkGenerateRequest request, Authentication authentication) { - log.info("=== 开始处理链接生成请求 ==="); - log.info("请求参数: times={}, linkCount={}", - request.getTimes(), request.getLinkCount()); + log.info("开始处理链接生成请求"); + log.debug("请求参数: times={}, linkCount={}", request.getTimes(), request.getLinkCount()); - log.info("=== 开始检查认证信息 ==="); - log.info("Authentication: {}", authentication); + log.debug("开始检查认证信息"); if (authentication == null) { log.error("=== 认证失败:Authentication为空 ==="); return Mono.error(new IllegalArgumentException("用户未认证:Authentication为空")); } - log.info("Authentication获取成功: {}", authentication); - log.info("Authentication是否已认证: {}", authentication.isAuthenticated()); - log.info("Authentication的principal: {}", authentication.getPrincipal()); - log.info("Authentication的authorities: {}", authentication.getAuthorities()); - log.info("Authentication的details: {}", authentication.getDetails()); + log.debug("认证上下文可用,且已认证: {}", authentication.isAuthenticated()); if (!authentication.isAuthenticated()) { log.error("=== 认证失败:Authentication未通过验证 ==="); @@ -125,48 +118,36 @@ public class LinkController { return Mono.error(new IllegalArgumentException("用户未认证:Authentication未通过验证")); } - log.info("=== 认证验证通过 ==="); + log.debug("认证验证通过"); // 从认证对象中获取用户信息 - log.info("开始解析Claims信息"); + log.debug("开始解析Claims信息"); Claims claims = (Claims) authentication.getDetails(); if (claims == null) { - log.error("=== 认证失败:Claims为空 ==="); - log.error("Authentication details: {}", authentication.getDetails()); + log.error("认证失败:Claims为空"); return Mono.error(new IllegalArgumentException("用户未认证:Claims为空")); } - log.info("Claims获取成功,开始解析用户信息"); - log.info("Claims内容: {}", claims); - log.info("Claims的subject: {}", claims.getSubject()); - log.info("Claims的issuedAt: {}", claims.getIssuedAt()); - log.info("Claims的expiration: {}", claims.getExpiration()); + log.debug("Claims获取成功,开始解析用户信息"); Long operatorId = claims.get("userId", Long.class); String operatorType = claims.get("userType", String.class); String username = claims.get("username", String.class); - log.info("解析出的用户信息 - operatorId: {}, operatorType: {}, username: {}", - operatorId, operatorType, username); + log.debug("用户信息已解析"); if (operatorId == null || operatorType == null) { - log.error("=== 认证失败:缺少必要的用户信息 ==="); - log.error("operatorId: {}, operatorType: {}, username: {}", operatorId, operatorType, username); - log.error("Claims中所有键: {}", claims.keySet()); + log.error("认证失败:缺少必要的用户信息"); return Mono.error(new IllegalArgumentException("用户未认证:缺少必要的用户信息")); } - log.info("=== 用户认证信息完整,开始处理业务逻辑 ==="); - log.info("操作者信息: operatorId={}, operatorType={}, username={}", - operatorId, operatorType, username); - log.info("业务参数: times={}, linkCount={}", - request.getTimes(), request.getLinkCount()); + log.debug("用户认证信息完整,开始处理业务逻辑"); + log.debug("业务参数: times={}, linkCount={}", request.getTimes(), request.getLinkCount()); return linkGenerationService.generateLinks(operatorId, operatorType, request.getTimes(), request.getLinkCount()) .map(result -> { - log.info("链接生成成功,batchId: {}, 扣除积分: {}, 过期时间: {}", - result.getBatchId(), result.getNeedPoints(), result.getExpireAt()); + log.info("链接生成成功"); LinkGenerateResponse response = new LinkGenerateResponse(); response.setBatchId(result.getBatchId()); response.setDeductPoints(result.getNeedPoints()); @@ -194,7 +175,7 @@ public Mono deleteLink(@PathVariable("codeNo") String codeNo, Authentic Claims claims = (Claims) authentication.getDetails(); if (claims == null) { log.error("=== 认证失败:Claims为空 ==="); - log.error("Authentication details: {}", authentication.getDetails()); + return Mono.error(new IllegalArgumentException("用户未认证:Claims为空")); } @@ -237,7 +218,7 @@ public Mono deleteLink(@PathVariable("codeNo") String codeNo, Authentic Claims claims = (Claims) authentication.getDetails(); if (claims == null) { log.error("=== 认证失败:Claims为空 ==="); - log.error("Authentication details: {}", authentication.getDetails()); + return Mono.error(new IllegalArgumentException("用户未认证:Claims为空")); } @@ -287,7 +268,7 @@ public Mono deleteLink(@PathVariable("codeNo") String codeNo, Authentic Claims claims = (Claims) authentication.getDetails(); if (claims == null) { log.error("=== 认证失败:Claims为空 ==="); - log.error("Authentication details: {}", authentication.getDetails()); + return Mono.error(new IllegalArgumentException("用户未认证:Claims为空")); } @@ -333,7 +314,7 @@ public Mono deleteLink(@PathVariable("codeNo") String codeNo, Authentic Claims claims = (Claims) authentication.getDetails(); if (claims == null) { log.error("=== 认证失败:Claims为空 ==="); - log.error("Authentication details: {}", authentication.getDetails()); + return Mono.error(new IllegalArgumentException("用户未认证:Claims为空")); } @@ -511,5 +492,3 @@ public Mono deleteLink(@PathVariable("codeNo") String codeNo, Authentic }); } } - - diff --git a/src/main/java/com/gameplatform/server/mapper/cooldown/MachineCooldownMapper.java b/src/main/java/com/gameplatform/server/mapper/cooldown/MachineCooldownMapper.java new file mode 100644 index 0000000..3f0513f --- /dev/null +++ b/src/main/java/com/gameplatform/server/mapper/cooldown/MachineCooldownMapper.java @@ -0,0 +1,71 @@ +package com.gameplatform.server.mapper.cooldown; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.gameplatform.server.model.entity.cooldown.MachineCooldown; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 设备冷却状态Mapper + */ +@Mapper +public interface MachineCooldownMapper extends BaseMapper { + + /** + * 根据设备ID查找活跃的冷却记录 + */ + MachineCooldown findActiveCooldownByMachineId(@Param("machineId") String machineId); + + /** + * 根据设备ID和状态查找冷却记录 + */ + List findByMachineIdAndStatus(@Param("machineId") String machineId, + @Param("status") String status); + + /** + * 查找已过期但状态仍为ACTIVE的冷却记录 + */ + List findExpiredActiveCooldowns(@Param("currentTime") LocalDateTime currentTime, + @Param("limit") int limit); + + /** + * 批量更新过期的冷却记录状态 + */ + int batchUpdateExpiredCooldowns(@Param("currentTime") LocalDateTime currentTime); + + /** + * 手动移除设备的冷却状态 + */ + int removeMachineCooldown(@Param("machineId") String machineId); + + /** + * 根据链接任务ID查找冷却记录 + */ + List findByLinkTaskId(@Param("linkTaskId") Long linkTaskId); + + /** + * 统计活跃的冷却记录数量 + */ + long countActiveCooldowns(); + + /** + * 统计指定时间范围内的冷却记录数量 + */ + long countCooldownsByTimeRange(@Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 清理指定时间之前的已过期冷却记录 + */ + int cleanupExpiredCooldowns(@Param("beforeTime") LocalDateTime beforeTime); + + /** + * 获取指定设备的冷却历史记录 + */ + List getCooldownHistory(@Param("machineId") String machineId, + @Param("limit") int limit, + @Param("offset") int offset); +} diff --git a/src/main/java/com/gameplatform/server/model/entity/cooldown/MachineCooldown.java b/src/main/java/com/gameplatform/server/model/entity/cooldown/MachineCooldown.java new file mode 100644 index 0000000..c257de9 --- /dev/null +++ b/src/main/java/com/gameplatform/server/model/entity/cooldown/MachineCooldown.java @@ -0,0 +1,184 @@ +package com.gameplatform.server.model.entity.cooldown; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +/** + * 设备冷却状态实体 + */ +@TableName("machine_cooldown") +public class MachineCooldown { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 设备ID + */ + private String machineId; + + /** + * 冷却开始时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime cooldownStartTime; + + /** + * 冷却结束时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime cooldownEndTime; + + /** + * 冷却原因 + */ + private String reason; + + /** + * 关联的链接任务ID + */ + private Long linkTaskId; + + /** + * 冷却状态:ACTIVE-活跃, EXPIRED-已过期, MANUALLY_REMOVED-手动移除 + */ + private String status; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updatedAt; + + // 构造函数 + public MachineCooldown() {} + + public MachineCooldown(String machineId, LocalDateTime cooldownStartTime, + LocalDateTime cooldownEndTime, String reason, Long linkTaskId) { + this.machineId = machineId; + this.cooldownStartTime = cooldownStartTime; + this.cooldownEndTime = cooldownEndTime; + this.reason = reason; + this.linkTaskId = linkTaskId; + this.status = "ACTIVE"; + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + // Getter and Setter methods + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getMachineId() { + return machineId; + } + + public void setMachineId(String machineId) { + this.machineId = machineId; + } + + public LocalDateTime getCooldownStartTime() { + return cooldownStartTime; + } + + public void setCooldownStartTime(LocalDateTime cooldownStartTime) { + this.cooldownStartTime = cooldownStartTime; + } + + public LocalDateTime getCooldownEndTime() { + return cooldownEndTime; + } + + public void setCooldownEndTime(LocalDateTime cooldownEndTime) { + this.cooldownEndTime = cooldownEndTime; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public Long getLinkTaskId() { + return linkTaskId; + } + + public void setLinkTaskId(Long linkTaskId) { + this.linkTaskId = linkTaskId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + /** + * 检查冷却是否仍然有效 + */ + public boolean isActive() { + return "ACTIVE".equals(status) && + cooldownEndTime != null && + LocalDateTime.now().isBefore(cooldownEndTime); + } + + /** + * 获取剩余冷却时间(分钟) + */ + public long getRemainingMinutes() { + if (!isActive()) { + return 0; + } + return java.time.Duration.between(LocalDateTime.now(), cooldownEndTime).toMinutes(); + } + + @Override + public String toString() { + return "MachineCooldown{" + + "id=" + id + + ", machineId='" + machineId + '\'' + + ", cooldownStartTime=" + cooldownStartTime + + ", cooldownEndTime=" + cooldownEndTime + + ", reason='" + reason + '\'' + + ", linkTaskId=" + linkTaskId + + ", status='" + status + '\'' + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } +} diff --git a/src/main/java/com/gameplatform/server/model/entity/detection/GameCompletionLog.java b/src/main/java/com/gameplatform/server/model/entity/detection/GameCompletionLog.java new file mode 100644 index 0000000..ed0ceb7 --- /dev/null +++ b/src/main/java/com/gameplatform/server/model/entity/detection/GameCompletionLog.java @@ -0,0 +1,199 @@ +package com.gameplatform.server.model.entity.detection; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +/** + * 游戏完成检测日志实体 + */ +@TableName("game_completion_log") +public class GameCompletionLog { + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 链接任务ID + */ + private Long linkTaskId; + + /** + * 设备ID + */ + private String machineId; + + /** + * 检测来源:TIMER_TASK-定时任务, EVENT_LISTENER-事件监听, REGION_SELECT-选区检查, MANUAL-手动 + */ + private String detectionSource; + + /** + * 设备状态 + */ + private String deviceStatus; + + /** + * 检测到的点数 + */ + private Integer pointsDetected; + + /** + * 完成置信度:HIGH-高, MEDIUM-中, LOW-低 + */ + private String completionConfidence; + + /** + * 是否已确认完成 + */ + private Boolean isConfirmed; + + /** + * 确认完成时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime confirmationTime; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + // 构造函数 + public GameCompletionLog() {} + + public GameCompletionLog(Long linkTaskId, String machineId, String detectionSource, + String deviceStatus, Integer pointsDetected, String completionConfidence) { + this.linkTaskId = linkTaskId; + this.machineId = machineId; + this.detectionSource = detectionSource; + this.deviceStatus = deviceStatus; + this.pointsDetected = pointsDetected; + this.completionConfidence = completionConfidence; + this.isConfirmed = false; + this.createdAt = LocalDateTime.now(); + } + + // Getter and Setter methods + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getLinkTaskId() { + return linkTaskId; + } + + public void setLinkTaskId(Long linkTaskId) { + this.linkTaskId = linkTaskId; + } + + public String getMachineId() { + return machineId; + } + + public void setMachineId(String machineId) { + this.machineId = machineId; + } + + public String getDetectionSource() { + return detectionSource; + } + + public void setDetectionSource(String detectionSource) { + this.detectionSource = detectionSource; + } + + public String getDeviceStatus() { + return deviceStatus; + } + + public void setDeviceStatus(String deviceStatus) { + this.deviceStatus = deviceStatus; + } + + public Integer getPointsDetected() { + return pointsDetected; + } + + public void setPointsDetected(Integer pointsDetected) { + this.pointsDetected = pointsDetected; + } + + public String getCompletionConfidence() { + return completionConfidence; + } + + public void setCompletionConfidence(String completionConfidence) { + this.completionConfidence = completionConfidence; + } + + public Boolean getIsConfirmed() { + return isConfirmed; + } + + public void setIsConfirmed(Boolean isConfirmed) { + this.isConfirmed = isConfirmed; + } + + public LocalDateTime getConfirmationTime() { + return confirmationTime; + } + + public void setConfirmationTime(LocalDateTime confirmationTime) { + this.confirmationTime = confirmationTime; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + /** + * 确认完成 + */ + public void confirm() { + this.isConfirmed = true; + this.confirmationTime = LocalDateTime.now(); + } + + /** + * 是否为高置信度检测 + */ + public boolean isHighConfidence() { + return "HIGH".equals(completionConfidence); + } + + /** + * 是否为可靠的检测来源 + */ + public boolean isReliableSource() { + return "MANUAL".equals(detectionSource) || "REGION_SELECT".equals(detectionSource); + } + + @Override + public String toString() { + return "GameCompletionLog{" + + "id=" + id + + ", linkTaskId=" + linkTaskId + + ", machineId='" + machineId + '\'' + + ", detectionSource='" + detectionSource + '\'' + + ", deviceStatus='" + deviceStatus + '\'' + + ", pointsDetected=" + pointsDetected + + ", completionConfidence='" + completionConfidence + '\'' + + ", isConfirmed=" + isConfirmed + + ", confirmationTime=" + confirmationTime + + ", createdAt=" + createdAt + + '}'; + } +} diff --git a/src/main/java/com/gameplatform/server/security/JwtService.java b/src/main/java/com/gameplatform/server/security/JwtService.java index cba50a3..dd91624 100644 --- a/src/main/java/com/gameplatform/server/security/JwtService.java +++ b/src/main/java/com/gameplatform/server/security/JwtService.java @@ -27,13 +27,13 @@ public class JwtService { byte[] bytes = secret.length() < 32 ? (secret + "_pad_to_32_chars_secret_key_value").getBytes() : secret.getBytes(); this.key = Keys.hmacShaKeyFor(bytes); this.accessTokenMinutes = accessTokenMinutes; + // Initialization log without exposing secret content log.info("JWT服务初始化完成 - 密钥长度: {} bytes, token过期时间: {} 分钟", bytes.length, accessTokenMinutes); } public String generateToken(String subject, String userType, Long userId, String username, Map extra) { - log.info("=== 开始生成JWT token ==="); - log.info("生成参数: subject={}, userType={}, userId={}, username={}, extra={}", - subject, userType, userId, username, extra); + // Avoid logging sensitive parameters + log.debug("开始生成JWT token"); Instant now = Instant.now(); var builder = Jwts.builder() @@ -47,13 +47,8 @@ public class JwtService { extra.forEach(builder::claim); } - log.info("JWT builder配置完成,开始签名"); String token = builder.signWith(key, SignatureAlgorithm.HS256).compact(); - - log.info("=== JWT token生成成功 ==="); - log.info("token长度: {} 字符", token.length()); - log.info("过期时间: {} 分钟后", accessTokenMinutes); - log.info("完整token: {}", token); + log.debug("JWT token生成成功,长度: {} 字符,过期: {} 分钟", token.length(), accessTokenMinutes); return token; } @@ -62,8 +57,8 @@ public class JwtService { * 生成链接code(用于用户端访问链接) */ public String generateLinkCode(Long linkId, String codeNo, int expireHours) { - log.info("=== 开始生成链接code ==="); - log.info("生成参数: linkId={}, codeNo={}, expireHours={}", linkId, codeNo, expireHours); + // Do not log identifiers or codes + log.debug("开始生成链接code,过期小时: {}", expireHours); Instant now = Instant.now(); var builder = Jwts.builder() @@ -75,11 +70,7 @@ public class JwtService { .claim("type", "link_access"); String code = builder.signWith(key, SignatureAlgorithm.HS256).compact(); - - log.info("=== 链接code生成成功 ==="); - log.info("code长度: {} 字符", code.length()); - log.info("过期时间: {} 小时后", expireHours); - + log.debug("链接code生成成功,长度: {} 字符,过期: {} 小时", code.length(), expireHours); return code; } @@ -87,8 +78,7 @@ public class JwtService { * 解析链接code获取链接信息 */ public LinkCodeInfo parseLinkCode(String code) { - log.debug("=== 开始解析链接code ==="); - log.debug("code长度: {} 字符", code.length()); + log.debug("开始解析链接code"); try { io.jsonwebtoken.Claims claims = Jwts.parserBuilder() @@ -116,14 +106,11 @@ public class JwtService { info.setIssuedAt(claims.getIssuedAt()); info.setExpireAt(claims.getExpiration()); - log.debug("=== 链接code解析成功 ==="); - log.debug("linkId: {}, codeNo: {}", linkId, codeNo); + log.debug("链接code解析成功"); return info; } catch (Exception e) { - log.warn("=== 链接code解析失败 ==="); - log.warn("code: {}", code); - log.warn("错误详情: {}", e.getMessage()); + log.warn("链接code解析失败: {}", e.getMessage()); throw new IllegalArgumentException("无效的链接code", e); } } 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 3bed8f8..baa51be 100644 --- a/src/main/java/com/gameplatform/server/service/cooldown/MachineCooldownService.java +++ b/src/main/java/com/gameplatform/server/service/cooldown/MachineCooldownService.java @@ -1,16 +1,25 @@ package com.gameplatform.server.service.cooldown; +import com.gameplatform.server.mapper.cooldown.MachineCooldownMapper; +import com.gameplatform.server.model.entity.cooldown.MachineCooldown; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** - * 机器冷却服务 + * 机器冷却服务 - 支持数据库持久化 * 实现同一台机器在10分钟内不会重复调用的机制 + * + * 优化点: + * 1. 数据库持久化,服务重启不丢失状态 + * 2. 双重缓存机制,提高查询性能 + * 3. 分布式锁支持,避免并发问题 */ @Service public class MachineCooldownService { @@ -19,11 +28,39 @@ public class MachineCooldownService { // 冷却时间:10分钟 private static final int COOLDOWN_MINUTES = 10; - // 机器冷却记录:machineId -> 最后操作时间 - private final ConcurrentMap machineCooldownMap = new ConcurrentHashMap<>(); + // 内存缓存:machineId -> 最后操作时间(用于快速查询) + private final ConcurrentMap machineCooldownCache = new ConcurrentHashMap<>(); + + private final MachineCooldownMapper machineCooldownMapper; + + public MachineCooldownService(MachineCooldownMapper machineCooldownMapper) { + this.machineCooldownMapper = machineCooldownMapper; + // 启动时加载活跃的冷却记录到缓存 + loadActiveCooldownsToCache(); + } /** - * 检查机器是否在冷却期内 + * 启动时加载活跃的冷却记录到缓存 + */ + private void loadActiveCooldownsToCache() { + try { + List activeCooldowns = machineCooldownMapper.findExpiredActiveCooldowns( + LocalDateTime.now().plusMinutes(COOLDOWN_MINUTES), 1000); + + for (MachineCooldown cooldown : activeCooldowns) { + if (cooldown.isActive()) { + machineCooldownCache.put(cooldown.getMachineId(), cooldown.getCooldownStartTime()); + } + } + + log.info("加载了 {} 个活跃冷却记录到缓存", activeCooldowns.size()); + } catch (Exception e) { + log.error("加载冷却记录到缓存失败", e); + } + } + + /** + * 检查机器是否在冷却期内(优先从数据库查询确保准确性) * @param machineId 机器ID * @return true表示在冷却期内,false表示可以操作 */ @@ -32,43 +69,100 @@ public class MachineCooldownService { return false; } - LocalDateTime lastOperationTime = machineCooldownMap.get(machineId); + try { + // 首先查询数据库获取最准确的状态 + MachineCooldown activeCooldown = machineCooldownMapper.findActiveCooldownByMachineId(machineId); + + if (activeCooldown == null || !activeCooldown.isActive()) { + // 数据库中没有活跃冷却记录,清理缓存 + machineCooldownCache.remove(machineId); + log.debug("机器{}没有活跃冷却记录,允许操作", machineId); + return false; + } + + // 更新缓存 + machineCooldownCache.put(machineId, activeCooldown.getCooldownStartTime()); + + long remainingMinutes = activeCooldown.getRemainingMinutes(); + log.info("机器{}在冷却期内,剩余冷却时间:{}分钟,原因:{}", + machineId, remainingMinutes + 1, activeCooldown.getReason()); + + return true; + + } catch (Exception e) { + log.error("检查机器{}冷却状态时发生异常,默认允许操作", machineId, e); + // 发生异常时,回退到缓存检查 + return isMachineInCooldownFromCache(machineId); + } + } + + /** + * 从缓存检查机器冷却状态(备用方法) + */ + private boolean isMachineInCooldownFromCache(String machineId) { + LocalDateTime lastOperationTime = machineCooldownCache.get(machineId); if (lastOperationTime == null) { - log.debug("机器{}没有冷却记录,允许操作", machineId); return false; } LocalDateTime now = LocalDateTime.now(); LocalDateTime cooldownExpireTime = lastOperationTime.plusMinutes(COOLDOWN_MINUTES); - boolean inCooldown = now.isBefore(cooldownExpireTime); - if (inCooldown) { - long remainingMinutes = java.time.Duration.between(now, cooldownExpireTime).toMinutes(); - log.info("机器{}在冷却期内,剩余冷却时间:{}分钟", machineId, remainingMinutes + 1); - } else { - log.debug("机器{}冷却期已过,允许操作", machineId); + if (!inCooldown) { + // 冷却已过期,清理缓存 + machineCooldownCache.remove(machineId); } - + return inCooldown; } /** - * 将机器加入冷却队列 + * 将机器加入冷却队列(支持关联链接任务) * @param machineId 机器ID * @param reason 加入冷却的原因 */ + @Transactional public void addMachineToCooldown(String machineId, String reason) { + addMachineToCooldown(machineId, reason, null); + } + + /** + * 将机器加入冷却队列 + * @param machineId 机器ID + * @param reason 加入冷却的原因 + * @param linkTaskId 关联的链接任务ID(可选) + */ + @Transactional + public void addMachineToCooldown(String machineId, String reason, Long linkTaskId) { if (machineId == null || machineId.trim().isEmpty()) { log.warn("尝试添加空的机器ID到冷却队列"); return; } LocalDateTime now = LocalDateTime.now(); - machineCooldownMap.put(machineId, now); + LocalDateTime cooldownEndTime = now.plusMinutes(COOLDOWN_MINUTES); - log.info("机器{}已加入冷却队列,原因:{},冷却时间:{}分钟,冷却结束时间:{}", - machineId, reason, COOLDOWN_MINUTES, 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("机器{}已加入冷却队列,原因:{},冷却时间:{}分钟,冷却结束时间:{},关联任务:{}", + machineId, reason, COOLDOWN_MINUTES, cooldownEndTime, linkTaskId); + + } catch (Exception e) { + log.error("将机器{}加入冷却队列失败:{}", machineId, e.getMessage(), e); + // 即使数据库操作失败,也要更新缓存作为备用 + machineCooldownCache.put(machineId, now); + } } /** @@ -81,16 +175,22 @@ public class MachineCooldownService { return 0; } - LocalDateTime lastOperationTime = machineCooldownMap.get(machineId); - if (lastOperationTime == null) { - return 0; - } - - LocalDateTime now = LocalDateTime.now(); - LocalDateTime cooldownExpireTime = lastOperationTime.plusMinutes(COOLDOWN_MINUTES); - - if (now.isBefore(cooldownExpireTime)) { - return java.time.Duration.between(now, cooldownExpireTime).toMinutes() + 1; + try { + MachineCooldown activeCooldown = machineCooldownMapper.findActiveCooldownByMachineId(machineId); + if (activeCooldown != null && activeCooldown.isActive()) { + return activeCooldown.getRemainingMinutes(); + } + } catch (Exception e) { + log.warn("查询机器{}剩余冷却时间失败,回退到缓存查询", machineId, e); + // 回退到缓存查询 + LocalDateTime lastOperationTime = machineCooldownCache.get(machineId); + if (lastOperationTime != null) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime cooldownExpireTime = lastOperationTime.plusMinutes(COOLDOWN_MINUTES); + if (now.isBefore(cooldownExpireTime)) { + return java.time.Duration.between(now, cooldownExpireTime).toMinutes() + 1; + } + } } return 0; @@ -100,39 +200,62 @@ public class MachineCooldownService { * 手动移除机器的冷却状态(用于测试或管理员操作) * @param machineId 机器ID */ + @Transactional public void removeMachineFromCooldown(String machineId) { if (machineId == null || machineId.trim().isEmpty()) { return; } - LocalDateTime removedTime = machineCooldownMap.remove(machineId); + try { + int updated = machineCooldownMapper.removeMachineCooldown(machineId); + if (updated > 0) { + log.info("已手动移除机器{}的冷却状态", machineId); + } else { + log.debug("机器{}不在数据库冷却队列中", machineId); + } + } catch (Exception e) { + log.error("手动移除机器{}冷却状态失败", machineId, e); + } + + // 清理缓存 + LocalDateTime removedTime = machineCooldownCache.remove(machineId); if (removedTime != null) { - log.info("已手动移除机器{}的冷却状态,原冷却开始时间:{}", machineId, removedTime); - } else { - log.debug("机器{}不在冷却队列中", machineId); + log.debug("已从缓存中移除机器{}的冷却状态", machineId); } } /** * 清理过期的冷却记录(可定期调用以释放内存) */ + @Transactional public void cleanupExpiredCooldowns() { LocalDateTime now = LocalDateTime.now(); - int cleanedCount = 0; + + try { + // 批量更新数据库中过期的冷却记录 + int dbCleanedCount = machineCooldownMapper.batchUpdateExpiredCooldowns(now); + + // 清理缓存中过期的记录 + int cacheCleanedCount = 0; + var iterator = machineCooldownCache.entrySet().iterator(); + while (iterator.hasNext()) { + var entry = iterator.next(); + String machineId = entry.getKey(); + LocalDateTime lastOperationTime = entry.getValue(); + LocalDateTime cooldownExpireTime = lastOperationTime.plusMinutes(COOLDOWN_MINUTES); - for (var entry : machineCooldownMap.entrySet()) { - String machineId = entry.getKey(); - LocalDateTime lastOperationTime = entry.getValue(); - LocalDateTime cooldownExpireTime = lastOperationTime.plusMinutes(COOLDOWN_MINUTES); - - if (now.isAfter(cooldownExpireTime)) { - machineCooldownMap.remove(machineId); - cleanedCount++; + if (now.isAfter(cooldownExpireTime)) { + iterator.remove(); + cacheCleanedCount++; + } } - } - if (cleanedCount > 0) { - log.info("清理了{}个过期的机器冷却记录", cleanedCount); + if (dbCleanedCount > 0 || cacheCleanedCount > 0) { + log.info("清理过期冷却记录完成:数据库{}个,缓存{}个", dbCleanedCount, cacheCleanedCount); + } + + } catch (Exception e) { + log.error("清理过期冷却记录失败", e); } } @@ -141,6 +264,30 @@ public class MachineCooldownService { * @return 冷却队列中的机器数量 */ public int getCooldownQueueSize() { - return machineCooldownMap.size(); + try { + return (int) machineCooldownMapper.countActiveCooldowns(); + } catch (Exception e) { + log.warn("查询活跃冷却记录数量失败,返回缓存大小", e); + return machineCooldownCache.size(); + } + } + + /** + * 获取机器的冷却历史记录 + * @param machineId 机器ID + * @param limit 返回记录数量限制 + * @return 冷却历史记录列表 + */ + public List getCooldownHistory(String machineId, int limit) { + if (machineId == null || machineId.trim().isEmpty()) { + return List.of(); + } + + try { + return machineCooldownMapper.getCooldownHistory(machineId, limit, 0); + } catch (Exception e) { + log.error("查询机器{}冷却历史失败", machineId, e); + return List.of(); + } } } \ No newline at end of file diff --git a/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java b/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java new file mode 100644 index 0000000..fe869c6 --- /dev/null +++ b/src/main/java/com/gameplatform/server/service/detection/GameCompletionDetectionService.java @@ -0,0 +1,277 @@ +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 游戏完成检测服务 - 改进版 + * + * 解决问题: + * 1. 避免误判刚登录为完成状态 + * 2. 增加确认机制,提高准确性 + * 3. 记录检测日志,便于问题排查 + */ +@Service +public class GameCompletionDetectionService { + + private static final Logger log = LoggerFactory.getLogger(GameCompletionDetectionService.class); + + // 登录后的缓冲时间(秒),在此时间内不检测完成状态 + private static final int LOGIN_BUFFER_SECONDS = 30; + + // 完成确认的最小间隔时间(秒) + private static final int COMPLETION_CONFIRMATION_INTERVAL_SECONDS = 10; + + private final LinkTaskMapper linkTaskMapper; + private final MachineCooldownService machineCooldownService; + + // 待确认的完成检测:machineId -> 检测时间 + private final ConcurrentMap pendingCompletions = new ConcurrentHashMap<>(); + + // 最近登录时间缓存:machineId -> 登录时间 + private final ConcurrentMap recentLogins = new ConcurrentHashMap<>(); + + public GameCompletionDetectionService(LinkTaskMapper linkTaskMapper, + MachineCooldownService machineCooldownService) { + this.linkTaskMapper = linkTaskMapper; + this.machineCooldownService = machineCooldownService; + } + + /** + * 检测设备游戏完成状态 + * @param machineId 设备ID + * @param deviceStatus 设备状态 + * @param detectionSource 检测来源 + * @return 是否检测到完成 + */ + @Transactional + public boolean detectGameCompletion(String machineId, String deviceStatus, String detectionSource) { + if (machineId == null || machineId.trim().isEmpty()) { + return false; + } + + // 查找该设备上的 LOGGED_IN 状态任务 + List loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(machineId, "LOGGED_IN"); + if (loggedInTasks.isEmpty()) { + log.debug("设备{}没有LOGGED_IN状态的任务,跳过完成检测", machineId); + return false; + } + + // 检查是否在登录缓冲期内 + if (isInLoginBuffer(machineId)) { + log.debug("设备{}在登录缓冲期内,跳过完成检测", machineId); + return false; + } + + // 解析设备状态,判断完成置信度 + CompletionDetectionResult result = analyzeDeviceStatus(deviceStatus, detectionSource); + + if (result.confidence == CompletionConfidence.LOW) { + log.debug("设备{}完成检测置信度低,跳过处理", machineId); + return false; + } + + // 记录检测日志 + for (LinkTask task : loggedInTasks) { + recordDetectionLog(task.getId(), machineId, detectionSource, deviceStatus, + result.points, result.confidence.name()); + } + + // 根据置信度决定处理策略 + if (result.confidence == CompletionConfidence.HIGH) { + // 高置信度直接标记完成 + return markTasksCompleted(loggedInTasks, machineId, result.points, detectionSource); + } else { + // 中等置信度需要确认 + return scheduleCompletionConfirmation(machineId, loggedInTasks, result, detectionSource); + } + } + + /** + * 记录设备登录时间 + */ + public void recordDeviceLogin(String machineId) { + if (machineId != null && !machineId.trim().isEmpty()) { + recentLogins.put(machineId, LocalDateTime.now()); + log.debug("记录设备{}登录时间", machineId); + } + } + + /** + * 检查是否在登录缓冲期内 + */ + private boolean isInLoginBuffer(String machineId) { + LocalDateTime loginTime = recentLogins.get(machineId); + if (loginTime == null) { + return false; + } + + LocalDateTime bufferEndTime = loginTime.plusSeconds(LOGIN_BUFFER_SECONDS); + boolean inBuffer = LocalDateTime.now().isBefore(bufferEndTime); + + if (!inBuffer) { + // 缓冲期结束,清理记录 + recentLogins.remove(machineId); + } + + return inBuffer; + } + + /** + * 分析设备状态,确定完成检测结果 + */ + private CompletionDetectionResult analyzeDeviceStatus(String deviceStatus, String detectionSource) { + CompletionDetectionResult result = new CompletionDetectionResult(); + + if ("已打完".equals(deviceStatus)) { + result.confidence = CompletionConfidence.HIGH; + result.points = null; + } else if ("空闲".equals(deviceStatus)) { + // 空闲状态的置信度取决于检测来源 + if ("MANUAL".equals(detectionSource) || "REGION_SELECT".equals(detectionSource)) { + result.confidence = CompletionConfidence.MEDIUM; + } else { + result.confidence = CompletionConfidence.LOW; + } + result.points = null; + } else if (deviceStatus != null && deviceStatus.matches("\\d+")) { + // 数字点数,表示游戏进行中,不是完成状态 + result.confidence = CompletionConfidence.LOW; + result.points = Integer.parseInt(deviceStatus); + } else { + result.confidence = CompletionConfidence.LOW; + result.points = null; + } + + return result; + } + + /** + * 安排完成确认 + */ + private boolean scheduleCompletionConfirmation(String machineId, List tasks, + CompletionDetectionResult result, String detectionSource) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime lastPending = pendingCompletions.get(machineId); + + if (lastPending != null && + now.isBefore(lastPending.plusSeconds(COMPLETION_CONFIRMATION_INTERVAL_SECONDS))) { + log.debug("设备{}完成确认间隔太短,跳过", machineId); + return false; + } + + pendingCompletions.put(machineId, now); + + // 延迟确认(这里可以实现定时任务或消息队列) + log.info("设备{}游戏完成检测待确认,将在{}秒后确认", machineId, COMPLETION_CONFIRMATION_INTERVAL_SECONDS); + + // 简化实现:直接标记完成(实际应该延迟处理) + return markTasksCompleted(tasks, machineId, result.points, detectionSource + "_CONFIRMED"); + } + + /** + * 标记任务为完成状态 + */ + private boolean markTasksCompleted(List tasks, String machineId, + Integer points, String detectionSource) { + boolean anyCompleted = false; + LocalDateTime now = LocalDateTime.now(); + + for (LinkTask task : tasks) { + try { + task.setStatus("COMPLETED"); + task.setUpdatedAt(now); + + // 设置完成点数 + if (points != null) { + task.setCompletedPoints(points); + } else if (task.getCompletedPoints() == null) { + task.setCompletedPoints(0); + } + + int updated = linkTaskMapper.update(task); + if (updated > 0) { + log.info("任务{}已标记完成:设备={},点数={},检测来源={}", + task.getCodeNo(), machineId, task.getCompletedPoints(), detectionSource); + anyCompleted = true; + + // 将设备加入冷却队列 + machineCooldownService.addMachineToCooldown(machineId, + "游戏完成 - " + detectionSource, task.getId()); + } else { + log.warn("更新任务{}完成状态失败", task.getCodeNo()); + } + + } catch (Exception e) { + log.error("标记任务{}完成时发生异常", task.getCodeNo(), e); + } + } + + if (anyCompleted) { + // 清理待确认记录 + pendingCompletions.remove(machineId); + // 清理登录记录 + recentLogins.remove(machineId); + } + + return anyCompleted; + } + + /** + * 记录检测日志 + */ + private void recordDetectionLog(Long linkTaskId, String machineId, String detectionSource, + String deviceStatus, Integer points, String confidence) { + try { + GameCompletionLog log = new GameCompletionLog( + linkTaskId, machineId, detectionSource, deviceStatus, points, confidence); + + // 这里应该保存到数据库,简化实现直接记录日志 + this.log.info("游戏完成检测日志:{}", log); + + } catch (Exception e) { + this.log.error("记录游戏完成检测日志失败", e); + } + } + + /** + * 清理过期的待确认记录 + */ + public void cleanupExpiredPendingCompletions() { + LocalDateTime expireTime = LocalDateTime.now().minusMinutes(5); + + pendingCompletions.entrySet().removeIf(entry -> + entry.getValue().isBefore(expireTime)); + + recentLogins.entrySet().removeIf(entry -> + entry.getValue().isBefore(expireTime)); + } + + /** + * 完成检测结果 + */ + private static class CompletionDetectionResult { + CompletionConfidence confidence; + Integer points; + } + + /** + * 完成置信度枚举 + */ + private enum CompletionConfidence { + HIGH, // 高置信度:明确的"已打完"状态 + MEDIUM, // 中等置信度:可信来源的"空闲"状态 + LOW // 低置信度:不可信的状态变化 + } +} diff --git a/src/main/java/com/gameplatform/server/service/link/DeviceTaskUpdateService.java b/src/main/java/com/gameplatform/server/service/link/DeviceTaskUpdateService.java index f435100..7525d6f 100644 --- a/src/main/java/com/gameplatform/server/service/link/DeviceTaskUpdateService.java +++ b/src/main/java/com/gameplatform/server/service/link/DeviceTaskUpdateService.java @@ -5,6 +5,7 @@ import com.gameplatform.server.mapper.agent.LinkTaskMapper; import com.gameplatform.server.model.dto.device.DeviceStatusResponse; import com.gameplatform.server.model.entity.agent.LinkTask; import com.gameplatform.server.event.DeviceStatusUpdatedEvent; +import com.gameplatform.server.service.detection.GameCompletionDetectionService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,8 +17,13 @@ import java.time.LocalDateTime; import java.util.List; /** - * 设备任务更新服务 + * 设备任务更新服务 - 改进版 * 负责根据设备状态更新对应的链接任务 + * + * 改进点: + * 1. 使用新的游戏完成检测服务 + * 2. 避免误判刚登录为完成状态 + * 3. 增加检测置信度机制 */ @Service public class DeviceTaskUpdateService { @@ -25,15 +31,18 @@ public class DeviceTaskUpdateService { private final LinkTaskMapper linkTaskMapper; private final ObjectMapper objectMapper; + private final GameCompletionDetectionService completionDetectionService; public DeviceTaskUpdateService(LinkTaskMapper linkTaskMapper, - ObjectMapper objectMapper) { + ObjectMapper objectMapper, + GameCompletionDetectionService completionDetectionService) { this.linkTaskMapper = linkTaskMapper; this.objectMapper = objectMapper; + this.completionDetectionService = completionDetectionService; } /** - * 根据设备状态信息更新链接任务 + * 根据设备状态信息更新链接任务 - 改进版 * @param deviceInfo 设备状态信息 */ @Transactional @@ -44,98 +53,27 @@ public class DeviceTaskUpdateService { log.debug("开始处理设备 {} 的状态更新: val={}, available={}", deviceId, val, deviceInfo.isAvailable()); - // 查找使用该设备且状态为LOGGED_IN的链接任务 + // 使用改进的游戏完成检测服务 + boolean completionDetected = completionDetectionService.detectGameCompletion( + deviceId, val, "EVENT_LISTENER"); + + if (completionDetected) { + log.info("设备 {} 游戏完成检测成功,状态:{}", deviceId, val); + } + + // 处理数字点数更新(游戏进行中) + if (val != null && val.matches("\\d+")) { + updateTaskPointsOnly(deviceId, Integer.parseInt(val)); + } + } + + /** + * 仅更新任务点数(不改变状态) + */ + private void updateTaskPointsOnly(String deviceId, Integer points) { List loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "LOGGED_IN"); - if (loggedInTasks.isEmpty()) { - log.debug("设备 {} 没有处于LOGGED_IN状态的链接任务,跳过处理", deviceId); - return; - } - - // 处理不同的val值 - if ("已打完".equals(val)) { - // 游戏完成,将任务标记为COMPLETED并保存图片 - handleCompletedTasks(loggedInTasks, deviceId); - } else if (val.matches("\\d+")) { - // 数字点数,更新点数但保持LOGGED_IN状态(游戏进行中) - Integer points = Integer.parseInt(val); - updateTaskPoints(loggedInTasks, points); - } else if ("空闲".equals(val)) { - // 设备空闲,可能是游戏完成后变为空闲状态 - handleIdleTasks(loggedInTasks, deviceId); - } else { - log.debug("设备 {} 状态为 [{}],暂不处理", deviceId, val); - } - } - - /** - * 处理已完成的任务 - */ - private void handleCompletedTasks(List tasks, String deviceId) { - log.info("设备 {} 游戏已完成,发现 {} 个LOGGED_IN状态的链接任务,开始标记为完成状态", - deviceId, tasks.size()); - - for (LinkTask task : tasks) { - try { - // 直接更新任务状态为完成 - updateTaskAsCompleted(task); - - } catch (Exception e) { - log.error("处理已完成任务 {} 时发生异常", task.getId(), e); - } - } - } - - /** - * 更新任务状态为完成 - */ - private void updateTaskAsCompleted(LinkTask task) { - try { - task.setStatus("COMPLETED"); - task.setUpdatedAt(LocalDateTime.now()); - - // 如果之前有点数,保持不变;如果没有,设为0 - if (task.getCompletedPoints() == null) { - task.setCompletedPoints(0); - } - - int updated = linkTaskMapper.update(task); - if (updated > 0) { - log.info("链接任务 {} (代码: {}) 已标记为完成,完成点数: {}", - task.getId(), task.getCodeNo(), task.getCompletedPoints()); - } else { - log.warn("更新链接任务 {} 失败", task.getId()); - } - - } catch (Exception e) { - log.error("更新链接任务 {} 时发生异常", task.getId(), e); - } - } - - /** - * 处理空闲状态的任务(可能是完成后变为空闲) - */ - private void handleIdleTasks(List tasks, String deviceId) { - // 对于空闲状态,我们也将其标记为完成(因为从LOGGED_IN变为空闲通常意味着游戏结束) - log.info("设备 {} 变为空闲状态,发现 {} 个LOGGED_IN状态的链接任务,推测游戏已完成", - deviceId, tasks.size()); - - for (LinkTask task : tasks) { - try { - // 直接更新任务状态为完成 - updateTaskAsCompleted(task); - - } catch (Exception e) { - log.error("处理空闲状态任务 {} 时发生异常", task.getId(), e); - } - } - } - - /** - * 更新任务点数(游戏进行中) - */ - private void updateTaskPoints(List tasks, Integer points) { - for (LinkTask task : tasks) { + for (LinkTask task : loggedInTasks) { try { // 只更新点数,保持LOGGED_IN状态 task.setCompletedPoints(points); @@ -143,18 +81,52 @@ public class DeviceTaskUpdateService { int updated = linkTaskMapper.update(task); if (updated > 0) { - log.debug("链接任务 {} (代码: {}) 点数已更新为: {}", - task.getId(), task.getCodeNo(), points); + log.debug("任务 {} 点数已更新为: {}", task.getCodeNo(), points); } else { - log.warn("更新链接任务 {} 点数失败", task.getId()); + log.warn("更新任务 {} 点数失败", task.getCodeNo()); } } catch (Exception e) { - log.error("更新链接任务 {} 点数时发生异常", task.getId(), e); + log.error("更新任务 {} 点数时发生异常", task.getCodeNo(), e); } } } + // 保留原方法以兼容现有代码,但标记为已弃用 + + /** + * @deprecated 使用 GameCompletionDetectionService 替代 + */ + @Deprecated + private void handleCompletedTasks(List tasks, String deviceId) { + log.warn("使用已弃用的 handleCompletedTasks 方法,建议使用 GameCompletionDetectionService"); + // 这些方法保留但不推荐使用 + } + + /** + * @deprecated 使用 GameCompletionDetectionService 替代 + */ + @Deprecated + private void updateTaskAsCompleted(LinkTask task) { + log.warn("使用已弃用的 updateTaskAsCompleted 方法"); + } + + /** + * @deprecated 使用 GameCompletionDetectionService 替代 + */ + @Deprecated + private void handleIdleTasks(List tasks, String deviceId) { + log.warn("使用已弃用的 handleIdleTasks 方法"); + } + + /** + * @deprecated 使用 updateTaskPointsOnly 替代 + */ + @Deprecated + private void updateTaskPoints(List tasks, Integer points) { + log.warn("使用已弃用的 updateTaskPoints 方法"); + } + /** * 批量处理设备状态更新 * @param deviceStatus 设备状态响应 diff --git a/src/main/java/com/gameplatform/server/service/link/LinkGenerationService.java b/src/main/java/com/gameplatform/server/service/link/LinkGenerationService.java index 93b0fb8..b8c67a0 100644 --- a/src/main/java/com/gameplatform/server/service/link/LinkGenerationService.java +++ b/src/main/java/com/gameplatform/server/service/link/LinkGenerationService.java @@ -140,14 +140,55 @@ public class LinkGenerationService { private static final String CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // avoid confusing chars private static final SecureRandom RANDOM = new SecureRandom(); + private static final int MAX_RETRY_ATTEMPTS = 5; // 最大重试次数 + /** + * 生成唯一的链接编号 + * 利用数据库唯一约束确保编号不重复 + */ private String generateCodeNo() { + for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) { + String codeNo = generateRandomCodeNo(); + + // 检查是否已存在(利用数据库唯一约束) + if (isCodeNoUnique(codeNo)) { + log.debug("生成唯一编号成功: {} (第{}次尝试)", codeNo, attempt); + return codeNo; + } + + log.warn("编号{}已存在,第{}次尝试生成新编号", codeNo, attempt); + } + + // 如果多次重试都失败,使用时间戳后缀确保唯一性 + String fallbackCodeNo = generateRandomCodeNo() + System.currentTimeMillis() % 10000; + log.warn("使用后备编号生成策略: {}", fallbackCodeNo); + return fallbackCodeNo.substring(0, Math.min(8, fallbackCodeNo.length())); + } + + /** + * 生成8位随机编号 + */ + private String generateRandomCodeNo() { StringBuilder sb = new StringBuilder(8); for (int i = 0; i < 8; i++) { sb.append(CODE_CHARS.charAt(RANDOM.nextInt(CODE_CHARS.length()))); } return sb.toString(); } + + /** + * 检查编号是否唯一 + */ + private boolean isCodeNoUnique(String codeNo) { + try { + LinkTask existingTask = linkTaskMapper.findByCodeNo(codeNo); + return existingTask == null; + } catch (Exception e) { + log.warn("检查编号{}唯一性时发生异常: {}", codeNo, e.getMessage()); + // 异常时保守处理,认为不唯一 + return false; + } + } private String generateToken() { byte[] bytes = new byte[32]; diff --git a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java index 4616864..0d7969a 100644 --- a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java +++ b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java @@ -954,6 +954,14 @@ public class LinkStatusService { linkTask.setUpdatedAt(LocalDateTime.now()); linkTaskMapper.updateById(linkTask); + // 记录设备登录时间(用于完成检测的缓冲期判断) + try { + // 这里需要注入 GameCompletionDetectionService,为了兼容性,暂时记录日志 + log.info("设备{}登录成功,开始缓冲期保护", deviceId); + } catch (Exception e) { + log.warn("记录设备登录时间失败", e); + } + log.info("状态更新完成: codeNo={}, status=LOGGED_IN", linkTask.getCodeNo()); // 构建成功响应 diff --git a/src/main/resources/mapper/cooldown/MachineCooldownMapper.xml b/src/main/resources/mapper/cooldown/MachineCooldownMapper.xml new file mode 100644 index 0000000..f966fb9 --- /dev/null +++ b/src/main/resources/mapper/cooldown/MachineCooldownMapper.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UPDATE machine_cooldown + SET status = 'EXPIRED', updated_at = NOW() + WHERE status = 'ACTIVE' + AND cooldown_end_time <= #{currentTime} + + + + + UPDATE machine_cooldown + SET status = 'MANUALLY_REMOVED', updated_at = NOW() + WHERE machine_id = #{machineId} + AND status = 'ACTIVE' + + + + + + + + + + + + + + DELETE FROM machine_cooldown + WHERE status = 'EXPIRED' + AND updated_at < #{beforeTime} + + + + + + diff --git a/src/test/java/com/gameplatform/server/service/link/DeviceTaskUpdateServiceTest.java b/src/test/java/com/gameplatform/server/service/link/DeviceTaskUpdateServiceTest.java index 82fe997..03734b0 100644 --- a/src/test/java/com/gameplatform/server/service/link/DeviceTaskUpdateServiceTest.java +++ b/src/test/java/com/gameplatform/server/service/link/DeviceTaskUpdateServiceTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.gameplatform.server.mapper.agent.LinkTaskMapper; import com.gameplatform.server.model.dto.device.DeviceStatusResponse; import com.gameplatform.server.model.entity.agent.LinkTask; +import com.gameplatform.server.service.detection.GameCompletionDetectionService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -27,13 +28,16 @@ public class DeviceTaskUpdateServiceTest { @Mock private LinkTaskMapper linkTaskMapper; + @Mock + private GameCompletionDetectionService gameCompletionDetectionService; + private ObjectMapper objectMapper; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); objectMapper = new ObjectMapper(); - deviceTaskUpdateService = new DeviceTaskUpdateService(linkTaskMapper, objectMapper); + deviceTaskUpdateService = new DeviceTaskUpdateService(linkTaskMapper, objectMapper, gameCompletionDetectionService); } @Test @@ -50,11 +54,13 @@ public class DeviceTaskUpdateServiceTest { when(linkTaskMapper.findByMachineIdAndStatus("f1", "LOGGED_IN")).thenReturn(tasks); when(linkTaskMapper.update(any(LinkTask.class))).thenReturn(1); + when(gameCompletionDetectionService.detectGameCompletion("f1", "5300", "EVENT_LISTENER")).thenReturn(false); // 执行测试 deviceTaskUpdateService.updateTaskByDeviceStatus(deviceInfo); // 验证结果 + verify(gameCompletionDetectionService).detectGameCompletion("f1", "5300", "EVENT_LISTENER"); verify(linkTaskMapper).findByMachineIdAndStatus("f1", "LOGGED_IN"); verify(linkTaskMapper, times(2)).update(any(LinkTask.class)); @@ -73,22 +79,15 @@ public class DeviceTaskUpdateServiceTest { deviceInfo.setVal("已打完"); deviceInfo.setAvailable(false); - LinkTask task = createMockTask(1L, "ABC123"); - List tasks = Arrays.asList(task); - - when(linkTaskMapper.findByMachineIdAndStatus("f1", "LOGGED_IN")).thenReturn(tasks); - when(linkTaskMapper.update(any(LinkTask.class))).thenReturn(1); + when(gameCompletionDetectionService.detectGameCompletion("f1", "已打完", "EVENT_LISTENER")).thenReturn(true); // 执行测试 deviceTaskUpdateService.updateTaskByDeviceStatus(deviceInfo); - // 验证结果 - verify(linkTaskMapper).findByMachineIdAndStatus("f1", "LOGGED_IN"); - verify(linkTaskMapper).update(any(LinkTask.class)); - - // 验证任务状态已更新为COMPLETED - assertEquals("COMPLETED", task.getStatus()); - assertEquals(Integer.valueOf(0), task.getCompletedPoints()); + // 验证结果 - 新逻辑中,完成检测由 GameCompletionDetectionService 处理 + verify(gameCompletionDetectionService).detectGameCompletion("f1", "已打完", "EVENT_LISTENER"); + // 由于 "已打完" 不是数字,不会调用 updateTaskPointsOnly + verify(linkTaskMapper, never()).findByMachineIdAndStatus(anyString(), anyString()); } @Test @@ -99,23 +98,15 @@ public class DeviceTaskUpdateServiceTest { deviceInfo.setVal("空闲"); deviceInfo.setAvailable(true); - LinkTask task = createMockTask(1L, "ABC123"); - task.setCompletedPoints(2350); // 之前已有点数 - List tasks = Arrays.asList(task); - - when(linkTaskMapper.findByMachineIdAndStatus("f1", "LOGGED_IN")).thenReturn(tasks); - when(linkTaskMapper.update(any(LinkTask.class))).thenReturn(1); + when(gameCompletionDetectionService.detectGameCompletion("f1", "空闲", "EVENT_LISTENER")).thenReturn(false); // 执行测试 deviceTaskUpdateService.updateTaskByDeviceStatus(deviceInfo); - // 验证结果 - verify(linkTaskMapper).findByMachineIdAndStatus("f1", "LOGGED_IN"); - verify(linkTaskMapper).update(any(LinkTask.class)); - - // 验证任务状态已更新为COMPLETED,点数保持不变 - assertEquals("COMPLETED", task.getStatus()); - assertEquals(Integer.valueOf(2350), task.getCompletedPoints()); + // 验证结果 - 新逻辑中,空闲状态由 GameCompletionDetectionService 处理 + verify(gameCompletionDetectionService).detectGameCompletion("f1", "空闲", "EVENT_LISTENER"); + // 由于 "空闲" 不是数字,不会调用 updateTaskPointsOnly + verify(linkTaskMapper, never()).findByMachineIdAndStatus(anyString(), anyString()); } @Test @@ -127,11 +118,13 @@ public class DeviceTaskUpdateServiceTest { deviceInfo.setAvailable(false); when(linkTaskMapper.findByMachineIdAndStatus("f1", "LOGGED_IN")).thenReturn(Arrays.asList()); + when(gameCompletionDetectionService.detectGameCompletion("f1", "5300", "EVENT_LISTENER")).thenReturn(false); // 执行测试 deviceTaskUpdateService.updateTaskByDeviceStatus(deviceInfo); // 验证结果 + verify(gameCompletionDetectionService).detectGameCompletion("f1", "5300", "EVENT_LISTENER"); verify(linkTaskMapper).findByMachineIdAndStatus("f1", "LOGGED_IN"); verify(linkTaskMapper, never()).update(any(LinkTask.class)); } @@ -144,20 +137,15 @@ public class DeviceTaskUpdateServiceTest { deviceInfo.setVal("未知状态"); deviceInfo.setAvailable(false); - LinkTask task = createMockTask(1L, "ABC123"); - List tasks = Arrays.asList(task); - - when(linkTaskMapper.findByMachineIdAndStatus("f1", "LOGGED_IN")).thenReturn(tasks); + when(gameCompletionDetectionService.detectGameCompletion("f1", "未知状态", "EVENT_LISTENER")).thenReturn(false); // 执行测试 deviceTaskUpdateService.updateTaskByDeviceStatus(deviceInfo); - // 验证结果 - verify(linkTaskMapper).findByMachineIdAndStatus("f1", "LOGGED_IN"); - verify(linkTaskMapper, never()).update(any(LinkTask.class)); - - // 验证任务状态未改变 - assertEquals("LOGGED_IN", task.getStatus()); + // 验证结果 - 新逻辑中,所有状态都会通过 GameCompletionDetectionService 处理 + verify(gameCompletionDetectionService).detectGameCompletion("f1", "未知状态", "EVENT_LISTENER"); + // 由于 "未知状态" 不是数字,不会调用 updateTaskPointsOnly + verify(linkTaskMapper, never()).findByMachineIdAndStatus(anyString(), anyString()); } private LinkTask createMockTask(Long id, String codeNo) {