feat: 改进设备任务更新服务,集成游戏完成检测,优化状态处理逻辑

This commit is contained in:
zyh
2025-09-09 19:31:52 +08:00
parent 75112674e8
commit 3a09a4469b
14 changed files with 1595 additions and 243 deletions

View File

@@ -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
<!-- 冷却服务日志 -->
<logger name="com.gameplatform.server.service.cooldown" level="INFO" />
<!-- 完成检测服务日志 -->
<logger name="com.gameplatform.server.service.detection" level="INFO" />
<!-- 编号生成日志 -->
<logger name="com.gameplatform.server.service.link.LinkGenerationService" level="DEBUG" />
```
## 🔍 功能验证
### 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. 说明当前系统状态
这样可以更快速地定位和解决问题。

98
database_improvements.sql Normal file
View File

@@ -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;

View File

@@ -57,7 +57,7 @@ public class LinkController {
@Operation(summary = "查询链接列表", description = "分页查询用户生成的链接列表支持按状态、批次ID等条件过滤和排序")
public Mono<LinkListResponse> getLinkList(@Valid LinkListRequest request, Authentication authentication) {
log.info("=== 开始查询链接列表 ===");
log.info("请求参数: page={}, pageSize={}, status={}, batchId={}, isExpired={}, sortBy={}, sortDir={}",
log.debug("请求参数: page={}, pageSize={}, status={}, batchId={}, isExpired={}, sortBy={}, sortDir={}",
request.getPage(), request.getPageSize(), request.getStatus(),
request.getBatchId(), request.getIsExpired(), request.getSortBy(), request.getSortDir());
@@ -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<LinkGenerateResponse> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> deleteLink(@PathVariable("codeNo") String codeNo, Authentic
});
}
}

View File

@@ -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<MachineCooldown> {
/**
* 根据设备ID查找活跃的冷却记录
*/
MachineCooldown findActiveCooldownByMachineId(@Param("machineId") String machineId);
/**
* 根据设备ID和状态查找冷却记录
*/
List<MachineCooldown> findByMachineIdAndStatus(@Param("machineId") String machineId,
@Param("status") String status);
/**
* 查找已过期但状态仍为ACTIVE的冷却记录
*/
List<MachineCooldown> findExpiredActiveCooldowns(@Param("currentTime") LocalDateTime currentTime,
@Param("limit") int limit);
/**
* 批量更新过期的冷却记录状态
*/
int batchUpdateExpiredCooldowns(@Param("currentTime") LocalDateTime currentTime);
/**
* 手动移除设备的冷却状态
*/
int removeMachineCooldown(@Param("machineId") String machineId);
/**
* 根据链接任务ID查找冷却记录
*/
List<MachineCooldown> findByLinkTaskId(@Param("linkTaskId") Long linkTaskId);
/**
* 统计活跃的冷却记录数量
*/
long countActiveCooldowns();
/**
* 统计指定时间范围内的冷却记录数量
*/
long countCooldownsByTimeRange(@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
/**
* 清理指定时间之前的已过期冷却记录
*/
int cleanupExpiredCooldowns(@Param("beforeTime") LocalDateTime beforeTime);
/**
* 获取指定设备的冷却历史记录
*/
List<MachineCooldown> getCooldownHistory(@Param("machineId") String machineId,
@Param("limit") int limit,
@Param("offset") int offset);
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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<String, Object> 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);
}
}

View File

@@ -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<String, LocalDateTime> machineCooldownMap = new ConcurrentHashMap<>();
// 内存缓存machineId -> 最后操作时间(用于快速查询)
private final ConcurrentMap<String, LocalDateTime> machineCooldownCache = new ConcurrentHashMap<>();
private final MachineCooldownMapper machineCooldownMapper;
public MachineCooldownService(MachineCooldownMapper machineCooldownMapper) {
this.machineCooldownMapper = machineCooldownMapper;
// 启动时加载活跃的冷却记录到缓存
loadActiveCooldownsToCache();
}
/**
* 检查机器是否在冷却期内
* 启动时加载活跃的冷却记录到缓存
*/
private void loadActiveCooldownsToCache() {
try {
List<MachineCooldown> 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;
for (var entry : machineCooldownMap.entrySet()) {
String machineId = entry.getKey();
LocalDateTime lastOperationTime = entry.getValue();
LocalDateTime cooldownExpireTime = lastOperationTime.plusMinutes(COOLDOWN_MINUTES);
try {
// 批量更新数据库中过期的冷却记录
int dbCleanedCount = machineCooldownMapper.batchUpdateExpiredCooldowns(now);
if (now.isAfter(cooldownExpireTime)) {
machineCooldownMap.remove(machineId);
cleanedCount++;
// 清理缓存中过期的记录
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);
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<MachineCooldown> 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();
}
}
}

View File

@@ -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<String, LocalDateTime> pendingCompletions = new ConcurrentHashMap<>();
// 最近登录时间缓存machineId -> 登录时间
private final ConcurrentMap<String, LocalDateTime> 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<LinkTask> 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<LinkTask> 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<LinkTask> 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 // 低置信度:不可信的状态变化
}
}

View File

@@ -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<LinkTask> 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<LinkTask> 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<LinkTask> 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<LinkTask> 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<LinkTask> 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<LinkTask> tasks, String deviceId) {
log.warn("使用已弃用的 handleIdleTasks 方法");
}
/**
* @deprecated 使用 updateTaskPointsOnly 替代
*/
@Deprecated
private void updateTaskPoints(List<LinkTask> tasks, Integer points) {
log.warn("使用已弃用的 updateTaskPoints 方法");
}
/**
* 批量处理设备状态更新
* @param deviceStatus 设备状态响应

View File

@@ -140,8 +140,35 @@ 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())));
@@ -149,6 +176,20 @@ public class LinkGenerationService {
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];
RANDOM.nextBytes(bytes);

View File

@@ -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());
// 构建成功响应

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gameplatform.server.mapper.cooldown.MachineCooldownMapper">
<!-- 结果映射 -->
<resultMap id="MachineCooldownMap" type="com.gameplatform.server.model.entity.cooldown.MachineCooldown">
<id property="id" column="id" />
<result property="machineId" column="machine_id" />
<result property="cooldownStartTime" column="cooldown_start_time" />
<result property="cooldownEndTime" column="cooldown_end_time" />
<result property="reason" column="reason" />
<result property="linkTaskId" column="link_task_id" />
<result property="status" column="status" />
<result property="createdAt" column="created_at" />
<result property="updatedAt" column="updated_at" />
</resultMap>
<!-- 根据设备ID查找活跃的冷却记录 -->
<select id="findActiveCooldownByMachineId" parameterType="string" resultMap="MachineCooldownMap">
SELECT * FROM machine_cooldown
WHERE machine_id = #{machineId}
AND status = 'ACTIVE'
AND cooldown_end_time > NOW()
ORDER BY created_at DESC
LIMIT 1
</select>
<!-- 根据设备ID和状态查找冷却记录 -->
<select id="findByMachineIdAndStatus" resultMap="MachineCooldownMap">
SELECT * FROM machine_cooldown
WHERE machine_id = #{machineId}
AND status = #{status}
ORDER BY created_at DESC
</select>
<!-- 查找已过期但状态仍为ACTIVE的冷却记录 -->
<select id="findExpiredActiveCooldowns" resultMap="MachineCooldownMap">
SELECT * FROM machine_cooldown
WHERE status = 'ACTIVE'
AND cooldown_end_time &lt;= #{currentTime}
ORDER BY cooldown_end_time ASC
LIMIT #{limit}
</select>
<!-- 批量更新过期的冷却记录状态 -->
<update id="batchUpdateExpiredCooldowns">
UPDATE machine_cooldown
SET status = 'EXPIRED', updated_at = NOW()
WHERE status = 'ACTIVE'
AND cooldown_end_time &lt;= #{currentTime}
</update>
<!-- 手动移除设备的冷却状态 -->
<update id="removeMachineCooldown">
UPDATE machine_cooldown
SET status = 'MANUALLY_REMOVED', updated_at = NOW()
WHERE machine_id = #{machineId}
AND status = 'ACTIVE'
</update>
<!-- 根据链接任务ID查找冷却记录 -->
<select id="findByLinkTaskId" parameterType="long" resultMap="MachineCooldownMap">
SELECT * FROM machine_cooldown
WHERE link_task_id = #{linkTaskId}
ORDER BY created_at DESC
</select>
<!-- 统计活跃的冷却记录数量 -->
<select id="countActiveCooldowns" resultType="long">
SELECT COUNT(*) FROM machine_cooldown
WHERE status = 'ACTIVE'
AND cooldown_end_time > NOW()
</select>
<!-- 统计指定时间范围内的冷却记录数量 -->
<select id="countCooldownsByTimeRange" resultType="long">
SELECT COUNT(*) FROM machine_cooldown
WHERE created_at BETWEEN #{startTime} AND #{endTime}
</select>
<!-- 清理指定时间之前的已过期冷却记录 -->
<delete id="cleanupExpiredCooldowns">
DELETE FROM machine_cooldown
WHERE status = 'EXPIRED'
AND updated_at &lt; #{beforeTime}
</delete>
<!-- 获取指定设备的冷却历史记录 -->
<select id="getCooldownHistory" resultMap="MachineCooldownMap">
SELECT * FROM machine_cooldown
WHERE machine_id = #{machineId}
ORDER BY created_at DESC
LIMIT #{limit} OFFSET #{offset}
</select>
</mapper>

View File

@@ -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<LinkTask> 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<LinkTask> 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<LinkTask> 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) {