From 7238e8eb463444129eeac116fe3113fd5ed17059 Mon Sep 17 00:00:00 2001 From: hy001 Date: Tue, 12 May 2026 20:12:16 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=BA=AB=E4=BB=BD=E5=92=8C?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E6=95=B0=E9=87=8Fbug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/UserHistoryIdentityClientApi.java | 3 + .../agency/agency/UserHistoryIdentityDTO.java | 7 +- .../team/manager/TeamManagerServiceImpl.java | 35 +- .../app/user/UserBaseInfoServiceImpl.java | 5 +- .../dto/clienobject/user/UserIdentityCO.java | 9 +- .../country-dashboard-game-metrics-design.md | 314 ++++++++++++++++++ .../bd/query/TeamBdMemberBillListQryExe.java | 46 ++- .../user/user/UserIdentityServiceImpl.java | 5 +- .../app/dto/clientobject/TeamBdMemberCO.java | 4 + .../other/app/dto/h5/UserIdentityVO.java | 8 +- .../rds/entity/team/UserHistoryIdentity.java | 8 +- .../team/UserHistoryIdentityService.java | 5 + .../impl/UserHistoryIdentityServiceImpl.java | 31 ++ .../team/UserHistoryIdentityEndpoint.java | 6 + .../UserHistoryIdentityClientService.java | 2 + .../UserHistoryIdentityClientServiceImpl.java | 5 + sql/20260512_team_yumi_manager_identity.sql | 16 + 17 files changed, 456 insertions(+), 53 deletions(-) create mode 100644 rc-service/rc-service-console/docs/country-dashboard-game-metrics-design.md create mode 100644 sql/20260512_team_yumi_manager_identity.sql diff --git a/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/team/target/api/UserHistoryIdentityClientApi.java b/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/team/target/api/UserHistoryIdentityClientApi.java index 51575954..59f3e8da 100644 --- a/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/team/target/api/UserHistoryIdentityClientApi.java +++ b/rc-service/rc-inner-api/other-inner/other-inner-api/src/main/java/com/red/circle/other/inner/endpoint/team/target/api/UserHistoryIdentityClientApi.java @@ -37,4 +37,7 @@ public interface UserHistoryIdentityClientApi { @GetMapping("/saveManager") ResultResponse saveManager(@RequestParam("userId") Long userId); + + @GetMapping("/saveYumiManager") + ResultResponse saveYumiManager(@RequestParam("userId") Long userId); } diff --git a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/agency/agency/UserHistoryIdentityDTO.java b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/agency/agency/UserHistoryIdentityDTO.java index cc30aaa0..e775908d 100644 --- a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/agency/agency/UserHistoryIdentityDTO.java +++ b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/agency/agency/UserHistoryIdentityDTO.java @@ -40,8 +40,13 @@ public class UserHistoryIdentityDTO implements Serializable { private Boolean host; /** - * 是否做过团队经理. + * 是否做过旧团队经理. */ private Boolean manager; + /** + * 是否做过 Yumi 团队经理. + */ + private Boolean yumiManager; + } diff --git a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/team/manager/TeamManagerServiceImpl.java b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/team/manager/TeamManagerServiceImpl.java index 08098609..64952eb5 100644 --- a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/team/manager/TeamManagerServiceImpl.java +++ b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/team/manager/TeamManagerServiceImpl.java @@ -15,8 +15,6 @@ import com.red.circle.other.inner.endpoint.team.target.UserHistoryIdentityClient import com.red.circle.other.inner.endpoint.user.region.RegionConfigClient; import com.red.circle.other.inner.endpoint.user.user.UserProfileClient; import com.red.circle.other.inner.enums.sys.appmanager.RoomRolesEnum; -import com.red.circle.other.inner.model.cmd.sys.AddSysAdministratorCmd; -import com.red.circle.other.inner.model.cmd.sys.SysAdministratorChangeStatusCmd; import com.red.circle.other.inner.model.cmd.team.manager.TeamManagerOpsPageQryCmd; import com.red.circle.other.inner.model.dto.sys.AdministratorDTO; import com.red.circle.other.inner.model.dto.agency.manager.TeamManagerDTO; @@ -117,8 +115,7 @@ public class TeamManagerServiceImpl implements TeamManagerService { .setCreateUser(cmd.getCreateUser()); ResponseAssert.requiredSuccess(teamManagerClient.add(addCmd)); - ResponseAssert.requiredSuccess(userHistoryIdentityClient.saveManager(cmd.getUserId())); - ensureAppManager(cmd.getUserId(), cmd.getCreateUser()); + ResponseAssert.requiredSuccess(userHistoryIdentityClient.saveYumiManager(cmd.getUserId())); } @Override @@ -138,36 +135,6 @@ public class TeamManagerServiceImpl implements TeamManagerService { removeAppManagerIfOnlyManagerRole(db.getUserId()); } - private void ensureAppManager(Long userId, Long operatorUserId) { - AdministratorDTO administrator = ResponseAssert.requiredSuccess( - administratorClient.getByUserId(userId)); - - if (Objects.isNull(administrator)) { - AddSysAdministratorCmd addCmd = new AddSysAdministratorCmd(); - addCmd.setUserId(userId); - addCmd.setRoles(RoomRolesEnum.MANAGER.name()); - addCmd.setReqUserId(operatorUserId); - ResponseAssert.requiredSuccess(administratorClient.addSysAdministrator(addCmd)); - administrator = ResponseAssert.requiredSuccess(administratorClient.getByUserId(userId)); - } - - ResponseAssert.notNull(CommonErrorCode.DATA_ERROR, administrator); - - if (StringUtils.isBlank(administrator.getRoles()) - || !Boolean.TRUE.equals(administrator.getStatus())) { - ResponseAssert.requiredSuccess( - administratorClient.changeRoles(administrator.getId(), RoomRolesEnum.MANAGER.name())); - } - - if (!Boolean.TRUE.equals(administrator.getStatus())) { - SysAdministratorChangeStatusCmd statusCmd = new SysAdministratorChangeStatusCmd() - .setId(administrator.getId()) - .setStatus(1); - statusCmd.setReqUserId(operatorUserId); - ResponseAssert.requiredSuccess(administratorClient.changeStatusAdministrator(statusCmd)); - } - } - private void removeAppManagerIfOnlyManagerRole(Long userId) { AdministratorDTO administrator = ResponseAssert.requiredSuccess( administratorClient.getByUserId(userId)); diff --git a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/user/UserBaseInfoServiceImpl.java b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/user/UserBaseInfoServiceImpl.java index 1e5483c7..d772a61e 100644 --- a/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/user/UserBaseInfoServiceImpl.java +++ b/rc-service/rc-service-console/console-application/src/main/java/com/red/circle/console/app/service/app/user/UserBaseInfoServiceImpl.java @@ -287,9 +287,10 @@ public class UserBaseInfoServiceImpl implements .setAnchor(Objects.nonNull(teamMember)) .setBd(ResponseAssert.requiredSuccess(bdTeamInfoClient.check(userId))) .setBdLeader(ResponseAssert.requiredSuccess(bdTeamLeaderClient.check(userId))) - .setManager(ResponseAssert.requiredSuccess(teamManagerClient.check(userId))) + .setManager(Boolean.FALSE) + .setYumiManager(ResponseAssert.requiredSuccess(teamManagerClient.check(userId))) .setFreightAgent( - ResponseAssert.requiredSuccess(freightGoldClient.checkFreightAgent(userId))) + ResponseAssert.requiredSuccess(freightGoldClient.checkFreightAgent(userId))) .setHistoryIdentity( ResponseAssert.requiredSuccess(historyIdentityClient.getLabels(userId))); } diff --git a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/user/UserIdentityCO.java b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/user/UserIdentityCO.java index 5fb47771..49c09cd4 100644 --- a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/user/UserIdentityCO.java +++ b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/user/UserIdentityCO.java @@ -37,13 +37,18 @@ public class UserIdentityCO implements Serializable { private Boolean bdLeader; /** - * true:团队经理,false:不是. + * true:旧团队经理,false:不是. */ private Boolean manager; + /** + * true:Yumi 团队经理,false:不是. + */ + private Boolean yumiManager; + /** * 货运代理: true:是 false:不是. - */ + */ private Boolean freightAgent; /** diff --git a/rc-service/rc-service-console/docs/country-dashboard-game-metrics-design.md b/rc-service/rc-service-console/docs/country-dashboard-game-metrics-design.md new file mode 100644 index 00000000..e36a9ccd --- /dev/null +++ b/rc-service/rc-service-console/docs/country-dashboard-game-metrics-design.md @@ -0,0 +1,314 @@ +# 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,不触发实时原始流水扫描。 +- 国家筛选、游戏厂商筛选、游戏关键字筛选、分页、排序都能返回稳定结果。 +- 刷新任务失败时,现有数据大屏总览接口不受影响。 diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/bd/query/TeamBdMemberBillListQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/bd/query/TeamBdMemberBillListQryExe.java index d33ea415..dbae1d47 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/bd/query/TeamBdMemberBillListQryExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/bd/query/TeamBdMemberBillListQryExe.java @@ -164,6 +164,7 @@ public class TeamBdMemberBillListQryExe { TeamBdMemberCO result = new TeamBdMemberCO(); result.setMemberBillList(memberBillList); result.setMemberCount(memberBillList.size()); + result.setAgencyNumber(memberBillList.size()); result.setTotalSalary(totalSalary); result.setTotalRecharge(totalRecharge); result.setBillTitle(TeamBillCycleUtils.parseBillBelongToDateRangeStr(currentBillBelong)); @@ -222,6 +223,8 @@ public class TeamBdMemberBillListQryExe { TeamBdMemberCO result = new TeamBdMemberCO(); result.setMemberBillList(memberBillList); result.setMemberCount(memberBillList.size()); + result.setBdNumber(memberBillList.size()); + result.setAgencyNumber(totalAgentCount); result.setTotalSalary(totalSalary); result.setTotalRecharge(totalRecharge); result.setBillTitle(TeamBillCycleUtils.parseBillBelongToDateRangeStr(currentBillBelong)); @@ -321,6 +324,8 @@ public class TeamBdMemberBillListQryExe { TeamBdMemberCO result = new TeamBdMemberCO(); result.setMemberBillList(Collections.emptyList()); result.setMemberCount(0); + result.setAgencyNumber(0); + result.setBdNumber(0); result.setTotalSalary(BigDecimal.ZERO); result.setTotalRecharge(BigDecimal.ZERO); result.setBillTitle(TeamBillCycleUtils.parseBillBelongToDateRangeStr(currentBillBelong)); @@ -335,24 +340,45 @@ public class TeamBdMemberBillListQryExe { @NotNull private Map getRechargeMap(Integer currentBillBelong, Set teamIds) { + if (CollectionUtils.isEmpty(teamIds)) { + return Collections.emptyMap(); + } Map> mapByTeamIds = teamMemberService.mapByTeamIds(teamIds); + if (Objects.isNull(mapByTeamIds) || mapByTeamIds.isEmpty()) { + return Collections.emptyMap(); + } List memberIds = mapByTeamIds.values().stream() .flatMap(List::stream) .map(TeamMember::getMemberId) .toList(); + if (CollectionUtils.isEmpty(memberIds)) { + return Collections.emptyMap(); + } LocalDateTime startTime = TeamBillCycleUtils.getBillBelongStartTime(currentBillBelong); LocalDateTime endTime = TeamBillCycleUtils.getBillBelongEndTime(currentBillBelong); - ResultResponse> response = - inAppPurchaseDetailsClient.batchUserRechargeStatistics( - memberIds, - SysOriginPlatformEnum.LIKEI.name(), - startTime, - endTime - ); - List body = response.getBody(); - Map rechargeMap = body.stream().collect(Collectors.toMap(BatchUserRechargeStatisticsDTO::getUserId, BatchUserRechargeStatisticsDTO::getTotalAmount, BigDecimal::add)); - return rechargeMap; + try { + ResultResponse> response = + inAppPurchaseDetailsClient.batchUserRechargeStatistics( + memberIds, + SysOriginPlatformEnum.LIKEI.name(), + startTime, + endTime + ); + List body = Optional.ofNullable(response) + .map(ResultResponse::getBody) + .orElse(Collections.emptyList()); + if (CollectionUtils.isEmpty(body)) { + return Collections.emptyMap(); + } + return body.stream().collect(Collectors.toMap( + BatchUserRechargeStatisticsDTO::getUserId, + BatchUserRechargeStatisticsDTO::getTotalAmount, + BigDecimal::add)); + } catch (Exception ex) { + log.warn("Failed to query BD team recharge statistics, teamIds={}", teamIds, ex); + return Collections.emptyMap(); + } } private Collection buildBdTeamVO(List teamMembers) { diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/user/user/UserIdentityServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/user/user/UserIdentityServiceImpl.java index f774a4b9..00e0f01e 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/user/user/UserIdentityServiceImpl.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/user/user/UserIdentityServiceImpl.java @@ -62,10 +62,11 @@ public class UserIdentityServiceImpl implements UserIdentityService { .setSuperAdmin(isSuperAdmin) .setAdmin(isValidAdmin) .setBdLeader(ResponseAssert.requiredSuccess(bdTeamLeaderClient.check(userId))) - .setManager(isTeamManager) + .setManager(Boolean.FALSE) + .setYumiManager(isTeamManager) .setAppManager(isManager) ; - } + } @Override public Boolean hasPermission(PropsStoreTypeQryCmd cmd) { diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/TeamBdMemberCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/TeamBdMemberCO.java index d00efbb2..5fed1b42 100644 --- a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/TeamBdMemberCO.java +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/TeamBdMemberCO.java @@ -17,6 +17,10 @@ public class TeamBdMemberCO { private Integer memberCount; + private Integer agencyNumber; + + private Integer bdNumber; + private List memberBillList; private BdHistoryCO bdHistoryCO; diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/h5/UserIdentityVO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/h5/UserIdentityVO.java index 3d96aa34..456f6de8 100644 --- a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/h5/UserIdentityVO.java +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/h5/UserIdentityVO.java @@ -55,10 +55,16 @@ public class UserIdentityVO implements Serializable { private Boolean superAdmin; /** - * 是否是经理 + * 旧经理字段,Yumi 经理身份不再使用. */ private Boolean manager; + /** + * 是否是 Yumi 经理. + */ + @JsonProperty("yumi_manager") + private Boolean yumiManager; + /** * 是否是App管理员角色里的经理. */ diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/team/UserHistoryIdentity.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/team/UserHistoryIdentity.java index 520be5fd..e1847be5 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/team/UserHistoryIdentity.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/team/UserHistoryIdentity.java @@ -51,9 +51,15 @@ public class UserHistoryIdentity extends TimestampBaseEntity { private Boolean host; /** - * 是否做过团队经理. + * 是否做过旧团队经理. */ @TableField("is_manager") private Boolean manager; + /** + * 是否做过 Yumi 团队经理. + */ + @TableField("is_yumi_manager") + private Boolean yumiManager; + } diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/team/UserHistoryIdentityService.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/team/UserHistoryIdentityService.java index a03ddcca..0ae3dfd5 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/team/UserHistoryIdentityService.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/service/team/UserHistoryIdentityService.java @@ -22,6 +22,11 @@ public interface UserHistoryIdentityService extends BaseService saveYumiManager(Long userId) { + historyIdentityClientService.saveYumiManager(userId); + return ResultResponse.success(); + } } diff --git a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/UserHistoryIdentityClientService.java b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/UserHistoryIdentityClientService.java index ccdb339c..59b89467 100644 --- a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/UserHistoryIdentityClientService.java +++ b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/UserHistoryIdentityClientService.java @@ -23,4 +23,6 @@ public interface UserHistoryIdentityClientService { void saveBd(Long userId); void saveManager(Long userId); + + void saveYumiManager(Long userId); } diff --git a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/UserHistoryIdentityClientServiceImpl.java b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/UserHistoryIdentityClientServiceImpl.java index 9c83cc83..3937eef9 100644 --- a/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/UserHistoryIdentityClientServiceImpl.java +++ b/rc-service/rc-service-other/other-inner-endpoint/src/main/java/com/red/circle/other/app/inner/service/team/impl/UserHistoryIdentityClientServiceImpl.java @@ -55,4 +55,9 @@ public class UserHistoryIdentityClientServiceImpl implements UserHistoryIdentity public void saveManager(Long userId) { historyIdentityService.saveManager(userId); } + + @Override + public void saveYumiManager(Long userId) { + historyIdentityService.saveYumiManager(userId); + } } diff --git a/sql/20260512_team_yumi_manager_identity.sql b/sql/20260512_team_yumi_manager_identity.sql new file mode 100644 index 00000000..3d2c582c --- /dev/null +++ b/sql/20260512_team_yumi_manager_identity.sql @@ -0,0 +1,16 @@ +SET @table_schema = DATABASE(); + +SET @ddl = IF( + EXISTS( + SELECT 1 + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @table_schema + AND TABLE_NAME = 'user_history_identity' + AND COLUMN_NAME = 'is_yumi_manager' + ), + 'SELECT 1', + "ALTER TABLE `user_history_identity` ADD COLUMN `is_yumi_manager` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否做过Yumi团队经理' AFTER `is_manager`" +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt;