# v4.25 - 玩家活跃记录 ## 一、需求概述 24小时监控玩家在线状态,每小时记录一次活跃数据。每个账户、每个游戏、每个角色每小时最多生成一条记录。 > 玩家A从1点在线到3点,实际产生3条数据(1点、2点、3点各一条)。 ## 二、方案选型 ### 方案对比 | 对比项 | 客户端每小时上报 | 心跳 + 服务端聚合 | |---|---|---| | iOS 可靠性 | ⚠️ 后台定时器不可靠 | ⚠️ 同样依赖前台定时器 | | H5 可靠性 | ⚠️ 切后台暂停JS | ⚠️ 同样依赖页面可见 | | 安卓兼容性 | ⚠️ 部分厂商杀后台 | ⚠️ 同样存在此问题 | | 实现复杂度 | 低 | 中(多一层Redis + Cron) | | 接口调用量 | 低(1次/小时) | 中(12次/小时) | | 扩展性 | 仅统计活跃 | 可扩展实时在线、掉线检测 | ### 结论 两种方案都依赖客户端定时器,切后台/被杀后定时器都会暂停,回到前台都会重新启动。核心区别在于漏报窗口:心跳5分钟 vs 每小时上报60分钟。 对于"每小时记录一次活跃"的需求,客户端每小时上报简单直接,够用。 **采用方案:客户端每小时上报。** ## 三、接口设计 ### 3.1 活跃上报接口 - **路径**:`POST /api/v2/User/playerActive` - **调用方**:客户端定时任务,每小时调用一次 - **请求参数**: | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | member_id | int | 是 | 玩家账号ID | | game_id | int | 是 | 游戏ID | | channel_id | int | 是 | 渠道ID(推广员) | | server_id | int | 是 | 区服ID | | server_name | string | 是 | 区服名 | | role_id | int | 是 | 角色ID | | role_name | string | 是 | 角色名 | - **响应示例**: ```json { "code": 200, "msg": "上报成功", "data": [] } ``` ### 3.2 服务端处理逻辑 1. 校验参数完整性 2. 查询玩家信息(`cy_members`)获取 `member_create_time` 3. 查询角色信息(`cy_role_info`)获取 `role_create_time` 4. 通过 `channel_id`(推广员,level=3)查询渠道层级关系,获取子会长、公会账号、商务账号 5. 通过 `game_id` 查询游戏名 6. 检查去重:同一 `member_id + game_id + role_id + 小时` 是否已存在记录 7. 不存在则写入,存在则跳过 ## 四、表结构 ### 4.1 活跃记录表 `cy_player_active_log` ```sql CREATE TABLE `cy_player_active_log` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `member_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '玩家账号ID', `subaccount_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '子账户ID', `game_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '游戏ID', `game_name` varchar(100) NOT NULL DEFAULT '' COMMENT '游戏名', `channel_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '渠道ID(推广员)', `server_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '区服ID', `server_name` varchar(100) NOT NULL DEFAULT '' COMMENT '区服名', `role_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '角色ID', `role_name` varchar(100) NOT NULL DEFAULT '' COMMENT '角色名', `role_create_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '角色创建时间(时间戳)', `member_create_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '账户注册时间(时间戳)', `promoter_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '推广员ID', `sub_president_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '子会长ID', `guild_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '公会账号ID(会长)', `business_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '商务账号ID', `create_time` datetime NOT NULL COMMENT '活跃时间(例如:2026-06-30 01:00:00)', PRIMARY KEY (`id`), UNIQUE KEY `uk_active` (`member_id`, `game_id`, `role_id`, `create_time`), KEY `idx_game` (`game_id`, `create_time`), KEY `idx_channel` (`channel_id`, `create_time`), KEY `idx_guild` (`guild_id`, `create_time`), KEY `idx_business` (`business_id`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='玩家活跃记录'; ``` ### 4.2 字段说明 | 字段 | 说明 | |---|---| | `member_id` | 来自请求参数,关联 `cy_members` 表 | | `subaccount_id` | 来自请求参数或玩家信息 | | `game_id` | 来自请求参数 | | `game_name` | 通过 `game_id` 查询 `cy_game` 表获取 | | `channel_id` | 来自请求参数,即推广员ID(level=3) | | `promoter_id` | 等于 `channel_id` | | `sub_president_id` | 通过 `channel.parent_id` 向上查 level=2 的渠道 | | `guild_id` | 通过 `channel.parent_id` 向上查 level=1 的渠道 | | `business_id` | 通过 `channel.parent_id` 向上查 level=0 的渠道 | | `create_time` | 服务端当前时间,取整到小时(如 2026-06-30 01:00:00) | ### 4.3 渠道层级关系查询 渠道表 `nw_channel` 通过 `id_path` 或 `parent_id` 维护四级树形结构: ``` 商务(level=0) → 公会/会长(level=1) → 子会长(level=2) → 推广员(level=3) ``` `id_path` 示例:`,10,20,300,500,`,一次查询即可拿到整条链路。 ## 五、数据流向 ``` 客户端(每小时定时器) │ ▼ POST /api/v2/User/playerActive │ ├─ 查询 cy_members → member_create_time ├─ 查询 cy_role_info → role_create_time ├─ 查询 cy_game → game_name ├─ 查询 nw_channel(向上查层级) → 子会长、公会、商务 ├─ 去重检查(uk_active唯一索引) │ ▼ 写入 cy_player_active_log ``` ## 六、注意事项 1. **去重**:`uk_active` 唯一索引保证同一玩家+游戏+角色+小时只有一条记录,重复上报用 `INSERT IGNORE` 或 `ON DUPLICATE KEY` 2. **服务端时间**:`create_time` 使用服务端时间,不依赖客户端时间 3. **渠道层级查询**:建议缓存渠道层级关系,避免每次请求都查 `nw_channel` 表 4. **批量查询优化**:如后续需要查询某公会下的活跃数据,通过 `guild_id` 索引直接查,无需再关联渠道表