# Admin 数据大屏游戏数据表技术方案 ## 目标 在新 admin 的 `统计管理 / 数据大屏` 中增加独立的“游戏数据”表,用于按日期、国家、游戏维度查看消耗和收益数据。 页面查询不能直接扫描 `likei_wallet.wallet_gold_asset_record_1..12`、第三方回调日志或游戏明细表。所有 admin 查询默认读取游戏维度预聚合表;只有后台刷新任务和手动回填任务允许按时间窗口扫描原始数据。 ## 现状 当前国家数据大屏入口: - 前端路由:`chatapp3-admin/apps/src/router/routes/modules/statistics.ts` - 前端页面:`chatapp3-admin/apps/src/views/statistics/datav.vue` - 前端接口:`chatapp3-admin/apps/src/api/legacy/datav.ts` - 后端 Controller:`DatavRestController` - 后端 Service:`CountryDashboardServiceImpl` - 后端 Mapper:`CountryDashboardDAO.xml` - 现有查询文档:`chatapp3-java/rc-service/rc-service-console/docs/country-dashboard-data-query.md` 现有大屏已经聚合了国家维度的 `game_total_flow`、`game_payout`、`game_user`,但没有保留游戏维度。新增功能不改动现有总览口径,新增一套游戏维度查询和预聚合表。 ## 页面设计 在 `datav.vue` 的表格区域增加第三个 tab: - `指标总览` - `充值相关数据` - `游戏数据` “游戏数据”表复用页面已有筛选项: - 系统:`sysOrigin` - 统计时区:`statTimezone` - 周期:`periodType` - 日期范围:`startDate/endDate` - 国家多选:`countryCodes` 新增游戏筛选项: - `gameProvider`:游戏厂商,支持 `INTERNAL`、`BAISHUN`、`GAME_OPEN`、`LINGXIAN`,可为空。 - `gameKeyword`:游戏 ID 或游戏名称模糊搜索。 表格字段: | 字段 | 说明 | | --- | --- | | 日期/周期 | `periodKey`,DAY 显示日期,WEEK/MONTH/ALL 显示周期 | | 国家 | `countryCode / countryName` | | 游戏厂商 | `gameProvider` | | 游戏 ID | `gameId` | | 游戏名称 | `gameName` | | 消耗 | 用户玩游戏支出,现有钱包口径为 `type = 1` | | 返奖 | 游戏返还给用户,现有钱包口径为 `type = 0` | | 收益 | `consumeAmount - payoutAmount` | | 返奖率 | `payoutAmount / consumeAmount` | | 收益率 | `profitAmount / consumeAmount` | | 游戏用户数 | 周期内去重用户数 | | 流水笔数 | 周期内消耗和返奖记录数 | | 更新时间 | 预聚合刷新时间 | 表格必须服务端分页,默认 `limit=50`,最大 `limit=200`。 ## 后端接口 新增接口: ```http GET /console/datav/country-dashboard/game-metrics ``` 请求参数: | 参数 | 必填 | 说明 | | --- | --- | --- | | `sysOrigin` | 是 | 系统来源 | | `periodType` | 是 | `DAY` / `WEEK` / `MONTH` / `ALL` | | `startDate` | DAY/WEEK/MONTH 必填 | 查询开始日期 | | `endDate` | DAY/WEEK/MONTH 必填 | 查询结束日期 | | `statTimezone` | 否 | 默认 `Asia/Riyadh` | | `countryCodes` | 否 | 多国家码,逗号分隔 | | `countryKeyword` | 否 | 国家码或国家名搜索 | | `gameProvider` | 否 | 游戏厂商 | | `gameKeyword` | 否 | 游戏 ID 或名称搜索 | | `cursor` | 否 | 页码,默认 1 | | `limit` | 否 | 页大小,默认 50,最大 200 | | `sortField` | 否 | 允许 `consumeAmount/payoutAmount/profitAmount/userCount/orderCount` | | `sortOrder` | 否 | `ascend/descend` | 响应结构: ```json { "current": 1, "size": 50, "total": 120, "records": [ { "periodKey": "2026-05-12", "periodName": "2026-05-12", "statTimezone": "Asia/Riyadh", "countryCode": "SA", "countryName": "Saudi Arabia", "gameProvider": "BAISHUN", "gameId": "1067", "gameName": "example game", "consumeAmount": "1000.00", "payoutAmount": "720.00", "profitAmount": "280.00", "payoutRate": "72.00%", "profitRate": "28.00%", "userCount": 86, "orderCount": 320, "refreshedAt": "2026-05-12 10:20:00" } ] } ``` 后端新增对象建议: - `CountryDashboardGameMetricQryCmd` - `CountryDashboardGameMetricCO` - `CountryDashboardGameMetricDTO` - `CountryDashboardGameMetricService` - `CountryDashboardGameMetricDAO` - `CountryDashboardGameMetricDAO.xml` ## 预聚合表 ### `country_dashboard_game_period_metric` 页面直接查询这张表。 ```sql CREATE TABLE IF NOT EXISTS `country_dashboard_game_period_metric` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `period_type` varchar(16) NOT NULL COMMENT '周期类型:DAY/WEEK/MONTH/ALL', `period_key` varchar(32) NOT NULL COMMENT '周期键', `stat_timezone` varchar(64) NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '统计时区', `period_name` varchar(64) NOT NULL DEFAULT '' COMMENT '周期名称', `period_start_date` date DEFAULT NULL COMMENT '周期开始日期', `period_end_date` date DEFAULT NULL COMMENT '周期结束日期,开区间', `sys_origin` varchar(32) NOT NULL DEFAULT '' COMMENT '系统来源', `country_code` varchar(64) NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家码', `country_name` varchar(128) NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家名称', `game_provider` varchar(32) NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏厂商', `game_id` varchar(64) NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏ID', `game_name` varchar(128) NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏名称', `consume_amount` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '用户消耗', `payout_amount` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '用户返奖', `profit_amount` decimal(20,2) NOT NULL DEFAULT 0.00 COMMENT '平台收益', `user_count` bigint NOT NULL DEFAULT 0 COMMENT '去重游戏用户数', `order_count` bigint NOT NULL DEFAULT 0 COMMENT '流水笔数', `refreshed_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最近刷新时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_game_period` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `country_code`, `game_provider`, `game_id`), KEY `idx_game_period_query` (`sys_origin`, `stat_timezone`, `period_type`, `period_key`, `country_code`), KEY `idx_game_lookup` (`sys_origin`, `stat_timezone`, `game_provider`, `game_id`, `period_type`, `period_key`), KEY `idx_game_period_sort` (`sys_origin`, `stat_timezone`, `period_type`, `period_key`, `consume_amount`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏游戏维度周期预聚合'; ``` ### `country_dashboard_game_period_user_metric` 用于 `WEEK/MONTH/ALL` 去重用户数,避免直接把日用户数相加导致重复。 ```sql CREATE TABLE IF NOT EXISTS `country_dashboard_game_period_user_metric` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `period_type` varchar(16) NOT NULL COMMENT '周期类型:DAY/WEEK/MONTH/ALL', `period_key` varchar(32) NOT NULL COMMENT '周期键', `stat_timezone` varchar(64) NOT NULL DEFAULT 'Asia/Riyadh' COMMENT '统计时区', `period_name` varchar(64) NOT NULL DEFAULT '' COMMENT '周期名称', `period_start_date` date DEFAULT NULL COMMENT '周期开始日期', `period_end_date` date DEFAULT NULL COMMENT '周期结束日期,开区间', `sys_origin` varchar(32) NOT NULL DEFAULT '' COMMENT '系统来源', `country_code` varchar(64) NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家码', `country_name` varchar(128) NOT NULL DEFAULT 'UNKNOWN' COMMENT '国家名称', `game_provider` varchar(32) NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏厂商', `game_id` varchar(64) NOT NULL DEFAULT 'UNKNOWN' COMMENT '游戏ID', `user_id` bigint NOT NULL COMMENT '用户ID', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_game_period_user` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `country_code`, `game_provider`, `game_id`, `user_id`), KEY `idx_game_user_count` (`period_type`, `period_key`, `stat_timezone`, `sys_origin`, `game_provider`, `game_id`, `country_code`), KEY `idx_game_user_lookup` (`sys_origin`, `stat_timezone`, `user_id`, `game_provider`, `game_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='国家数据大屏游戏维度周期用户去重明细'; ``` ## 数据来源和游戏识别 以钱包流水作为财务主口径: - `wallet_gold_asset_record_1..12.type = 1`:消耗 - `wallet_gold_asset_record_1..12.type = 0`:返奖 - 金额沿用现有大屏口径:`penny_amount / 100.00` - `LUCKY_GIFT`、`LUCKY_GIFT_GOLD_REWARD` 继续归属幸运礼物模块,不并入游戏数据表。 游戏维度来源: | 来源 | 识别方式 | 名称补全 | | --- | --- | --- | | 百顺 | 钱包 `event_id = baishun_order_idempotency.wallet_event_id`,取 `vendor_game_id`;兜底解析 `event_type = BAISHUN_GAME_{gameId}` | `baishun_game_catalog.name` | | 统一三方游戏 | 钱包 `event_id = CONCAT('GAME_OPEN:', game_open_callback_log.order_id)`,取 `game_open_callback_log.game_id`;兜底解析 `event_type = GAME_OPEN_{gameId}` | `lingxian_game_catalog.name` 或 `sys_game_list_config.name` | | Lucky Box | `game_lucky_box_count` 固定为 `gameProvider=INTERNAL, gameId=LUCKY_BOX` | 固定名称 `Lucky Box` | | 其他站内游戏 | 按 `event_type` 映射稳定 `gameId`,例如 `FRUIT_GAME/GAME_FRUIT_* -> FRUIT_GAME`、`LUDO_* -> LUDO_GAME` | 代码内映射或后续配置表 | 如果某条流水无法识别具体游戏,落到: - `gameProvider = UNKNOWN` - `gameId = event_type` - `gameName = event_type` 这样可以保证金额不丢失,同时便于后续补映射。 ## 刷新和回填 新增刷新服务建议为 `CountryDashboardGameMetricService`。 默认任务: - 每 10 分钟刷新最近 1 天。 - 每天 00:30 刷新最近 8 天。 - 支持手动回填,单次最大 366 天,和现有国家大屏保持一致。 刷新逻辑: 1. 按 `sysOrigin` 和 `statTimezone` 计算周期窗口,转成存储时区时间窗口。 2. 扫描窗口内原始游戏流水,按 `period + country + gameProvider + gameId` 聚合金额和流水笔数。 3. 删除对应周期的 `country_dashboard_game_period_user_metric`,写入去重用户明细。 4. 从用户明细聚合 `user_count`。 5. 删除并写入对应周期的 `country_dashboard_game_period_metric`。 6. `ALL` 周期的金额从 `DAY` 周期表汇总,用户数从日级用户明细去重汇总。 刷新任务失败只影响游戏数据表,不影响现有国家大屏总览。 ## 查询性能要求 页面查询只能访问 `country_dashboard_game_period_metric`,禁止在请求链路访问以下原始表: - `likei_wallet.wallet_gold_asset_record_1..12` - `game_open_callback_log` - `baishun_order_idempotency` - `game_lucky_box_count` - 第三方游戏 catalog 表 分页查询必须满足: - `sysOrigin` 必传。 - `periodType`、`statTimezone` 必须落到联合索引前缀。 - `DAY/WEEK/MONTH` 查询必须带日期范围,并转成 `period_key` 或 `period_start_date` 范围。 - `ALL` 只查 `period_type='ALL' AND period_key='ALL'`,不回退全历史实时扫描。 - `countryKeyword` 和 `gameKeyword` 只允许在已经命中周期范围之后过滤;大范围模糊搜索需要限制日期范围。 - 只允许白名单排序字段,避免动态 SQL 注入和无法使用索引的任意排序。 - `limit` 上限 200。 刷新任务扫描原始钱包分表时必须带: - `sys_origin` - `create_time` 毫秒闭开区间 - 游戏 `event_type` 白名单或前缀条件 现有 wallet 分表有 `idx_sorigin_etype_ctime(sys_origin,event_type,create_time)` 和 `idx_event(event_id)`,刷新 SQL 应优先利用这两个索引。不能对 `create_time` 列包函数做过滤,只能在 SELECT 或 GROUP BY 阶段转换时间。 ## 实施步骤 1. 新增 SQL 迁移:创建 `country_dashboard_game_period_metric` 和 `country_dashboard_game_period_user_metric`。 2. 新增后端 DTO/Cmd/CO/DAO/Service,并在 `DatavRestController` 暴露 `/country-dashboard/game-metrics`。 3. 新增刷新任务和手动回填入口,复用现有 `red-circle.country-dashboard.refresh-sys-origins`、`refresh-timezones` 配置。 4. 新 admin 新增接口方法 `countryDashboardGameMetrics`。 5. `datav.vue` 增加“游戏数据”tab、游戏筛选项和分页表格。 6. 首次上线后先回填最近 8 天,再回填历史需要展示的范围。 ## 验证方案 SQL 验证: ```sql EXPLAIN SELECT * FROM country_dashboard_game_period_metric WHERE sys_origin = 'LIKEI' AND stat_timezone = 'Asia/Riyadh' AND period_type = 'DAY' AND period_key BETWEEN '2026-05-01' AND '2026-05-12' ORDER BY consume_amount DESC LIMIT 50; ``` 预期使用 `idx_game_period_query` 或 `idx_game_period_sort`,不能全表扫描。 数据口径验证: ```sql SELECT SUM(consume_amount), SUM(payout_amount) FROM country_dashboard_game_period_metric WHERE sys_origin = 'LIKEI' AND stat_timezone = 'Asia/Riyadh' AND period_type = 'DAY' AND period_key = '2026-05-12'; ``` 对比现有 `country_dashboard_period_metric` 同日同国家汇总的 `game_total_flow/game_payout`,差异应只来自“无法识别但仍落 UNKNOWN”的映射归类,不应出现金额丢失。 功能验证: - 查询 DAY 日期范围,表格按日期、国家、游戏拆分。 - 查询 WEEK/MONTH,用户数为周期内去重,不是日用户数累加。 - 查询 ALL,不触发实时原始流水扫描。 - 国家筛选、游戏厂商筛选、游戏关键字筛选、分页、排序都能返回稳定结果。 - 刷新任务失败时,现有数据大屏总览接口不受影响。