diff --git a/docs/database_migration_add_completed_points.sql b/docs/database_migration_add_completed_points.sql new file mode 100644 index 0000000..702702f --- /dev/null +++ b/docs/database_migration_add_completed_points.sql @@ -0,0 +1,29 @@ +-- 数据库迁移脚本:添加完成时点数字段和COMPLETED状态 +-- 执行时间:2025-08-27 + +-- 1. 添加 completed_points 字段 +ALTER TABLE `link_task` +ADD COLUMN `completed_points` int UNSIGNED NULL DEFAULT NULL COMMENT '完成时的点数' +AFTER `first_region_select_at`; + +-- 2. 修改 status 枚举,添加 COMPLETED 状态 +ALTER TABLE `link_task` +MODIFY COLUMN `status` enum('NEW','USING','LOGGED_IN','COMPLETED','REFUNDED','EXPIRED') +CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NEW'; + +-- 3. 验证修改结果 +SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_TYPE, + IS_NULLABLE, + COLUMN_DEFAULT, + COLUMN_COMMENT +FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'link_task' + AND COLUMN_NAME IN ('status', 'completed_points') +ORDER BY ORDINAL_POSITION; + +-- 4. 检查表结构 +SHOW CREATE TABLE `link_task`; diff --git a/docs/database_migration_add_completed_status.sql b/docs/database_migration_add_completed_status.sql new file mode 100644 index 0000000..802a90f --- /dev/null +++ b/docs/database_migration_add_completed_status.sql @@ -0,0 +1,20 @@ +-- 数据库迁移脚本:添加 COMPLETED 状态到 link_task 表 +-- 执行时间:请在维护窗口期间执行 +-- 影响:修改 link_task 表的 status 字段枚举值 + +-- 修改 link_task 表的 status 字段,添加 'COMPLETED' 状态 +-- 'COMPLETED' 状态表示用户正常完成了游戏任务 +ALTER TABLE `link_task` +MODIFY COLUMN `status` enum('NEW','USING','LOGGED_IN','COMPLETED','REFUNDED','EXPIRED') +CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NEW' +COMMENT '任务状态: NEW=新建, USING=使用中, LOGGED_IN=已登录, COMPLETED=正常完成, REFUNDED=已退款, EXPIRED=已过期'; + +-- 验证修改结果 +-- 查看表结构确认枚举值已更新 +DESCRIBE `link_task`; + +-- 查看当前各状态的统计 +SELECT status, COUNT(*) as count +FROM `link_task` +GROUP BY status +ORDER BY status; diff --git a/docs/game.sql b/docs/game.sql index 4251b7d..a2c5967 100644 --- a/docs/game.sql +++ b/docs/game.sql @@ -11,7 +11,7 @@ Target Server Version : 80043 (8.0.43) File Encoding : 65001 - Date: 24/08/2025 19:08:50 + Date: 27/08/2025 15:45:17 */ SET NAMES utf8mb4; @@ -37,7 +37,7 @@ CREATE TABLE `agent_points_tx` ( INDEX `fk_apx_operator`(`operator_id` ASC) USING BTREE, CONSTRAINT `fk_apx_account` FOREIGN KEY (`account_id`) REFERENCES `user_account` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, CONSTRAINT `fk_apx_operator` FOREIGN KEY (`operator_id`) REFERENCES `user_account` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for announcement @@ -52,7 +52,7 @@ CREATE TABLE `announcement` ( `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_0900_ai_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for link_batch @@ -63,8 +63,6 @@ CREATE TABLE `link_batch` ( `agent_id` bigint UNSIGNED NOT NULL, `quantity` int UNSIGNED NOT NULL, `times` int UNSIGNED NOT NULL, - `batch_size` int UNSIGNED NOT NULL, - `deduct_points` bigint UNSIGNED NOT NULL, `operator_id` bigint UNSIGNED NULL DEFAULT NULL, `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (`id`) USING BTREE, @@ -72,10 +70,9 @@ CREATE TABLE `link_batch` ( INDEX `fk_lb_operator`(`operator_id` ASC) USING BTREE, CONSTRAINT `fk_lb_agent` FOREIGN KEY (`agent_id`) REFERENCES `user_account` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, CONSTRAINT `fk_lb_operator` FOREIGN KEY (`operator_id`) REFERENCES `user_account` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, - CONSTRAINT `chk_lb_batch_pos` CHECK (`batch_size` > 0), CONSTRAINT `chk_lb_quantity_pos` CHECK (`quantity` > 0), CONSTRAINT `chk_lb_times_pos` CHECK (`times` > 0) -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for link_task @@ -88,7 +85,7 @@ CREATE TABLE `link_task` ( `code_no` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `token_hash` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `expire_at` datetime(3) NOT NULL, - `status` enum('NEW','USING','LOGGED_IN','REFUNDED','EXPIRED') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NEW', + `status` enum('NEW','USING','LOGGED_IN','COMPLETED','REFUNDED','EXPIRED') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NEW', `region` enum('Q','V') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `machine_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, `login_at` datetime(3) NULL DEFAULT NULL, @@ -96,6 +93,12 @@ CREATE TABLE `link_task` ( `revoked_at` datetime(3) NULL DEFAULT NULL, `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), + `need_refresh` tinyint(1) NULL DEFAULT 0 COMMENT '是否需要刷新(0否,1是)', + `refresh_time` datetime(3) NULL DEFAULT NULL COMMENT '刷新时间', + `qr_created_at` datetime(3) NULL DEFAULT NULL COMMENT '二维码创建时间', + `qr_expire_at` datetime(3) NULL DEFAULT NULL COMMENT '二维码过期时间', + `first_region_select_at` datetime(3) NULL DEFAULT NULL COMMENT '首次选区时间', + `completed_points` int UNSIGNED NULL DEFAULT NULL COMMENT '完成时的点数', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_code_no`(`code_no` ASC) USING BTREE, UNIQUE INDEX `uk_token_hash`(`token_hash` ASC) USING BTREE, @@ -103,9 +106,12 @@ CREATE TABLE `link_task` ( INDEX `idx_expire_at`(`expire_at` ASC) USING BTREE, INDEX `idx_created_at`(`created_at` ASC) USING BTREE, INDEX `fk_lt_batch`(`batch_id` ASC) USING BTREE, + INDEX `idx_need_refresh`(`need_refresh` ASC) USING BTREE, + INDEX `idx_qr_expire`(`qr_expire_at` ASC) USING BTREE, + INDEX `idx_first_region_select`(`first_region_select_at` ASC) USING BTREE, CONSTRAINT `fk_lt_agent` FOREIGN KEY (`agent_id`) REFERENCES `user_account` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, CONSTRAINT `fk_lt_batch` FOREIGN KEY (`batch_id`) REFERENCES `link_batch` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for operation_log @@ -124,7 +130,25 @@ CREATE TABLE `operation_log` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_log_code_time`(`code_no` ASC, `created_at` ASC) USING BTREE, INDEX `idx_log_time`(`created_at` ASC) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for system_config +-- ---------------------------- +DROP TABLE IF EXISTS `system_config`; +CREATE TABLE `system_config` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, + `config_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '配置键', + `config_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '配置值', + `config_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'STRING' COMMENT '配置类型:STRING, INTEGER, BOOLEAN, JSON', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '配置描述', + `is_system` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否系统配置(1是,0否)', + `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_config_key`(`config_key` ASC) USING BTREE, + INDEX `idx_config_type`(`config_type` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Table structure for user_account @@ -144,33 +168,4 @@ CREATE TABLE `user_account` ( CONSTRAINT `chk_points_nonneg` CHECK (`points_balance` >= 0) ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; --- ---------------------------- --- Table structure for system_config --- ---------------------------- -DROP TABLE IF EXISTS `system_config`; -CREATE TABLE `system_config` ( - `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, - `config_key` varchar(100) NOT NULL COMMENT '配置键', - `config_value` text NOT NULL COMMENT '配置值', - `config_type` varchar(50) NOT NULL DEFAULT 'STRING' COMMENT '配置类型:STRING, INTEGER, BOOLEAN, JSON', - `description` varchar(500) NULL DEFAULT NULL COMMENT '配置描述', - `is_system` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否系统配置(1是,0否)', - `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_config_key`(`config_key` ASC) USING BTREE, - INDEX `idx_config_type`(`config_type` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; - --- 插入默认配置 -INSERT INTO `system_config` (`config_key`, `config_value`, `config_type`, `description`, `is_system`) VALUES -('link.default_quantity', '50', 'INTEGER', '链接生成默认奖励点数', 1), -('link.refresh_interval', '300', 'INTEGER', '链接刷新间隔(秒)', 1), -('link.qr_expire_time', '600', 'INTEGER', '二维码过期时间(秒)', 1), -('link.max_times_per_batch', '100', 'INTEGER', '每批次最大刷奖励次数', 1), -('link.min_quantity', '10', 'INTEGER', '最小奖励点数', 1), -('link.max_quantity', '1000', 'INTEGER', '最大奖励点数', 1), -('script.server_url', 'http://36.138.184.60:12345', 'STRING', '脚本服务器地址', 1), -('script.qr_path_template', '/{machineId}/二维码.png', 'STRING', '二维码图片路径模板', 1); - SET FOREIGN_KEY_CHECKS = 1; diff --git a/docs/game_completion_detection_implementation.md b/docs/game_completion_detection_implementation.md new file mode 100644 index 0000000..6876b4d --- /dev/null +++ b/docs/game_completion_detection_implementation.md @@ -0,0 +1,176 @@ +# 游戏完成检测实现文档 + +## 📋 功能概述 + +实现了两种方式来自动检测游戏是否完成,并在检测到完成时自动更新链接状态为 `COMPLETED`,同时记录完成时的点数。 + +## 🔧 实现方案 + +### **方案1: 定时检查空闲设备** +- **触发方式**: 每1分钟自动执行 +- **检查逻辑**: + 1. 获取所有空闲设备列表 + 2. 对每个空闲设备,查找是否有 `LOGGED_IN` 状态的链接任务使用该设备 + 3. 如果找到,则将该链接任务标记为 `COMPLETED`,并记录当前点数 + +### **方案2: 选区请求时检查** +- **触发方式**: 用户发起选区请求时 +- **检查逻辑**: + 1. 在分配新设备给用户前,先检查该设备是否空闲 + 2. 如果设备空闲且有之前的 `LOGGED_IN` 状态链接任务,则标记为完成 + 3. 然后继续正常的选区流程 + +## 🗄️ 数据库变更 + +### **1. link_task 表结构更新** + +```sql +-- 添加完成时点数字段 +ALTER TABLE `link_task` +ADD COLUMN `completed_points` int UNSIGNED NULL DEFAULT NULL COMMENT '完成时的点数' +AFTER `first_region_select_at`; + +-- 更新状态枚举,添加 COMPLETED 状态 +ALTER TABLE `link_task` +MODIFY COLUMN `status` enum('NEW','USING','LOGGED_IN','COMPLETED','REFUNDED','EXPIRED') +CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NEW'; +``` + +### **2. 状态流转图** + +``` +NEW (新建) + ↓ +USING (使用中) + ↓ +LOGGED_IN (已登录) + ↓ +COMPLETED (正常完成) ← 新增状态 +``` + +**终态状态**: `COMPLETED`, `REFUNDED`, `EXPIRED` + +## 💻 代码实现 + +### **1. 核心服务类** + +#### **DeviceStatusCheckService** +- 负责设备状态检查和链接任务更新逻辑 +- 解析设备状态响应数据 (f0: 点数, f1: 状态) +- 更新相关链接任务为完成状态 + +#### **DeviceStatusCheckTask** +- 定时任务类,每分钟执行一次 +- 获取空闲设备列表并逐个检查 + +### **2. API 接口更新** + +#### **ScriptClient** +```java +// 新增方法:获取特定设备状态 +public Mono> getDeviceStatus(String machineId) +``` + +#### **DeviceStatusService** +```java +// 新增方法:解析特定设备状态 +public Map parseDeviceStatusForMachine(String jsonResponse, String machineId) +``` + +#### **LinkTaskMapper** +```java +// 新增方法:根据设备ID和状态查询链接任务 +List findByMachineIdAndStatus(String machineId, String status); +``` + +### **3. 实体类更新** + +#### **LinkTask** +```java +@TableField("completed_points") +private Integer completedPoints; // 完成时的点数 +``` + +## 📊 数据处理流程 + +### **设备状态数据格式** +```json +{ + "f0": { + "val": "900", // 当前点数 + "time": "2025-08-27 15:49:04" + }, + "f1": { + "val": "空闲", // 设备状态 + "time": "2025-08-27 15:49:01" + } +} +``` + +### **完成检测逻辑** +1. **获取设备状态**: 调用 `readAllMsg?文件名=判断分数` 接口 +2. **解析状态**: 提取 f0(点数) 和 f1(状态) 信息 +3. **判断空闲**: 检查 f1.val 是否为 "空闲" +4. **查找任务**: 根据 machine_id 和 status='LOGGED_IN' 查询链接任务 +5. **更新状态**: 设置 status='COMPLETED', completed_points=f0.val + +## 🔧 配置说明 + +### **定时任务配置** +- **执行频率**: 每60秒 (`@Scheduled(fixedRate = 60000)`) +- **启用方式**: 主应用类添加 `@EnableScheduling` 注解 + +### **日志记录** +- **定时检查**: `DEBUG` 级别,避免日志过多 +- **状态更新**: `INFO` 级别,记录重要操作 +- **异常处理**: `WARN/ERROR` 级别,不影响主流程 + +## 🚀 部署说明 + +### **1. 数据库迁移** +```bash +# 执行迁移脚本 +mysql -u username -p database_name < docs/database_migration_add_completed_points.sql +``` + +### **2. 应用重启** +- 新增的定时任务会在应用启动后自动运行 +- 选区请求的检查逻辑会立即生效 + +### **3. 验证方法** +1. **查看日志**: 观察定时任务执行日志 +2. **数据库检查**: 验证 `completed_points` 字段是否正确填充 +3. **API 测试**: 测试选区请求是否正常工作 + +## 📈 监控指标 + +### **关键日志** +- `=== 开始定时检查空闲设备 ===` +- `发现 X 个空闲设备: [设备列表]` +- `链接任务 X (代码: X) 已标记为完成,完成点数: X` + +### **异常监控** +- 设备状态获取失败 +- JSON 解析异常 +- 数据库更新失败 + +## 🔄 扩展性 + +### **支持更多设备状态** +- 可扩展 DeviceStatusInfo 类添加更多状态字段 +- 支持不同的完成判断条件 + +### **自定义检查频率** +- 通过配置文件调整定时任务执行频率 +- 支持不同环境使用不同配置 + +### **完成通知** +- 可扩展添加完成通知功能 (邮件、短信、webhook等) +- 支持完成统计和报表功能 + +## ⚠️ 注意事项 + +1. **性能考虑**: 定时任务频率不宜过高,避免对系统造成压力 +2. **异常处理**: 单个设备检查失败不应影响其他设备的检查 +3. **数据一致性**: 使用事务确保状态更新的原子性 +4. **日志管理**: 合理设置日志级别,避免日志过多影响性能 diff --git a/src/main/java/com/gameplatform/server/GamePlatformServerApplication.java b/src/main/java/com/gameplatform/server/GamePlatformServerApplication.java index 3f35810..6d73196 100644 --- a/src/main/java/com/gameplatform/server/GamePlatformServerApplication.java +++ b/src/main/java/com/gameplatform/server/GamePlatformServerApplication.java @@ -3,9 +3,11 @@ package com.gameplatform.server; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @MapperScan("com.gameplatform.server.mapper") +@EnableScheduling public class GamePlatformServerApplication { public static void main(String[] args) { SpringApplication.run(GamePlatformServerApplication.class, args); diff --git a/src/main/java/com/gameplatform/server/mapper/agent/LinkTaskMapper.java b/src/main/java/com/gameplatform/server/mapper/agent/LinkTaskMapper.java index 71f1524..e15daed 100644 --- a/src/main/java/com/gameplatform/server/mapper/agent/LinkTaskMapper.java +++ b/src/main/java/com/gameplatform/server/mapper/agent/LinkTaskMapper.java @@ -90,4 +90,9 @@ public interface LinkTaskMapper extends BaseMapper { * 根据链接编号列表和代理ID查询链接任务(用于验证权限) */ List findByCodeNosAndAgentId(@Param("codeNos") List codeNos, @Param("agentId") Long agentId); + + /** + * 根据设备ID和状态查询链接任务 + */ + List findByMachineIdAndStatus(@Param("machineId") String machineId, @Param("status") String status); } diff --git a/src/main/java/com/gameplatform/server/model/dto/link/LinkStatusResponse.java b/src/main/java/com/gameplatform/server/model/dto/link/LinkStatusResponse.java index 0e63a2c..03c7fc2 100644 --- a/src/main/java/com/gameplatform/server/model/dto/link/LinkStatusResponse.java +++ b/src/main/java/com/gameplatform/server/model/dto/link/LinkStatusResponse.java @@ -13,7 +13,7 @@ public class LinkStatusResponse { @Schema(description = "批次ID", example = "123") private Long batchId; - @Schema(description = "链接状态", example = "NEW", allowableValues = {"NEW", "USING", "LOGGED_IN", "REFUNDED", "EXPIRED"}) + @Schema(description = "链接状态", example = "NEW", allowableValues = {"NEW", "USING", "LOGGED_IN", "COMPLETED", "REFUNDED", "EXPIRED"}) private String status; @Schema(description = "链接状态描述", example = "新建") diff --git a/src/main/java/com/gameplatform/server/model/dto/link/UserLinkStatusResponse.java b/src/main/java/com/gameplatform/server/model/dto/link/UserLinkStatusResponse.java index d20ad1b..24a2f8b 100644 --- a/src/main/java/com/gameplatform/server/model/dto/link/UserLinkStatusResponse.java +++ b/src/main/java/com/gameplatform/server/model/dto/link/UserLinkStatusResponse.java @@ -5,7 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "用户端链接状态响应") public class UserLinkStatusResponse { - @Schema(description = "链接状态", example = "NEW", allowableValues = {"NEW", "USING", "LOGGED_IN", "REFUNDED", "EXPIRED"}) + @Schema(description = "链接状态", example = "NEW", allowableValues = {"NEW", "USING", "LOGGED_IN", "COMPLETED", "REFUNDED", "EXPIRED"}) private String status; // Getter and Setter diff --git a/src/main/java/com/gameplatform/server/model/entity/agent/LinkTask.java b/src/main/java/com/gameplatform/server/model/entity/agent/LinkTask.java index 3b60f48..d8e794e 100644 --- a/src/main/java/com/gameplatform/server/model/entity/agent/LinkTask.java +++ b/src/main/java/com/gameplatform/server/model/entity/agent/LinkTask.java @@ -25,7 +25,7 @@ public class LinkTask { @TableField("expire_at") private LocalDateTime expireAt; - private String status; // NEW | USING | LOGGED_IN | REFUNDED | EXPIRED + private String status; // NEW | USING | LOGGED_IN | COMPLETED | REFUNDED | EXPIRED private String region; // Q | V @@ -61,6 +61,9 @@ public class LinkTask { @TableField("first_region_select_at") private LocalDateTime firstRegionSelectAt; + + @TableField("completed_points") + private Integer completedPoints; public Long getId() { return id; } public void setId(Long id) { this.id = id; } @@ -118,4 +121,7 @@ public class LinkTask { public LocalDateTime getFirstRegionSelectAt() { return firstRegionSelectAt; } public void setFirstRegionSelectAt(LocalDateTime firstRegionSelectAt) { this.firstRegionSelectAt = firstRegionSelectAt; } + + public Integer getCompletedPoints() { return completedPoints; } + public void setCompletedPoints(Integer completedPoints) { this.completedPoints = completedPoints; } } diff --git a/src/main/java/com/gameplatform/server/service/device/DeviceStatusCheckService.java b/src/main/java/com/gameplatform/server/service/device/DeviceStatusCheckService.java new file mode 100644 index 0000000..5208181 --- /dev/null +++ b/src/main/java/com/gameplatform/server/service/device/DeviceStatusCheckService.java @@ -0,0 +1,158 @@ +package com.gameplatform.server.service.device; + +import com.gameplatform.server.mapper.agent.LinkTaskMapper; +import com.gameplatform.server.model.entity.agent.LinkTask; +import com.gameplatform.server.service.external.ScriptClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Service +@Slf4j +public class DeviceStatusCheckService { + + private final ScriptClient scriptClient; + private final LinkTaskMapper linkTaskMapper; + + public DeviceStatusCheckService(ScriptClient scriptClient, LinkTaskMapper linkTaskMapper) { + this.scriptClient = scriptClient; + this.linkTaskMapper = linkTaskMapper; + } + + /** + * 检查设备状态并更新相关链接任务 + * @param machineId 设备ID + * @param reason 检查原因(定时检查 或 选区请求) + */ + @Transactional + public void checkDeviceStatusAndUpdateTasks(String machineId, String reason) { + log.info("=== 开始检查设备状态 ==="); + log.info("设备ID: {}, 检查原因: {}", machineId, reason); + + try { + // 1. 获取设备状态 + Map deviceStatus = scriptClient.getDeviceStatus(machineId).block(); + if (deviceStatus == null || deviceStatus.isEmpty()) { + log.warn("获取设备状态失败,设备ID: {}", machineId); + return; + } + + // 2. 解析设备状态 + DeviceStatusInfo statusInfo = parseDeviceStatus(deviceStatus); + log.info("设备状态解析结果: {}", statusInfo); + + // 3. 如果设备空闲,检查是否有使用该设备的LOGGED_IN状态链接 + if (statusInfo.isIdle()) { + log.info("设备 {} 处于空闲状态,检查相关链接任务", machineId); + updateCompletedTasks(machineId, statusInfo.getPoints()); + } else { + log.info("设备 {} 不是空闲状态: {}", machineId, statusInfo.getStatus()); + } + + } catch (Exception e) { + log.error("检查设备状态时发生异常,设备ID: {}", machineId, e); + } + + log.info("=== 设备状态检查完成 ==="); + } + + /** + * 解析设备状态响应 + */ + private DeviceStatusInfo parseDeviceStatus(Map deviceStatus) { + DeviceStatusInfo info = new DeviceStatusInfo(); + + try { + // 解析 f0 (点数) + Object f0Obj = deviceStatus.get("f0"); + if (f0Obj instanceof Map) { + @SuppressWarnings("unchecked") + Map f0 = (Map) f0Obj; + if (f0.get("val") != null) { + String pointsStr = f0.get("val").toString(); + info.setPoints(Integer.parseInt(pointsStr)); + } + } + + // 解析 f1 (状态) + Object f1Obj = deviceStatus.get("f1"); + if (f1Obj instanceof Map) { + @SuppressWarnings("unchecked") + Map f1 = (Map) f1Obj; + if (f1.get("val") != null) { + String status = f1.get("val").toString(); + info.setStatus(status); + info.setIdle("空闲".equals(status)); + } + } + + } catch (Exception e) { + log.error("解析设备状态时发生异常", e); + } + + return info; + } + + /** + * 更新已完成的任务 + */ + private void updateCompletedTasks(String machineId, Integer points) { + // 查找使用该设备且状态为LOGGED_IN的链接任务 + List loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(machineId, "LOGGED_IN"); + + if (loggedInTasks.isEmpty()) { + log.info("设备 {} 没有处于LOGGED_IN状态的链接任务", machineId); + return; + } + + log.info("找到 {} 个使用设备 {} 且状态为LOGGED_IN的链接任务", loggedInTasks.size(), machineId); + + // 更新所有相关任务为COMPLETED状态 + for (LinkTask task : loggedInTasks) { + try { + task.setStatus("COMPLETED"); + task.setCompletedPoints(points); + task.setUpdatedAt(LocalDateTime.now()); + + int updated = linkTaskMapper.updateById(task); + if (updated > 0) { + log.info("链接任务 {} (代码: {}) 已标记为完成,完成点数: {}", + task.getId(), task.getCodeNo(), points); + } else { + log.warn("更新链接任务 {} 失败", task.getId()); + } + + } catch (Exception e) { + log.error("更新链接任务 {} 时发生异常", task.getId(), e); + } + } + } + + /** + * 设备状态信息 + */ + private static class DeviceStatusInfo { + private String status; + private Integer points; + private boolean idle; + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public Integer getPoints() { return points; } + public void setPoints(Integer points) { this.points = points; } + + public boolean isIdle() { return idle; } + public void setIdle(boolean idle) { this.idle = idle; } + + @Override + public String toString() { + return String.format("DeviceStatusInfo{status='%s', points=%d, idle=%s}", + status, points, idle); + } + } +} diff --git a/src/main/java/com/gameplatform/server/service/device/DeviceStatusService.java b/src/main/java/com/gameplatform/server/service/device/DeviceStatusService.java index d428e92..0857222 100644 --- a/src/main/java/com/gameplatform/server/service/device/DeviceStatusService.java +++ b/src/main/java/com/gameplatform/server/service/device/DeviceStatusService.java @@ -129,6 +129,65 @@ public class DeviceStatusService { private boolean isDeviceAvailable(String val) { return "空闲".equals(val); } + + /** + * 解析特定设备的状态信息(用于检查是否完成游戏) + * @param jsonResponse JSON响应字符串 + * @param machineId 设备ID + * @return 包含f0(点数)和f1(状态)的Map + */ + public Map parseDeviceStatusForMachine(String jsonResponse, String machineId) { + Map result = new HashMap<>(); + + try { + log.debug("解析设备 {} 的状态信息", machineId); + + JsonNode rootNode = objectMapper.readTree(jsonResponse); + + // 查找指定设备的信息 + JsonNode deviceNode = rootNode.get(machineId); + if (deviceNode == null) { + log.warn("未找到设备 {} 的状态信息", machineId); + return result; + } + + // 解析f0(点数)和f1(状态)等信息 + String val = deviceNode.get("val").asText(); + String time = deviceNode.get("time").asText(); + + // 构建f0和f1格式的返回数据 + Map f0Info = new HashMap<>(); + Map f1Info = new HashMap<>(); + + // 假设val包含点数信息,f1包含状态信息 + // 根据实际API响应格式调整 + if (val.matches("\\d+")) { + // 如果val是数字,则认为是点数 + f0Info.put("val", val); + f0Info.put("time", time); + result.put("f0", f0Info); + + // 状态信息需要额外获取,这里先设置默认值 + f1Info.put("val", "空闲"); // 如果能获取到点数,可能表示空闲 + f1Info.put("time", time); + result.put("f1", f1Info); + } else { + // 如果val不是数字,可能是状态信息 + f1Info.put("val", val); + f1Info.put("time", time); + result.put("f1", f1Info); + } + + log.debug("设备 {} 状态解析完成: {}", machineId, result); + + } catch (JsonProcessingException e) { + log.error("解析设备 {} 状态JSON失败: {}", machineId, e.getMessage(), e); + } catch (Exception e) { + log.error("解析设备 {} 状态时发生异常", machineId, e); + } + + return result; + } /** * 按系列分组空闲设备 diff --git a/src/main/java/com/gameplatform/server/service/external/ScriptClient.java b/src/main/java/com/gameplatform/server/service/external/ScriptClient.java index ac95c63..55e4479 100644 --- a/src/main/java/com/gameplatform/server/service/external/ScriptClient.java +++ b/src/main/java/com/gameplatform/server/service/external/ScriptClient.java @@ -280,6 +280,33 @@ public class ScriptClient { .doOnSuccess(result -> log.info("保存总次数接口调用成功: url={}, result={}", url, result)) .doOnError(e -> log.warn("保存总次数接口调用失败: times={}, error={}", times, e.toString())); } + + /** + * 获取特定设备的状态信息(用于检查是否完成游戏) + * @param machineId 设备ID + * @return 设备状态信息的Map,包含f0(点数)和f1(状态)等信息 + */ + public Mono> getDeviceStatus(String machineId) { + String url = "http://36.138.184.60:1234/yijianwan_netfile/readAllMsg?文件名=判断分数"; + log.debug("获取设备状态: 设备={}, url={}", machineId, url); + + return webClient.get() + .uri(url) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(String.class) + .timeout(Duration.ofSeconds(10)) + .map(jsonResponse -> { + // 解析JSON响应,提取指定设备的状态信息 + return deviceStatusService.parseDeviceStatusForMachine(jsonResponse, machineId); + }) + .doOnSuccess(deviceStatus -> { + log.debug("获取设备状态成功: 设备={}, 状态={}", machineId, deviceStatus); + }) + .doOnError(e -> { + log.warn("获取设备状态失败: 设备={}, 错误={}", machineId, e.toString()); + }); + } } diff --git a/src/main/java/com/gameplatform/server/service/link/LinkListService.java b/src/main/java/com/gameplatform/server/service/link/LinkListService.java index db3b689..230a31c 100644 --- a/src/main/java/com/gameplatform/server/service/link/LinkListService.java +++ b/src/main/java/com/gameplatform/server/service/link/LinkListService.java @@ -190,6 +190,7 @@ public class LinkListService { case "NEW" -> "新建"; case "USING" -> "使用中"; case "LOGGED_IN" -> "已登录"; + case "COMPLETED" -> "已完成"; case "REFUNDED" -> "已退款"; case "EXPIRED" -> "已过期"; default -> "未知状态"; 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 118c591..d2d72d0 100644 --- a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java +++ b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java @@ -13,6 +13,7 @@ import com.gameplatform.server.model.entity.agent.LinkTask; import com.gameplatform.server.service.external.ScriptClient; import com.gameplatform.server.service.device.DeviceCodeMappingService; +import com.gameplatform.server.service.device.DeviceStatusCheckService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -37,6 +38,7 @@ public class LinkStatusService { private final LinkBatchMapper linkBatchMapper; private final ScriptClient scriptClient; private final DeviceCodeMappingService deviceCodeMappingService; + private final DeviceStatusCheckService deviceStatusCheckService; // 状态描述映射 @@ -45,17 +47,19 @@ public class LinkStatusService { STATUS_DESC_MAP.put("NEW", "新建"); STATUS_DESC_MAP.put("USING", "使用中"); STATUS_DESC_MAP.put("LOGGED_IN", "已登录"); + STATUS_DESC_MAP.put("COMPLETED", "已完成"); STATUS_DESC_MAP.put("REFUNDED", "已退款"); STATUS_DESC_MAP.put("EXPIRED", "已过期"); } public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper, - ScriptClient scriptClient, DeviceCodeMappingService deviceCodeMappingService) { + ScriptClient scriptClient, DeviceCodeMappingService deviceCodeMappingService, + DeviceStatusCheckService deviceStatusCheckService) { this.linkTaskMapper = linkTaskMapper; this.linkBatchMapper = linkBatchMapper; this.scriptClient = scriptClient; this.deviceCodeMappingService = deviceCodeMappingService; - + this.deviceStatusCheckService = deviceStatusCheckService; } /** @@ -312,8 +316,8 @@ public class LinkStatusService { // 如果未超过10分钟,执行自动刷新 log.info("链接状态是USING,执行自动刷新"); performAutoRefresh(linkTask); - } else if ("LOGGED_IN".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) { - // 已上号或已退款状态,不需要刷新 + } else if ("LOGGED_IN".equals(linkTask.getStatus()) || "COMPLETED".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) { + // 已上号、已完成或已退款状态,不需要刷新 log.info("链接状态为 {},不需要刷新", linkTask.getStatus()); } @@ -447,6 +451,15 @@ public class LinkStatusService { log.info("从空闲设备列表中选择设备: {}", selectedDevice); log.info("设备选择详情: 可用设备总数={}, 选择了第一个设备={}", deviceStatus.getAvailableDevices().size(), selectedDevice); + + // 7.5. 检查该设备是否有之前的LOGGED_IN状态链接任务需要完成 + try { + log.info("检查设备 {} 是否有需要完成的链接任务", selectedDevice); + deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(selectedDevice, "选区请求"); + } catch (Exception e) { + log.warn("检查设备状态时发生异常,继续选区流程: {}", e.getMessage()); + // 不影响选区流程,只记录警告日志 + } // 8. 调用保存总次数接口 try { diff --git a/src/main/java/com/gameplatform/server/task/DeviceStatusCheckTask.java b/src/main/java/com/gameplatform/server/task/DeviceStatusCheckTask.java new file mode 100644 index 0000000..3dc99b5 --- /dev/null +++ b/src/main/java/com/gameplatform/server/task/DeviceStatusCheckTask.java @@ -0,0 +1,67 @@ +package com.gameplatform.server.task; + +import com.gameplatform.server.model.dto.device.DeviceStatusResponse; +import com.gameplatform.server.service.device.DeviceStatusCheckService; +import com.gameplatform.server.service.external.ScriptClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 设备状态检查定时任务 + */ +@Component +@Slf4j +public class DeviceStatusCheckTask { + + private final ScriptClient scriptClient; + private final DeviceStatusCheckService deviceStatusCheckService; + + public DeviceStatusCheckTask(ScriptClient scriptClient, DeviceStatusCheckService deviceStatusCheckService) { + this.scriptClient = scriptClient; + this.deviceStatusCheckService = deviceStatusCheckService; + } + + /** + * 每分钟检查一次空闲设备,并更新相关链接任务状态 + */ + @Scheduled(fixedRate = 60000) // 每60秒执行一次 + public void checkIdleDevicesAndUpdateTasks() { + log.debug("=== 开始定时检查空闲设备 ==="); + + try { + // 1. 获取所有设备状态 + DeviceStatusResponse deviceStatus = scriptClient.checkAvailableDeviceStatus().block(); + + if (deviceStatus == null) { + log.warn("获取设备状态失败,跳过本次检查"); + return; + } + + List availableDevices = deviceStatus.getAvailableDevices(); + if (availableDevices.isEmpty()) { + log.debug("当前没有空闲设备"); + return; + } + + log.info("发现 {} 个空闲设备: {}", availableDevices.size(), availableDevices); + + // 2. 对每个空闲设备检查是否有相关的LOGGED_IN状态链接任务 + for (String deviceId : availableDevices) { + try { + deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(deviceId, "定时检查"); + } catch (Exception e) { + log.error("检查设备 {} 状态时发生异常", deviceId, e); + // 继续检查下一个设备,不因为一个设备出错而中断整个流程 + } + } + + } catch (Exception e) { + log.error("定时检查空闲设备时发生异常", e); + } + + log.debug("=== 定时检查空闲设备完成 ==="); + } +} diff --git a/src/main/resources/mapper/agent/LinkTaskMapper.xml b/src/main/resources/mapper/agent/LinkTaskMapper.xml index 58e0695..6447a68 100644 --- a/src/main/resources/mapper/agent/LinkTaskMapper.xml +++ b/src/main/resources/mapper/agent/LinkTaskMapper.xml @@ -21,24 +21,25 @@ + - SELECT id, batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at, created_at, updated_at, need_refresh, refresh_time, qr_created_at, qr_expire_at, first_region_select_at + SELECT id, batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at, created_at, updated_at, need_refresh, refresh_time, qr_created_at, qr_expire_at, first_region_select_at, completed_points FROM link_task WHERE agent_id = #{agentId} ORDER BY created_at DESC @@ -90,7 +91,7 @@ - SELECT id, batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at, created_at, updated_at, need_refresh, refresh_time, qr_created_at, qr_expire_at, first_region_select_at + SELECT id, batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at, created_at, updated_at, need_refresh, refresh_time, qr_created_at, qr_expire_at, first_region_select_at, completed_points, completed_points FROM link_task WHERE agent_id = #{agentId} AND code_no IN @@ -207,4 +208,10 @@ #{codeNo} + +