yumi-java/rc-service/rc-service-console/docs/country-dashboard-game-metrics-design.md
2026-05-12 20:12:16 +08:00

315 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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不触发实时原始流水扫描。
- 国家筛选、游戏厂商筛选、游戏关键字筛选、分页、排序都能返回稳定结果。
- 刷新任务失败时,现有数据大屏总览接口不受影响。