完善接口
This commit is contained in:
parent
0bfe1cf57d
commit
1b3cf31a7d
19
AGENTS.md
19
AGENTS.md
@ -21,10 +21,23 @@
|
||||
- `notice-service` 负责从各 owner service 的 outbox/事件源读取事实并投递私有通知、腾讯云 IM 单聊、后续 push/inbox;钱包、活动和房间不能直接耦合私有通知投递实现。
|
||||
- `user-service` 负责用户主数据,不应该成为房间高频流程的同步中心。
|
||||
- `cron-service` 只负责后台调度、任务运行记录和通过内部 gRPC 触发 owner service 批处理;不能直接拥有房间、账务、用户或活动业务状态。
|
||||
- `statistics-service` 只负责消费 owner service 已发布到 RocketMQ 的业务事实并写本服务聚合表;它不是业务事实 owner,不能直连 room/wallet/user/game 等 owner 数据库补数或查询明细。
|
||||
- `server/admin` 是后台管理服务独立 module,负责管理员、RBAC、菜单、审计和后台 HTTP API;访问 app 后端只能通过 `hyapp.local/api` 生成的 protobuf client 或明确的集成接口。
|
||||
- `api` 是独立 Go module,module path 为 `hyapp.local/api`,只放 protobuf contract 和生成代码,不放服务实现或业务工具包。
|
||||
- `wallet_outbox` 和 `room_outbox` 只能由各自 owner service 直接读取并发布到 RocketMQ;其他服务不能配置 `wallet_mysql_dsn`、`room_mysql_dsn` 或直连 owner 数据库扫描 outbox。
|
||||
- 下游服务消费 wallet/room outbox 必须使用自己的 RocketMQ consumer group 和本服务幂等表;消费位点属于消息系统和本地幂等事实,不能共享 owner service 的 outbox 状态字段。
|
||||
- `wallet_outbox`、`room_outbox`、`user_outbox` 和 `game_outbox` 只能由各自 owner service 直接读取并发布到 RocketMQ;其他服务不能配置 owner 数据库 DSN 或直连 owner 数据库扫描 outbox。
|
||||
- 下游服务消费 owner outbox 必须使用自己的 RocketMQ consumer group 和本服务幂等表;消费位点属于消息系统和本地幂等事实,不能共享 owner service 的 outbox 状态字段。
|
||||
- 数据大屏查询入口在 `server/admin`,由 admin 调 `statistics-service` 内部查询 API;`gateway-service` 是 App HTTP 入口,除非有明确 App 产品需求,否则不要把后台统计大屏暴露到 gateway。
|
||||
|
||||
## Statistics Rules
|
||||
|
||||
- 硬性规则:统计查询不能查全表,不能回查 owner service 明细表,不能在 HTTP handler 临时扫业务流水;所有统计展示一律查询 `statistics-service` 的聚合表。
|
||||
- `statistics-service` 的事实输入只来自 RocketMQ:`WalletRechargeRecorded`、`GameOrderSettled`、`RoomGiftSent`、`RoomUserJoined`、`UserRegistered` 等事件必须先由 owner service outbox 持久化,再由 owner service 发布。
|
||||
- 充值口径以 `WalletRechargeRecorded` 为准;渠道分桶只能使用事件里的 `recharge_type`,当前缺渠道时归入币商充值,不能从支付表或用户资料反推。
|
||||
- 游戏口径以 `GameOrderSettled` 为准;`debit/bet` 计投注流水,`credit/payout` 计返奖,`refund/reverse` 计退款,利润按 `投注 - 返奖 - 退款` 计算。
|
||||
- 活跃口径以事实事件去重用户:进房来自 `RoomUserJoined`,游戏活跃来自 `GameOrderSettled`,统计服务用本地唯一键收敛日活,不能依赖 Redis presence。
|
||||
- 留存口径以 `UserRegistered` cohort 和 `stat_user_day_activity` 活跃表计算;注册、活跃、充值、游戏和礼物事件都按 UTC `stat_day` 聚合。
|
||||
- ARPU 直接由充值聚合除以活跃用户;ARPPU/UP 值直接由充值聚合除以付费用户。产品若要调整 UP 值定义,必须先改明确字段名和边界测试。
|
||||
- 国家维度只能来自事件字段,例如用户注册 region、房间 `visible_region_id`、充值目标区域或游戏事件区域;不能用服务所在地、IP 或本地时区推断。
|
||||
|
||||
## Persistence Rules
|
||||
|
||||
@ -32,6 +45,7 @@
|
||||
- `memory` repository/directory 只用于单元测试或明确的本地隔离测试。
|
||||
- MySQL 是房间恢复的持久来源:`rooms`、`room_snapshots`、`room_command_log`、`room_outbox`。
|
||||
- `wallet-service` 的 `wallet_outbox` 是账务事实的持久来源;它负责补偿发布到 RocketMQ,下游不得把钱包库当作事件查询库。
|
||||
- `statistics-service` 的 MySQL 聚合表是数据大屏唯一查询来源:`statistics_event_consumption` 记录幂等事实,`stat_app_day_country`、`stat_game_day_country`、`stat_user_day_activity`、`stat_user_registration`、`stat_recharge_day_payers` 和 `stat_lucky_gift_day_payers` 保存统计读模型。
|
||||
- Redis 只保存当前路由/lease 等热状态,不能作为唯一恢复来源。
|
||||
- Redis lease 更新必须保持原子性,不能改回非原子的 GET + SET 接管逻辑。
|
||||
|
||||
@ -68,6 +82,7 @@
|
||||
- `13007`: cron gRPC
|
||||
- `13008`: game gRPC
|
||||
- `13009`: notice gRPC
|
||||
- `13010`: statistics query HTTP
|
||||
- `23306`: local Docker MySQL host port
|
||||
- `13379`: local Docker Redis host port
|
||||
|
||||
|
||||
54
README.md
54
README.md
@ -15,6 +15,7 @@ HY Voice Room 是语音房后端 monorepo。当前架构不再自研 IM 长连
|
||||
- `activity-service` 当前承接每日任务、活动/系统消息基础能力和房间事件 consumer;可通过 room outbox direct gRPC 或 RocketMQ `hyapp_room_outbox` 独立 consumer group 消费房间事实。
|
||||
- `cron-service` 当前负责后台调度、任务运行记录,以及通过内部 gRPC 触发 owner service 批处理;它不直接拥有房间、账务、用户或活动业务状态。
|
||||
- `notice-service` 当前消费 `wallet_outbox` 的余额变更事件和 `room_outbox` 的踢人事件,异步投递腾讯云 IM 单聊私有通知;房间群消息和群成员控制仍由 `room-service` 的房间 IM bridge 处理,MQ 模式下通过独立 consumer group 解耦。
|
||||
- `statistics-service` 当前消费 user/wallet/room/game outbox 的 RocketMQ 事实,写入本服务 MySQL 聚合表,并通过内部 HTTP 查询 API 支撑后台数据大屏;它不拥有注册、账务、房间或游戏业务事实。
|
||||
- 腾讯云 IM 承担客户端长连接、群消息、公屏、单聊、离线/漫游和全球接入能力;本仓库不再部署 `im-service`。
|
||||
- 多 App 共用同一套后端服务,入口由 `gateway-service` 通过包名或 `app_code` 解析租户;所有业务库表按 `app_code` 隔离,设计细节见 `docs/多App租户架构.md`。
|
||||
|
||||
@ -23,9 +24,10 @@ HY Voice Room 是语音房后端 monorepo。当前架构不再自研 IM 长连
|
||||
- 客户端命令面:`gateway-service` 暴露 HTTP JSON。
|
||||
- 客户端实时面:腾讯云 IM SDK,客户端用 gateway 签发的 UserSig 登录。
|
||||
- 服务内部调用:gRPC + protobuf,契约在独立 Go module `api`,业务代码通过 `hyapp.local/api/proto/...` 引用生成包。
|
||||
- 房间外事件:protobuf outbox 先落 MySQL;生产可由 RocketMQ fanout 到 activity、notice、IM bridge 和后续 push/inbox,消费端按 `event_id` 幂等。
|
||||
- outbox 读取权只属于 owner service。`room-service` 读取 `room_outbox` 并发布 RocketMQ,`wallet-service` 读取 `wallet_outbox` 并发布 RocketMQ;其他服务不能通过 `room_mysql_dsn`、`wallet_mysql_dsn` 直连 owner 数据库扫描 outbox。
|
||||
- 下游消费 wallet/room outbox 必须使用本服务独立 RocketMQ consumer group,并在本服务库内用幂等表或唯一键记录已消费 `event_id`;消费位点不能写回或复用 owner service 的 outbox 状态。
|
||||
- 房间外事件:protobuf/JSON outbox 先落 MySQL;生产可由 RocketMQ fanout 到 activity、notice、IM bridge、statistics 和后续 push/inbox,消费端按 `event_id` 幂等。
|
||||
- outbox 读取权只属于 owner service。`room-service` 读取 `room_outbox`、`wallet-service` 读取 `wallet_outbox`、`user-service` 读取 `user_outbox`、`game-service` 读取 `game_outbox` 并发布 RocketMQ;其他服务不能直连 owner 数据库扫描 outbox。
|
||||
- 下游消费 owner outbox 必须使用本服务独立 RocketMQ consumer group,并在本服务库内用幂等表或唯一键记录已消费 `event_id`;消费位点不能写回或复用 owner service 的 outbox 状态。
|
||||
- 后台统计查询面:`server/admin` 调 `statistics-service` 内部 HTTP API;`gateway-service` 是 App 入口,除非有明确 App 产品需求,否则不暴露后台数据大屏统计。
|
||||
|
||||
## Time Model
|
||||
|
||||
@ -51,9 +53,40 @@ HY Voice Room 是语音房后端 monorepo。当前架构不再自研 IM 长连
|
||||
| `cron-service` | `13007` | gRPC |
|
||||
| `game-service` | `13008` | gRPC |
|
||||
| `notice-service` | `13009` | gRPC |
|
||||
| `statistics-service` | `13010` | internal query HTTP |
|
||||
| `statistics-service` | `13110` | health HTTP |
|
||||
| MySQL | `23306 -> 3306` | local Docker only |
|
||||
| Redis | `13379 -> 6379` | local Docker only |
|
||||
|
||||
## Statistics
|
||||
|
||||
统计系统的硬边界是“不查全表,不回查 owner 明细库”。数据大屏只能查询 `statistics-service` 的聚合表;任何新指标都必须先定义事件事实、聚合表字段和 UTC 时间范围,再接入查询接口。
|
||||
|
||||
事实来源:
|
||||
|
||||
- `user-service`: `UserRegistered`,用于新增用户和留存 cohort。
|
||||
- `wallet-service`: `WalletRechargeRecorded`,用于总充值、首充、付费用户、币商/MifaPay/Google 渠道分桶、ARPU、ARPPU 和 UP 值。
|
||||
- `room-service`: `RoomUserJoined` 用于进房活跃,`RoomGiftSent` 用于礼物消耗、幸运礼物流水和幸运礼物付费用户;国家维度来自事件中的 `visible_region_id`。
|
||||
- `game-service`: `GameOrderSettled`,`debit/bet` 计投注流水,`credit/payout` 计返奖,`refund/reverse` 计退款;游戏利润为 `投注 - 返奖 - 退款`。
|
||||
|
||||
聚合读模型:
|
||||
|
||||
- `statistics_event_consumption`: 本服务消费幂等表,按 `app_code + source + event_id` 去重。
|
||||
- `stat_app_day_country`: App/国家/UTC 日总览,保存新增、活跃、付费、充值渠道、礼物、幸运礼物、游戏总览。
|
||||
- `stat_user_day_activity`: 用户 UTC 日活跃事实,用于 DAU 和留存 join。
|
||||
- `stat_user_registration`: 注册 cohort,用于次留、7 留和 30 留。
|
||||
- `stat_recharge_day_payers`: 充值付费用户去重,用于付费人数、ARPPU 和 UP 值。
|
||||
- `stat_lucky_gift_day_payers`: 幸运礼物付费用户去重。
|
||||
- `stat_game_day_country` / `stat_game_day_players`: 游戏维度流水、返奖、退款、玩家数和排行。
|
||||
|
||||
查询契约:
|
||||
|
||||
```text
|
||||
GET /internal/v1/statistics/overview?app_code=lalu&start_ms=<UTC ms>&end_ms=<UTC ms>&country_id=<optional>
|
||||
```
|
||||
|
||||
`start_ms`/`end_ms` 统一使用 `[start_ms, end_ms)`。返回值只来自聚合表;`ARPU = recharge_usd_minor / active_users`,`ARPPU = recharge_usd_minor / paid_users`,当前 `up_value_usd_minor` 与 ARPPU 同口径。产品若要改变 UP 值定义,必须先改字段名、契约和边界测试。
|
||||
|
||||
## Run Locally
|
||||
|
||||
Use Docker for local and test environments:
|
||||
@ -134,12 +167,12 @@ docs/flutter对接/Flutter App 通知与钱包余额对接.md
|
||||
|
||||
## RocketMQ
|
||||
|
||||
本地默认不依赖 RocketMQ,`room-service` 可使用 `outbox_worker.publish_mode=direct` 直连腾讯云 IM publisher 和 activity gRPC publisher。线上推荐启用腾讯云 RocketMQ,并把 room/wallet outbox 统一发布到消息总线,下游服务各自 consumer group 消费。
|
||||
本地默认不依赖 RocketMQ,`room-service` 可使用 `outbox_worker.publish_mode=direct` 直连腾讯云 IM publisher 和 activity gRPC publisher。线上推荐启用腾讯云 RocketMQ,并把 owner outbox 统一发布到消息总线,下游服务各自 consumer group 消费。
|
||||
|
||||
规则:
|
||||
|
||||
- `room_outbox` 只由 `room-service` 读取和发布;`wallet_outbox` 只由 `wallet-service` 读取和发布。
|
||||
- `user-service`、`activity-service`、`notice-service`、`cron-service` 等非 owner service 不能配置 `room_mysql_dsn`、`wallet_mysql_dsn` 或扫描其他服务 outbox 表。
|
||||
- `room_outbox` 只由 `room-service` 读取和发布;`wallet_outbox` 只由 `wallet-service` 读取和发布;`user_outbox` 只由 `user-service` 读取和发布;`game_outbox` 只由 `game-service` 读取和发布。
|
||||
- `statistics-service`、`activity-service`、`notice-service`、`cron-service` 等非 owner service 不能配置 owner 数据库 DSN 或扫描其他服务 outbox 表。
|
||||
- 每个业务消费者使用独立 consumer group。即使消费同一个 topic,不同业务也不能共用 group,否则会互相抢消息。
|
||||
- 每个消费者在自己的库内维护幂等事实,例如按 `app_code + event_id` 或业务唯一键建唯一约束;RocketMQ 的消费位点只解决投递进度,不替代业务幂等。
|
||||
- owner service 的 MySQL outbox 仍是可靠事实源。RocketMQ 发布失败由 owner outbox worker 补偿重试;下游处理失败由各自 consumer group 重投和本地幂等表收敛。
|
||||
@ -152,13 +185,17 @@ docs/flutter对接/Flutter App 通知与钱包余额对接.md
|
||||
| `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-notice-room-outbox` | notice-service 消费 `RoomUserKicked` 等私有通知事实 |
|
||||
| `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-room-im-bridge` | room-service IM bridge 消费房间群系统消息和群成员控制事实 |
|
||||
| `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-user-mictime-room-outbox` | user-service 消费 `RoomMicChanged` 并更新用户麦时读模型 |
|
||||
| `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-statistics-room-outbox` | statistics-service 消费 `RoomGiftSent`、`RoomUserJoined` 并更新聚合表 |
|
||||
| `hyapp_user_outbox` | `hyapp-user-outbox-producer` | `hyapp-statistics-user-outbox` | statistics-service 消费 `UserRegistered` 并更新注册 cohort |
|
||||
| `hyapp_wallet_outbox` | `hyapp-wallet-outbox-producer` | `hyapp-user-invite-wallet-outbox` | user-service 消费 `WalletRechargeRecorded` 并更新邀请有效充值 |
|
||||
| `hyapp_wallet_outbox` | `hyapp-wallet-outbox-producer` | `hyapp-activity-first-recharge-wallet-outbox` | activity-service 消费 `WalletRechargeRecorded` 并处理首充奖励 |
|
||||
| `hyapp_wallet_outbox` | `hyapp-wallet-outbox-producer` | `hyapp-activity-red-packet-wallet-outbox` | activity-service 消费 `WalletRedPacketCreated` 并生成区域飘屏 outbox |
|
||||
| `hyapp_wallet_outbox` | `hyapp-wallet-outbox-producer` | `hyapp-notice-wallet-outbox` | notice-service 消费余额、账务等私有通知事实 |
|
||||
| `hyapp_wallet_outbox` | `hyapp-wallet-outbox-producer` | `hyapp-statistics-wallet-outbox` | statistics-service 消费 `WalletRechargeRecorded` 并更新充值聚合 |
|
||||
| `hyapp_game_outbox` | `hyapp-game-outbox-producer` | `hyapp-statistics-game-outbox` | statistics-service 消费 `GameOrderSettled` 并更新游戏聚合 |
|
||||
| `hyapp_room_treasure_open` | `hyapp-room-treasure-open-producer` | `hyapp-room-treasure-open` | 宝箱倒计时到点唤醒 room-service 开箱命令 |
|
||||
|
||||
RocketMQ 不拥有房间或账务状态。`room_outbox`、`wallet_outbox` 仍是各 owner service MySQL 内的可靠事实源;MQ 发布失败时 outbox worker 退避重试,到达重试上限后进入死信。宝箱延迟消息只负责唤醒,到点后 room-service 会重新校验 `box_id`、等级、`open_at_ms`、状态和 UTC 重置边界。
|
||||
RocketMQ 不拥有业务状态。`room_outbox`、`wallet_outbox`、`user_outbox`、`game_outbox` 仍是各 owner service MySQL 内的可靠事实源;MQ 发布失败时 outbox worker 退避重试,到达重试上限后进入死信。宝箱延迟消息只负责唤醒,到点后 room-service 会重新校验 `box_id`、等级、`open_at_ms`、状态和 UTC 重置边界。
|
||||
|
||||
## Storage Model
|
||||
|
||||
@ -171,6 +208,7 @@ MySQL 是本地和线上运行的持久化底座:
|
||||
- `cron-service`: `hyapp_cron`,保存后台任务定义、运行记录和调度锁;实际业务批处理仍由各 owner service 执行。
|
||||
- `game-service`: `hyapp_game`,保存游戏平台、房间游戏会话和游戏交易映射。
|
||||
- `notice-service`: `hyapp_notice`,保存外部通知投递位点、重试锁和死信状态;业务事实仍留在 owner service outbox。
|
||||
- `statistics-service`: `hyapp_statistics`,保存事件消费幂等和数据大屏聚合读模型;禁止把 owner service 明细表当作统计查询来源。
|
||||
- `hyapp_admin`: 后台管理后端独立库,只保存后台账号、权限和审计;不要把后台审计耦合进 App 业务库。
|
||||
|
||||
`room-service` uses MySQL as the room recovery durable source:
|
||||
@ -220,6 +258,7 @@ services/activity-service/ activity foundation and room event consumer boundary
|
||||
services/cron-service/ background scheduling and owner-service batch trigger
|
||||
services/game-service/ game session and wallet integration
|
||||
services/notice-service/ async user notice delivery workers
|
||||
services/statistics-service/ data dashboard event consumers and aggregate query API
|
||||
```
|
||||
|
||||
## Current Limits
|
||||
@ -230,4 +269,5 @@ services/notice-service/ async user notice delivery workers
|
||||
- `notice-service` 本地默认不启用腾讯云 IM worker;线上启用钱包通知或房间通知 RocketMQ consumer 时,必须同时启用 `tencent_im.enabled=true`,且不能直连 wallet/room 数据库扫描 outbox。
|
||||
- `activity-service` 已承接每日任务、消息/活动基础能力和 room outbox consumer;本地可走 direct gRPC,线上推荐走 RocketMQ `hyapp_room_outbox`。
|
||||
- `room-service` App 已启动 `room_outbox` 补偿 worker;`direct` 模式表示直连 publisher 完成,`mq` 模式表示事件已发布到 RocketMQ,下游成功由各服务消费位点保证。
|
||||
- `statistics-service` 只保证从已发布 MQ 事件生成聚合表;如果历史库没有对应 outbox 事实,例如早期用户没有 `UserRegistered`、本地没有 `game_outbox`,统计结果会按当前事实为 0,不允许临时扫业务表补数。
|
||||
- JWT、腾讯云 IM secret 等本地配置只能用开发值,线上必须走安全配置源。
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.26.1
|
||||
// protoc v7.35.0
|
||||
// source: proto/activity/v1/activity.proto
|
||||
|
||||
package activityv1
|
||||
@ -11415,6 +11415,274 @@ func (x *ListLuckyGiftDrawsResponse) GetTotal() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
type LuckyGiftDrawSummary struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
PoolId string `protobuf:"bytes,1,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
||||
TotalDraws int64 `protobuf:"varint,2,opt,name=total_draws,json=totalDraws,proto3" json:"total_draws,omitempty"`
|
||||
UniqueUsers int64 `protobuf:"varint,3,opt,name=unique_users,json=uniqueUsers,proto3" json:"unique_users,omitempty"`
|
||||
UniqueRooms int64 `protobuf:"varint,4,opt,name=unique_rooms,json=uniqueRooms,proto3" json:"unique_rooms,omitempty"`
|
||||
TotalSpentCoins int64 `protobuf:"varint,5,opt,name=total_spent_coins,json=totalSpentCoins,proto3" json:"total_spent_coins,omitempty"`
|
||||
TotalRewardCoins int64 `protobuf:"varint,6,opt,name=total_reward_coins,json=totalRewardCoins,proto3" json:"total_reward_coins,omitempty"`
|
||||
BaseRewardCoins int64 `protobuf:"varint,7,opt,name=base_reward_coins,json=baseRewardCoins,proto3" json:"base_reward_coins,omitempty"`
|
||||
RoomAtmosphereRewardCoins int64 `protobuf:"varint,8,opt,name=room_atmosphere_reward_coins,json=roomAtmosphereRewardCoins,proto3" json:"room_atmosphere_reward_coins,omitempty"`
|
||||
ActivitySubsidyCoins int64 `protobuf:"varint,9,opt,name=activity_subsidy_coins,json=activitySubsidyCoins,proto3" json:"activity_subsidy_coins,omitempty"`
|
||||
ActualRtpPpm int64 `protobuf:"varint,10,opt,name=actual_rtp_ppm,json=actualRtpPpm,proto3" json:"actual_rtp_ppm,omitempty"`
|
||||
PendingDraws int64 `protobuf:"varint,11,opt,name=pending_draws,json=pendingDraws,proto3" json:"pending_draws,omitempty"`
|
||||
GrantedDraws int64 `protobuf:"varint,12,opt,name=granted_draws,json=grantedDraws,proto3" json:"granted_draws,omitempty"`
|
||||
FailedDraws int64 `protobuf:"varint,13,opt,name=failed_draws,json=failedDraws,proto3" json:"failed_draws,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) Reset() {
|
||||
*x = LuckyGiftDrawSummary{}
|
||||
mi := &file_proto_activity_v1_activity_proto_msgTypes[141]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*LuckyGiftDrawSummary) ProtoMessage() {}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_activity_v1_activity_proto_msgTypes[141]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use LuckyGiftDrawSummary.ProtoReflect.Descriptor instead.
|
||||
func (*LuckyGiftDrawSummary) Descriptor() ([]byte, []int) {
|
||||
return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{141}
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetPoolId() string {
|
||||
if x != nil {
|
||||
return x.PoolId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetTotalDraws() int64 {
|
||||
if x != nil {
|
||||
return x.TotalDraws
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetUniqueUsers() int64 {
|
||||
if x != nil {
|
||||
return x.UniqueUsers
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetUniqueRooms() int64 {
|
||||
if x != nil {
|
||||
return x.UniqueRooms
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetTotalSpentCoins() int64 {
|
||||
if x != nil {
|
||||
return x.TotalSpentCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetTotalRewardCoins() int64 {
|
||||
if x != nil {
|
||||
return x.TotalRewardCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetBaseRewardCoins() int64 {
|
||||
if x != nil {
|
||||
return x.BaseRewardCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetRoomAtmosphereRewardCoins() int64 {
|
||||
if x != nil {
|
||||
return x.RoomAtmosphereRewardCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetActivitySubsidyCoins() int64 {
|
||||
if x != nil {
|
||||
return x.ActivitySubsidyCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetActualRtpPpm() int64 {
|
||||
if x != nil {
|
||||
return x.ActualRtpPpm
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetPendingDraws() int64 {
|
||||
if x != nil {
|
||||
return x.PendingDraws
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetGrantedDraws() int64 {
|
||||
if x != nil {
|
||||
return x.GrantedDraws
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *LuckyGiftDrawSummary) GetFailedDraws() int64 {
|
||||
if x != nil {
|
||||
return x.FailedDraws
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetLuckyGiftDrawSummaryRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
GiftId string `protobuf:"bytes,2,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
|
||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
RoomId string `protobuf:"bytes,4,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"`
|
||||
Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"`
|
||||
PoolId string `protobuf:"bytes,6,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryRequest) Reset() {
|
||||
*x = GetLuckyGiftDrawSummaryRequest{}
|
||||
mi := &file_proto_activity_v1_activity_proto_msgTypes[142]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetLuckyGiftDrawSummaryRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_activity_v1_activity_proto_msgTypes[142]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetLuckyGiftDrawSummaryRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetLuckyGiftDrawSummaryRequest) Descriptor() ([]byte, []int) {
|
||||
return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{142}
|
||||
}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryRequest) GetMeta() *RequestMeta {
|
||||
if x != nil {
|
||||
return x.Meta
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryRequest) GetGiftId() string {
|
||||
if x != nil {
|
||||
return x.GiftId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryRequest) GetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryRequest) GetRoomId() string {
|
||||
if x != nil {
|
||||
return x.RoomId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryRequest) GetStatus() string {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryRequest) GetPoolId() string {
|
||||
if x != nil {
|
||||
return x.PoolId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetLuckyGiftDrawSummaryResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Summary *LuckyGiftDrawSummary `protobuf:"bytes,1,opt,name=summary,proto3" json:"summary,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryResponse) Reset() {
|
||||
*x = GetLuckyGiftDrawSummaryResponse{}
|
||||
mi := &file_proto_activity_v1_activity_proto_msgTypes[143]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetLuckyGiftDrawSummaryResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_activity_v1_activity_proto_msgTypes[143]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetLuckyGiftDrawSummaryResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetLuckyGiftDrawSummaryResponse) Descriptor() ([]byte, []int) {
|
||||
return file_proto_activity_v1_activity_proto_rawDescGZIP(), []int{143}
|
||||
}
|
||||
|
||||
func (x *GetLuckyGiftDrawSummaryResponse) GetSummary() *LuckyGiftDrawSummary {
|
||||
if x != nil {
|
||||
return x.Summary
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_proto_activity_v1_activity_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proto_activity_v1_activity_proto_rawDesc = "" +
|
||||
@ -12461,7 +12729,32 @@ const file_proto_activity_v1_activity_proto_rawDesc = "" +
|
||||
"\apool_id\x18\b \x01(\tR\x06poolId\"p\n" +
|
||||
"\x1aListLuckyGiftDrawsResponse\x12<\n" +
|
||||
"\x05draws\x18\x01 \x03(\v2&.hyapp.activity.v1.LuckyGiftDrawResultR\x05draws\x12\x14\n" +
|
||||
"\x05total\x18\x02 \x01(\x03R\x05total2\xe2\x01\n" +
|
||||
"\x05total\x18\x02 \x01(\x03R\x05total\"\xa6\x04\n" +
|
||||
"\x14LuckyGiftDrawSummary\x12\x17\n" +
|
||||
"\apool_id\x18\x01 \x01(\tR\x06poolId\x12\x1f\n" +
|
||||
"\vtotal_draws\x18\x02 \x01(\x03R\n" +
|
||||
"totalDraws\x12!\n" +
|
||||
"\funique_users\x18\x03 \x01(\x03R\vuniqueUsers\x12!\n" +
|
||||
"\funique_rooms\x18\x04 \x01(\x03R\vuniqueRooms\x12*\n" +
|
||||
"\x11total_spent_coins\x18\x05 \x01(\x03R\x0ftotalSpentCoins\x12,\n" +
|
||||
"\x12total_reward_coins\x18\x06 \x01(\x03R\x10totalRewardCoins\x12*\n" +
|
||||
"\x11base_reward_coins\x18\a \x01(\x03R\x0fbaseRewardCoins\x12?\n" +
|
||||
"\x1croom_atmosphere_reward_coins\x18\b \x01(\x03R\x19roomAtmosphereRewardCoins\x124\n" +
|
||||
"\x16activity_subsidy_coins\x18\t \x01(\x03R\x14activitySubsidyCoins\x12$\n" +
|
||||
"\x0eactual_rtp_ppm\x18\n" +
|
||||
" \x01(\x03R\factualRtpPpm\x12#\n" +
|
||||
"\rpending_draws\x18\v \x01(\x03R\fpendingDraws\x12#\n" +
|
||||
"\rgranted_draws\x18\f \x01(\x03R\fgrantedDraws\x12!\n" +
|
||||
"\ffailed_draws\x18\r \x01(\x03R\vfailedDraws\"\xd0\x01\n" +
|
||||
"\x1eGetLuckyGiftDrawSummaryRequest\x122\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1e.hyapp.activity.v1.RequestMetaR\x04meta\x12\x17\n" +
|
||||
"\agift_id\x18\x02 \x01(\tR\x06giftId\x12\x17\n" +
|
||||
"\auser_id\x18\x03 \x01(\x03R\x06userId\x12\x17\n" +
|
||||
"\aroom_id\x18\x04 \x01(\tR\x06roomId\x12\x16\n" +
|
||||
"\x06status\x18\x05 \x01(\tR\x06status\x12\x17\n" +
|
||||
"\apool_id\x18\x06 \x01(\tR\x06poolId\"d\n" +
|
||||
"\x1fGetLuckyGiftDrawSummaryResponse\x12A\n" +
|
||||
"\asummary\x18\x01 \x01(\v2'.hyapp.activity.v1.LuckyGiftDrawSummaryR\asummary2\xe2\x01\n" +
|
||||
"\x0fActivityService\x12_\n" +
|
||||
"\fPingActivity\x12&.hyapp.activity.v1.PingActivityRequest\x1a'.hyapp.activity.v1.PingActivityResponse\x12n\n" +
|
||||
"\x11GetActivityStatus\x12+.hyapp.activity.v1.GetActivityStatusRequest\x1a,.hyapp.activity.v1.GetActivityStatusResponse2\xb1\x06\n" +
|
||||
@ -12534,12 +12827,13 @@ const file_proto_activity_v1_activity_proto_rawDesc = "" +
|
||||
"\x0fUpsertLevelTier\x12).hyapp.activity.v1.UpsertLevelTierRequest\x1a*.hyapp.activity.v1.UpsertLevelTierResponse2\x9f\x02\n" +
|
||||
"\x17AdminAchievementService\x12u\n" +
|
||||
"\x1aListAchievementDefinitions\x12*.hyapp.activity.v1.ListAchievementsRequest\x1a+.hyapp.activity.v1.ListAchievementsResponse\x12\x8c\x01\n" +
|
||||
"\x1bUpsertAchievementDefinition\x125.hyapp.activity.v1.UpsertAchievementDefinitionRequest\x1a6.hyapp.activity.v1.UpsertAchievementDefinitionResponse2\xf2\x03\n" +
|
||||
"\x1bUpsertAchievementDefinition\x125.hyapp.activity.v1.UpsertAchievementDefinitionRequest\x1a6.hyapp.activity.v1.UpsertAchievementDefinitionResponse2\xf5\x04\n" +
|
||||
"\x15AdminLuckyGiftService\x12q\n" +
|
||||
"\x12GetLuckyGiftConfig\x12,.hyapp.activity.v1.GetLuckyGiftConfigRequest\x1a-.hyapp.activity.v1.GetLuckyGiftConfigResponse\x12z\n" +
|
||||
"\x15UpsertLuckyGiftConfig\x12/.hyapp.activity.v1.UpsertLuckyGiftConfigRequest\x1a0.hyapp.activity.v1.UpsertLuckyGiftConfigResponse\x12w\n" +
|
||||
"\x14ListLuckyGiftConfigs\x12..hyapp.activity.v1.ListLuckyGiftConfigsRequest\x1a/.hyapp.activity.v1.ListLuckyGiftConfigsResponse\x12q\n" +
|
||||
"\x12ListLuckyGiftDraws\x12,.hyapp.activity.v1.ListLuckyGiftDrawsRequest\x1a-.hyapp.activity.v1.ListLuckyGiftDrawsResponseB.Z,hyapp.local/api/proto/activity/v1;activityv1b\x06proto3"
|
||||
"\x12ListLuckyGiftDraws\x12,.hyapp.activity.v1.ListLuckyGiftDrawsRequest\x1a-.hyapp.activity.v1.ListLuckyGiftDrawsResponse\x12\x80\x01\n" +
|
||||
"\x17GetLuckyGiftDrawSummary\x121.hyapp.activity.v1.GetLuckyGiftDrawSummaryRequest\x1a2.hyapp.activity.v1.GetLuckyGiftDrawSummaryResponseB.Z,hyapp.local/api/proto/activity/v1;activityv1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_proto_activity_v1_activity_proto_rawDescOnce sync.Once
|
||||
@ -12553,7 +12847,7 @@ func file_proto_activity_v1_activity_proto_rawDescGZIP() []byte {
|
||||
return file_proto_activity_v1_activity_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proto_activity_v1_activity_proto_msgTypes = make([]protoimpl.MessageInfo, 141)
|
||||
var file_proto_activity_v1_activity_proto_msgTypes = make([]protoimpl.MessageInfo, 144)
|
||||
var file_proto_activity_v1_activity_proto_goTypes = []any{
|
||||
(*RequestMeta)(nil), // 0: hyapp.activity.v1.RequestMeta
|
||||
(*PingActivityRequest)(nil), // 1: hyapp.activity.v1.PingActivityRequest
|
||||
@ -12696,7 +12990,10 @@ var file_proto_activity_v1_activity_proto_goTypes = []any{
|
||||
(*ListLuckyGiftConfigsResponse)(nil), // 138: hyapp.activity.v1.ListLuckyGiftConfigsResponse
|
||||
(*ListLuckyGiftDrawsRequest)(nil), // 139: hyapp.activity.v1.ListLuckyGiftDrawsRequest
|
||||
(*ListLuckyGiftDrawsResponse)(nil), // 140: hyapp.activity.v1.ListLuckyGiftDrawsResponse
|
||||
(*v1.EventEnvelope)(nil), // 141: hyapp.events.room.v1.EventEnvelope
|
||||
(*LuckyGiftDrawSummary)(nil), // 141: hyapp.activity.v1.LuckyGiftDrawSummary
|
||||
(*GetLuckyGiftDrawSummaryRequest)(nil), // 142: hyapp.activity.v1.GetLuckyGiftDrawSummaryRequest
|
||||
(*GetLuckyGiftDrawSummaryResponse)(nil), // 143: hyapp.activity.v1.GetLuckyGiftDrawSummaryResponse
|
||||
(*v1.EventEnvelope)(nil), // 144: hyapp.events.room.v1.EventEnvelope
|
||||
}
|
||||
var file_proto_activity_v1_activity_proto_depIdxs = []int32{
|
||||
0, // 0: hyapp.activity.v1.PingActivityRequest.meta:type_name -> hyapp.activity.v1.RequestMeta
|
||||
@ -12721,7 +13018,7 @@ var file_proto_activity_v1_activity_proto_depIdxs = []int32{
|
||||
0, // 19: hyapp.activity.v1.PublishGlobalBroadcastRequest.meta:type_name -> hyapp.activity.v1.RequestMeta
|
||||
0, // 20: hyapp.activity.v1.RemoveRegionBroadcastMemberRequest.meta:type_name -> hyapp.activity.v1.RequestMeta
|
||||
0, // 21: hyapp.activity.v1.ConsumeRoomEventRequest.meta:type_name -> hyapp.activity.v1.RequestMeta
|
||||
141, // 22: hyapp.activity.v1.ConsumeRoomEventRequest.envelope:type_name -> hyapp.events.room.v1.EventEnvelope
|
||||
144, // 22: hyapp.activity.v1.ConsumeRoomEventRequest.envelope:type_name -> hyapp.events.room.v1.EventEnvelope
|
||||
0, // 23: hyapp.activity.v1.ListTaskDefinitionsRequest.meta:type_name -> hyapp.activity.v1.RequestMeta
|
||||
42, // 24: hyapp.activity.v1.ListTaskDefinitionsResponse.tasks:type_name -> hyapp.activity.v1.TaskDefinition
|
||||
0, // 25: hyapp.activity.v1.UpsertTaskDefinitionRequest.meta:type_name -> hyapp.activity.v1.RequestMeta
|
||||
@ -12812,129 +13109,133 @@ var file_proto_activity_v1_activity_proto_depIdxs = []int32{
|
||||
127, // 110: hyapp.activity.v1.ListLuckyGiftConfigsResponse.configs:type_name -> hyapp.activity.v1.LuckyGiftConfig
|
||||
0, // 111: hyapp.activity.v1.ListLuckyGiftDrawsRequest.meta:type_name -> hyapp.activity.v1.RequestMeta
|
||||
130, // 112: hyapp.activity.v1.ListLuckyGiftDrawsResponse.draws:type_name -> hyapp.activity.v1.LuckyGiftDrawResult
|
||||
1, // 113: hyapp.activity.v1.ActivityService.PingActivity:input_type -> hyapp.activity.v1.PingActivityRequest
|
||||
3, // 114: hyapp.activity.v1.ActivityService.GetActivityStatus:input_type -> hyapp.activity.v1.GetActivityStatusRequest
|
||||
6, // 115: hyapp.activity.v1.MessageInboxService.ListMessageTabs:input_type -> hyapp.activity.v1.ListMessageTabsRequest
|
||||
9, // 116: hyapp.activity.v1.MessageInboxService.ListInboxMessages:input_type -> hyapp.activity.v1.ListInboxMessagesRequest
|
||||
11, // 117: hyapp.activity.v1.MessageInboxService.MarkInboxMessageRead:input_type -> hyapp.activity.v1.MarkInboxMessageReadRequest
|
||||
13, // 118: hyapp.activity.v1.MessageInboxService.MarkInboxSectionRead:input_type -> hyapp.activity.v1.MarkInboxSectionReadRequest
|
||||
15, // 119: hyapp.activity.v1.MessageInboxService.DeleteInboxMessage:input_type -> hyapp.activity.v1.DeleteInboxMessageRequest
|
||||
17, // 120: hyapp.activity.v1.MessageInboxService.CreateInboxMessage:input_type -> hyapp.activity.v1.CreateInboxMessageRequest
|
||||
19, // 121: hyapp.activity.v1.MessageInboxService.CreateFanoutJob:input_type -> hyapp.activity.v1.CreateFanoutJobRequest
|
||||
21, // 122: hyapp.activity.v1.ActivityCronService.ProcessMessageFanoutBatch:input_type -> hyapp.activity.v1.CronBatchRequest
|
||||
21, // 123: hyapp.activity.v1.ActivityCronService.ProcessLevelRewardBatch:input_type -> hyapp.activity.v1.CronBatchRequest
|
||||
21, // 124: hyapp.activity.v1.ActivityCronService.ProcessAchievementRewardBatch:input_type -> hyapp.activity.v1.CronBatchRequest
|
||||
25, // 125: hyapp.activity.v1.TaskService.ListUserTasks:input_type -> hyapp.activity.v1.ListUserTasksRequest
|
||||
27, // 126: hyapp.activity.v1.TaskService.ClaimTaskReward:input_type -> hyapp.activity.v1.ClaimTaskRewardRequest
|
||||
29, // 127: hyapp.activity.v1.TaskService.ConsumeTaskEvent:input_type -> hyapp.activity.v1.ConsumeTaskEventRequest
|
||||
94, // 128: hyapp.activity.v1.GrowthLevelService.GetMyLevelOverview:input_type -> hyapp.activity.v1.GetMyLevelOverviewRequest
|
||||
96, // 129: hyapp.activity.v1.GrowthLevelService.GetLevelTrack:input_type -> hyapp.activity.v1.GetLevelTrackRequest
|
||||
99, // 130: hyapp.activity.v1.GrowthLevelService.ListLevelRewards:input_type -> hyapp.activity.v1.ListLevelRewardsRequest
|
||||
101, // 131: hyapp.activity.v1.GrowthLevelService.ConsumeLevelEvent:input_type -> hyapp.activity.v1.ConsumeLevelEventRequest
|
||||
114, // 132: hyapp.activity.v1.AchievementService.ListAchievements:input_type -> hyapp.activity.v1.ListAchievementsRequest
|
||||
116, // 133: hyapp.activity.v1.AchievementService.ConsumeAchievementEvent:input_type -> hyapp.activity.v1.ConsumeAchievementEventRequest
|
||||
119, // 134: hyapp.activity.v1.AchievementService.ListMyBadges:input_type -> hyapp.activity.v1.ListMyBadgesRequest
|
||||
121, // 135: hyapp.activity.v1.AchievementService.SetBadgeDisplay:input_type -> hyapp.activity.v1.SetBadgeDisplayRequest
|
||||
128, // 136: hyapp.activity.v1.LuckyGiftService.CheckLuckyGift:input_type -> hyapp.activity.v1.CheckLuckyGiftRequest
|
||||
131, // 137: hyapp.activity.v1.LuckyGiftService.ExecuteLuckyGiftDraw:input_type -> hyapp.activity.v1.ExecuteLuckyGiftDrawRequest
|
||||
32, // 138: hyapp.activity.v1.BroadcastService.EnsureBroadcastGroups:input_type -> hyapp.activity.v1.EnsureBroadcastGroupsRequest
|
||||
34, // 139: hyapp.activity.v1.BroadcastService.PublishRegionBroadcast:input_type -> hyapp.activity.v1.PublishRegionBroadcastRequest
|
||||
35, // 140: hyapp.activity.v1.BroadcastService.PublishGlobalBroadcast:input_type -> hyapp.activity.v1.PublishGlobalBroadcastRequest
|
||||
37, // 141: hyapp.activity.v1.BroadcastService.RemoveRegionBroadcastMember:input_type -> hyapp.activity.v1.RemoveRegionBroadcastMemberRequest
|
||||
21, // 142: hyapp.activity.v1.BroadcastService.ProcessBroadcastOutboxBatch:input_type -> hyapp.activity.v1.CronBatchRequest
|
||||
40, // 143: hyapp.activity.v1.RoomEventConsumerService.ConsumeRoomEvent:input_type -> hyapp.activity.v1.ConsumeRoomEventRequest
|
||||
43, // 144: hyapp.activity.v1.AdminTaskService.ListTaskDefinitions:input_type -> hyapp.activity.v1.ListTaskDefinitionsRequest
|
||||
45, // 145: hyapp.activity.v1.AdminTaskService.UpsertTaskDefinition:input_type -> hyapp.activity.v1.UpsertTaskDefinitionRequest
|
||||
47, // 146: hyapp.activity.v1.AdminTaskService.SetTaskDefinitionStatus:input_type -> hyapp.activity.v1.SetTaskDefinitionStatusRequest
|
||||
52, // 147: hyapp.activity.v1.RegistrationRewardService.GetRegistrationRewardEligibility:input_type -> hyapp.activity.v1.GetRegistrationRewardEligibilityRequest
|
||||
54, // 148: hyapp.activity.v1.RegistrationRewardService.IssueRegistrationReward:input_type -> hyapp.activity.v1.IssueRegistrationRewardRequest
|
||||
56, // 149: hyapp.activity.v1.AdminRegistrationRewardService.GetRegistrationRewardConfig:input_type -> hyapp.activity.v1.GetRegistrationRewardConfigRequest
|
||||
58, // 150: hyapp.activity.v1.AdminRegistrationRewardService.UpdateRegistrationRewardConfig:input_type -> hyapp.activity.v1.UpdateRegistrationRewardConfigRequest
|
||||
60, // 151: hyapp.activity.v1.AdminRegistrationRewardService.ListRegistrationRewardClaims:input_type -> hyapp.activity.v1.ListRegistrationRewardClaimsRequest
|
||||
66, // 152: hyapp.activity.v1.FirstRechargeRewardService.GetFirstRechargeRewardStatus:input_type -> hyapp.activity.v1.GetFirstRechargeRewardStatusRequest
|
||||
68, // 153: hyapp.activity.v1.FirstRechargeRewardService.ConsumeFirstRechargeReward:input_type -> hyapp.activity.v1.ConsumeFirstRechargeRewardRequest
|
||||
70, // 154: hyapp.activity.v1.AdminFirstRechargeRewardService.GetFirstRechargeRewardConfig:input_type -> hyapp.activity.v1.GetFirstRechargeRewardConfigRequest
|
||||
72, // 155: hyapp.activity.v1.AdminFirstRechargeRewardService.UpdateFirstRechargeRewardConfig:input_type -> hyapp.activity.v1.UpdateFirstRechargeRewardConfigRequest
|
||||
74, // 156: hyapp.activity.v1.AdminFirstRechargeRewardService.ListFirstRechargeRewardClaims:input_type -> hyapp.activity.v1.ListFirstRechargeRewardClaimsRequest
|
||||
79, // 157: hyapp.activity.v1.SevenDayCheckInService.GetSevenDayCheckInStatus:input_type -> hyapp.activity.v1.GetSevenDayCheckInStatusRequest
|
||||
81, // 158: hyapp.activity.v1.SevenDayCheckInService.SignSevenDayCheckIn:input_type -> hyapp.activity.v1.SignSevenDayCheckInRequest
|
||||
83, // 159: hyapp.activity.v1.AdminSevenDayCheckInService.GetSevenDayCheckInConfig:input_type -> hyapp.activity.v1.GetSevenDayCheckInConfigRequest
|
||||
85, // 160: hyapp.activity.v1.AdminSevenDayCheckInService.UpdateSevenDayCheckInConfig:input_type -> hyapp.activity.v1.UpdateSevenDayCheckInConfigRequest
|
||||
88, // 161: hyapp.activity.v1.AdminSevenDayCheckInService.ListSevenDayCheckInClaims:input_type -> hyapp.activity.v1.ListSevenDayCheckInClaimsRequest
|
||||
109, // 162: hyapp.activity.v1.AdminGrowthLevelService.ListLevelConfig:input_type -> hyapp.activity.v1.ListLevelConfigRequest
|
||||
103, // 163: hyapp.activity.v1.AdminGrowthLevelService.UpsertLevelTrack:input_type -> hyapp.activity.v1.UpsertLevelTrackRequest
|
||||
105, // 164: hyapp.activity.v1.AdminGrowthLevelService.UpsertLevelRule:input_type -> hyapp.activity.v1.UpsertLevelRuleRequest
|
||||
107, // 165: hyapp.activity.v1.AdminGrowthLevelService.UpsertLevelTier:input_type -> hyapp.activity.v1.UpsertLevelTierRequest
|
||||
114, // 166: hyapp.activity.v1.AdminAchievementService.ListAchievementDefinitions:input_type -> hyapp.activity.v1.ListAchievementsRequest
|
||||
123, // 167: hyapp.activity.v1.AdminAchievementService.UpsertAchievementDefinition:input_type -> hyapp.activity.v1.UpsertAchievementDefinitionRequest
|
||||
133, // 168: hyapp.activity.v1.AdminLuckyGiftService.GetLuckyGiftConfig:input_type -> hyapp.activity.v1.GetLuckyGiftConfigRequest
|
||||
135, // 169: hyapp.activity.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:input_type -> hyapp.activity.v1.UpsertLuckyGiftConfigRequest
|
||||
137, // 170: hyapp.activity.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:input_type -> hyapp.activity.v1.ListLuckyGiftConfigsRequest
|
||||
139, // 171: hyapp.activity.v1.AdminLuckyGiftService.ListLuckyGiftDraws:input_type -> hyapp.activity.v1.ListLuckyGiftDrawsRequest
|
||||
2, // 172: hyapp.activity.v1.ActivityService.PingActivity:output_type -> hyapp.activity.v1.PingActivityResponse
|
||||
4, // 173: hyapp.activity.v1.ActivityService.GetActivityStatus:output_type -> hyapp.activity.v1.GetActivityStatusResponse
|
||||
7, // 174: hyapp.activity.v1.MessageInboxService.ListMessageTabs:output_type -> hyapp.activity.v1.ListMessageTabsResponse
|
||||
10, // 175: hyapp.activity.v1.MessageInboxService.ListInboxMessages:output_type -> hyapp.activity.v1.ListInboxMessagesResponse
|
||||
12, // 176: hyapp.activity.v1.MessageInboxService.MarkInboxMessageRead:output_type -> hyapp.activity.v1.MarkInboxMessageReadResponse
|
||||
14, // 177: hyapp.activity.v1.MessageInboxService.MarkInboxSectionRead:output_type -> hyapp.activity.v1.MarkInboxSectionReadResponse
|
||||
16, // 178: hyapp.activity.v1.MessageInboxService.DeleteInboxMessage:output_type -> hyapp.activity.v1.DeleteInboxMessageResponse
|
||||
18, // 179: hyapp.activity.v1.MessageInboxService.CreateInboxMessage:output_type -> hyapp.activity.v1.CreateInboxMessageResponse
|
||||
20, // 180: hyapp.activity.v1.MessageInboxService.CreateFanoutJob:output_type -> hyapp.activity.v1.CreateFanoutJobResponse
|
||||
22, // 181: hyapp.activity.v1.ActivityCronService.ProcessMessageFanoutBatch:output_type -> hyapp.activity.v1.CronBatchResponse
|
||||
22, // 182: hyapp.activity.v1.ActivityCronService.ProcessLevelRewardBatch:output_type -> hyapp.activity.v1.CronBatchResponse
|
||||
22, // 183: hyapp.activity.v1.ActivityCronService.ProcessAchievementRewardBatch:output_type -> hyapp.activity.v1.CronBatchResponse
|
||||
26, // 184: hyapp.activity.v1.TaskService.ListUserTasks:output_type -> hyapp.activity.v1.ListUserTasksResponse
|
||||
28, // 185: hyapp.activity.v1.TaskService.ClaimTaskReward:output_type -> hyapp.activity.v1.ClaimTaskRewardResponse
|
||||
30, // 186: hyapp.activity.v1.TaskService.ConsumeTaskEvent:output_type -> hyapp.activity.v1.ConsumeTaskEventResponse
|
||||
95, // 187: hyapp.activity.v1.GrowthLevelService.GetMyLevelOverview:output_type -> hyapp.activity.v1.GetMyLevelOverviewResponse
|
||||
97, // 188: hyapp.activity.v1.GrowthLevelService.GetLevelTrack:output_type -> hyapp.activity.v1.GetLevelTrackResponse
|
||||
100, // 189: hyapp.activity.v1.GrowthLevelService.ListLevelRewards:output_type -> hyapp.activity.v1.ListLevelRewardsResponse
|
||||
102, // 190: hyapp.activity.v1.GrowthLevelService.ConsumeLevelEvent:output_type -> hyapp.activity.v1.ConsumeLevelEventResponse
|
||||
115, // 191: hyapp.activity.v1.AchievementService.ListAchievements:output_type -> hyapp.activity.v1.ListAchievementsResponse
|
||||
117, // 192: hyapp.activity.v1.AchievementService.ConsumeAchievementEvent:output_type -> hyapp.activity.v1.ConsumeAchievementEventResponse
|
||||
120, // 193: hyapp.activity.v1.AchievementService.ListMyBadges:output_type -> hyapp.activity.v1.ListMyBadgesResponse
|
||||
122, // 194: hyapp.activity.v1.AchievementService.SetBadgeDisplay:output_type -> hyapp.activity.v1.SetBadgeDisplayResponse
|
||||
129, // 195: hyapp.activity.v1.LuckyGiftService.CheckLuckyGift:output_type -> hyapp.activity.v1.CheckLuckyGiftResponse
|
||||
132, // 196: hyapp.activity.v1.LuckyGiftService.ExecuteLuckyGiftDraw:output_type -> hyapp.activity.v1.ExecuteLuckyGiftDrawResponse
|
||||
33, // 197: hyapp.activity.v1.BroadcastService.EnsureBroadcastGroups:output_type -> hyapp.activity.v1.EnsureBroadcastGroupsResponse
|
||||
36, // 198: hyapp.activity.v1.BroadcastService.PublishRegionBroadcast:output_type -> hyapp.activity.v1.PublishBroadcastResponse
|
||||
36, // 199: hyapp.activity.v1.BroadcastService.PublishGlobalBroadcast:output_type -> hyapp.activity.v1.PublishBroadcastResponse
|
||||
38, // 200: hyapp.activity.v1.BroadcastService.RemoveRegionBroadcastMember:output_type -> hyapp.activity.v1.RemoveRegionBroadcastMemberResponse
|
||||
39, // 201: hyapp.activity.v1.BroadcastService.ProcessBroadcastOutboxBatch:output_type -> hyapp.activity.v1.ProcessBroadcastOutboxBatchResponse
|
||||
41, // 202: hyapp.activity.v1.RoomEventConsumerService.ConsumeRoomEvent:output_type -> hyapp.activity.v1.ConsumeRoomEventResponse
|
||||
44, // 203: hyapp.activity.v1.AdminTaskService.ListTaskDefinitions:output_type -> hyapp.activity.v1.ListTaskDefinitionsResponse
|
||||
46, // 204: hyapp.activity.v1.AdminTaskService.UpsertTaskDefinition:output_type -> hyapp.activity.v1.UpsertTaskDefinitionResponse
|
||||
48, // 205: hyapp.activity.v1.AdminTaskService.SetTaskDefinitionStatus:output_type -> hyapp.activity.v1.SetTaskDefinitionStatusResponse
|
||||
53, // 206: hyapp.activity.v1.RegistrationRewardService.GetRegistrationRewardEligibility:output_type -> hyapp.activity.v1.GetRegistrationRewardEligibilityResponse
|
||||
55, // 207: hyapp.activity.v1.RegistrationRewardService.IssueRegistrationReward:output_type -> hyapp.activity.v1.IssueRegistrationRewardResponse
|
||||
57, // 208: hyapp.activity.v1.AdminRegistrationRewardService.GetRegistrationRewardConfig:output_type -> hyapp.activity.v1.GetRegistrationRewardConfigResponse
|
||||
59, // 209: hyapp.activity.v1.AdminRegistrationRewardService.UpdateRegistrationRewardConfig:output_type -> hyapp.activity.v1.UpdateRegistrationRewardConfigResponse
|
||||
61, // 210: hyapp.activity.v1.AdminRegistrationRewardService.ListRegistrationRewardClaims:output_type -> hyapp.activity.v1.ListRegistrationRewardClaimsResponse
|
||||
67, // 211: hyapp.activity.v1.FirstRechargeRewardService.GetFirstRechargeRewardStatus:output_type -> hyapp.activity.v1.GetFirstRechargeRewardStatusResponse
|
||||
69, // 212: hyapp.activity.v1.FirstRechargeRewardService.ConsumeFirstRechargeReward:output_type -> hyapp.activity.v1.ConsumeFirstRechargeRewardResponse
|
||||
71, // 213: hyapp.activity.v1.AdminFirstRechargeRewardService.GetFirstRechargeRewardConfig:output_type -> hyapp.activity.v1.GetFirstRechargeRewardConfigResponse
|
||||
73, // 214: hyapp.activity.v1.AdminFirstRechargeRewardService.UpdateFirstRechargeRewardConfig:output_type -> hyapp.activity.v1.UpdateFirstRechargeRewardConfigResponse
|
||||
75, // 215: hyapp.activity.v1.AdminFirstRechargeRewardService.ListFirstRechargeRewardClaims:output_type -> hyapp.activity.v1.ListFirstRechargeRewardClaimsResponse
|
||||
80, // 216: hyapp.activity.v1.SevenDayCheckInService.GetSevenDayCheckInStatus:output_type -> hyapp.activity.v1.GetSevenDayCheckInStatusResponse
|
||||
82, // 217: hyapp.activity.v1.SevenDayCheckInService.SignSevenDayCheckIn:output_type -> hyapp.activity.v1.SignSevenDayCheckInResponse
|
||||
84, // 218: hyapp.activity.v1.AdminSevenDayCheckInService.GetSevenDayCheckInConfig:output_type -> hyapp.activity.v1.GetSevenDayCheckInConfigResponse
|
||||
86, // 219: hyapp.activity.v1.AdminSevenDayCheckInService.UpdateSevenDayCheckInConfig:output_type -> hyapp.activity.v1.UpdateSevenDayCheckInConfigResponse
|
||||
89, // 220: hyapp.activity.v1.AdminSevenDayCheckInService.ListSevenDayCheckInClaims:output_type -> hyapp.activity.v1.ListSevenDayCheckInClaimsResponse
|
||||
110, // 221: hyapp.activity.v1.AdminGrowthLevelService.ListLevelConfig:output_type -> hyapp.activity.v1.ListLevelConfigResponse
|
||||
104, // 222: hyapp.activity.v1.AdminGrowthLevelService.UpsertLevelTrack:output_type -> hyapp.activity.v1.UpsertLevelTrackResponse
|
||||
106, // 223: hyapp.activity.v1.AdminGrowthLevelService.UpsertLevelRule:output_type -> hyapp.activity.v1.UpsertLevelRuleResponse
|
||||
108, // 224: hyapp.activity.v1.AdminGrowthLevelService.UpsertLevelTier:output_type -> hyapp.activity.v1.UpsertLevelTierResponse
|
||||
115, // 225: hyapp.activity.v1.AdminAchievementService.ListAchievementDefinitions:output_type -> hyapp.activity.v1.ListAchievementsResponse
|
||||
124, // 226: hyapp.activity.v1.AdminAchievementService.UpsertAchievementDefinition:output_type -> hyapp.activity.v1.UpsertAchievementDefinitionResponse
|
||||
134, // 227: hyapp.activity.v1.AdminLuckyGiftService.GetLuckyGiftConfig:output_type -> hyapp.activity.v1.GetLuckyGiftConfigResponse
|
||||
136, // 228: hyapp.activity.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:output_type -> hyapp.activity.v1.UpsertLuckyGiftConfigResponse
|
||||
138, // 229: hyapp.activity.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:output_type -> hyapp.activity.v1.ListLuckyGiftConfigsResponse
|
||||
140, // 230: hyapp.activity.v1.AdminLuckyGiftService.ListLuckyGiftDraws:output_type -> hyapp.activity.v1.ListLuckyGiftDrawsResponse
|
||||
172, // [172:231] is the sub-list for method output_type
|
||||
113, // [113:172] is the sub-list for method input_type
|
||||
113, // [113:113] is the sub-list for extension type_name
|
||||
113, // [113:113] is the sub-list for extension extendee
|
||||
0, // [0:113] is the sub-list for field type_name
|
||||
0, // 113: hyapp.activity.v1.GetLuckyGiftDrawSummaryRequest.meta:type_name -> hyapp.activity.v1.RequestMeta
|
||||
141, // 114: hyapp.activity.v1.GetLuckyGiftDrawSummaryResponse.summary:type_name -> hyapp.activity.v1.LuckyGiftDrawSummary
|
||||
1, // 115: hyapp.activity.v1.ActivityService.PingActivity:input_type -> hyapp.activity.v1.PingActivityRequest
|
||||
3, // 116: hyapp.activity.v1.ActivityService.GetActivityStatus:input_type -> hyapp.activity.v1.GetActivityStatusRequest
|
||||
6, // 117: hyapp.activity.v1.MessageInboxService.ListMessageTabs:input_type -> hyapp.activity.v1.ListMessageTabsRequest
|
||||
9, // 118: hyapp.activity.v1.MessageInboxService.ListInboxMessages:input_type -> hyapp.activity.v1.ListInboxMessagesRequest
|
||||
11, // 119: hyapp.activity.v1.MessageInboxService.MarkInboxMessageRead:input_type -> hyapp.activity.v1.MarkInboxMessageReadRequest
|
||||
13, // 120: hyapp.activity.v1.MessageInboxService.MarkInboxSectionRead:input_type -> hyapp.activity.v1.MarkInboxSectionReadRequest
|
||||
15, // 121: hyapp.activity.v1.MessageInboxService.DeleteInboxMessage:input_type -> hyapp.activity.v1.DeleteInboxMessageRequest
|
||||
17, // 122: hyapp.activity.v1.MessageInboxService.CreateInboxMessage:input_type -> hyapp.activity.v1.CreateInboxMessageRequest
|
||||
19, // 123: hyapp.activity.v1.MessageInboxService.CreateFanoutJob:input_type -> hyapp.activity.v1.CreateFanoutJobRequest
|
||||
21, // 124: hyapp.activity.v1.ActivityCronService.ProcessMessageFanoutBatch:input_type -> hyapp.activity.v1.CronBatchRequest
|
||||
21, // 125: hyapp.activity.v1.ActivityCronService.ProcessLevelRewardBatch:input_type -> hyapp.activity.v1.CronBatchRequest
|
||||
21, // 126: hyapp.activity.v1.ActivityCronService.ProcessAchievementRewardBatch:input_type -> hyapp.activity.v1.CronBatchRequest
|
||||
25, // 127: hyapp.activity.v1.TaskService.ListUserTasks:input_type -> hyapp.activity.v1.ListUserTasksRequest
|
||||
27, // 128: hyapp.activity.v1.TaskService.ClaimTaskReward:input_type -> hyapp.activity.v1.ClaimTaskRewardRequest
|
||||
29, // 129: hyapp.activity.v1.TaskService.ConsumeTaskEvent:input_type -> hyapp.activity.v1.ConsumeTaskEventRequest
|
||||
94, // 130: hyapp.activity.v1.GrowthLevelService.GetMyLevelOverview:input_type -> hyapp.activity.v1.GetMyLevelOverviewRequest
|
||||
96, // 131: hyapp.activity.v1.GrowthLevelService.GetLevelTrack:input_type -> hyapp.activity.v1.GetLevelTrackRequest
|
||||
99, // 132: hyapp.activity.v1.GrowthLevelService.ListLevelRewards:input_type -> hyapp.activity.v1.ListLevelRewardsRequest
|
||||
101, // 133: hyapp.activity.v1.GrowthLevelService.ConsumeLevelEvent:input_type -> hyapp.activity.v1.ConsumeLevelEventRequest
|
||||
114, // 134: hyapp.activity.v1.AchievementService.ListAchievements:input_type -> hyapp.activity.v1.ListAchievementsRequest
|
||||
116, // 135: hyapp.activity.v1.AchievementService.ConsumeAchievementEvent:input_type -> hyapp.activity.v1.ConsumeAchievementEventRequest
|
||||
119, // 136: hyapp.activity.v1.AchievementService.ListMyBadges:input_type -> hyapp.activity.v1.ListMyBadgesRequest
|
||||
121, // 137: hyapp.activity.v1.AchievementService.SetBadgeDisplay:input_type -> hyapp.activity.v1.SetBadgeDisplayRequest
|
||||
128, // 138: hyapp.activity.v1.LuckyGiftService.CheckLuckyGift:input_type -> hyapp.activity.v1.CheckLuckyGiftRequest
|
||||
131, // 139: hyapp.activity.v1.LuckyGiftService.ExecuteLuckyGiftDraw:input_type -> hyapp.activity.v1.ExecuteLuckyGiftDrawRequest
|
||||
32, // 140: hyapp.activity.v1.BroadcastService.EnsureBroadcastGroups:input_type -> hyapp.activity.v1.EnsureBroadcastGroupsRequest
|
||||
34, // 141: hyapp.activity.v1.BroadcastService.PublishRegionBroadcast:input_type -> hyapp.activity.v1.PublishRegionBroadcastRequest
|
||||
35, // 142: hyapp.activity.v1.BroadcastService.PublishGlobalBroadcast:input_type -> hyapp.activity.v1.PublishGlobalBroadcastRequest
|
||||
37, // 143: hyapp.activity.v1.BroadcastService.RemoveRegionBroadcastMember:input_type -> hyapp.activity.v1.RemoveRegionBroadcastMemberRequest
|
||||
21, // 144: hyapp.activity.v1.BroadcastService.ProcessBroadcastOutboxBatch:input_type -> hyapp.activity.v1.CronBatchRequest
|
||||
40, // 145: hyapp.activity.v1.RoomEventConsumerService.ConsumeRoomEvent:input_type -> hyapp.activity.v1.ConsumeRoomEventRequest
|
||||
43, // 146: hyapp.activity.v1.AdminTaskService.ListTaskDefinitions:input_type -> hyapp.activity.v1.ListTaskDefinitionsRequest
|
||||
45, // 147: hyapp.activity.v1.AdminTaskService.UpsertTaskDefinition:input_type -> hyapp.activity.v1.UpsertTaskDefinitionRequest
|
||||
47, // 148: hyapp.activity.v1.AdminTaskService.SetTaskDefinitionStatus:input_type -> hyapp.activity.v1.SetTaskDefinitionStatusRequest
|
||||
52, // 149: hyapp.activity.v1.RegistrationRewardService.GetRegistrationRewardEligibility:input_type -> hyapp.activity.v1.GetRegistrationRewardEligibilityRequest
|
||||
54, // 150: hyapp.activity.v1.RegistrationRewardService.IssueRegistrationReward:input_type -> hyapp.activity.v1.IssueRegistrationRewardRequest
|
||||
56, // 151: hyapp.activity.v1.AdminRegistrationRewardService.GetRegistrationRewardConfig:input_type -> hyapp.activity.v1.GetRegistrationRewardConfigRequest
|
||||
58, // 152: hyapp.activity.v1.AdminRegistrationRewardService.UpdateRegistrationRewardConfig:input_type -> hyapp.activity.v1.UpdateRegistrationRewardConfigRequest
|
||||
60, // 153: hyapp.activity.v1.AdminRegistrationRewardService.ListRegistrationRewardClaims:input_type -> hyapp.activity.v1.ListRegistrationRewardClaimsRequest
|
||||
66, // 154: hyapp.activity.v1.FirstRechargeRewardService.GetFirstRechargeRewardStatus:input_type -> hyapp.activity.v1.GetFirstRechargeRewardStatusRequest
|
||||
68, // 155: hyapp.activity.v1.FirstRechargeRewardService.ConsumeFirstRechargeReward:input_type -> hyapp.activity.v1.ConsumeFirstRechargeRewardRequest
|
||||
70, // 156: hyapp.activity.v1.AdminFirstRechargeRewardService.GetFirstRechargeRewardConfig:input_type -> hyapp.activity.v1.GetFirstRechargeRewardConfigRequest
|
||||
72, // 157: hyapp.activity.v1.AdminFirstRechargeRewardService.UpdateFirstRechargeRewardConfig:input_type -> hyapp.activity.v1.UpdateFirstRechargeRewardConfigRequest
|
||||
74, // 158: hyapp.activity.v1.AdminFirstRechargeRewardService.ListFirstRechargeRewardClaims:input_type -> hyapp.activity.v1.ListFirstRechargeRewardClaimsRequest
|
||||
79, // 159: hyapp.activity.v1.SevenDayCheckInService.GetSevenDayCheckInStatus:input_type -> hyapp.activity.v1.GetSevenDayCheckInStatusRequest
|
||||
81, // 160: hyapp.activity.v1.SevenDayCheckInService.SignSevenDayCheckIn:input_type -> hyapp.activity.v1.SignSevenDayCheckInRequest
|
||||
83, // 161: hyapp.activity.v1.AdminSevenDayCheckInService.GetSevenDayCheckInConfig:input_type -> hyapp.activity.v1.GetSevenDayCheckInConfigRequest
|
||||
85, // 162: hyapp.activity.v1.AdminSevenDayCheckInService.UpdateSevenDayCheckInConfig:input_type -> hyapp.activity.v1.UpdateSevenDayCheckInConfigRequest
|
||||
88, // 163: hyapp.activity.v1.AdminSevenDayCheckInService.ListSevenDayCheckInClaims:input_type -> hyapp.activity.v1.ListSevenDayCheckInClaimsRequest
|
||||
109, // 164: hyapp.activity.v1.AdminGrowthLevelService.ListLevelConfig:input_type -> hyapp.activity.v1.ListLevelConfigRequest
|
||||
103, // 165: hyapp.activity.v1.AdminGrowthLevelService.UpsertLevelTrack:input_type -> hyapp.activity.v1.UpsertLevelTrackRequest
|
||||
105, // 166: hyapp.activity.v1.AdminGrowthLevelService.UpsertLevelRule:input_type -> hyapp.activity.v1.UpsertLevelRuleRequest
|
||||
107, // 167: hyapp.activity.v1.AdminGrowthLevelService.UpsertLevelTier:input_type -> hyapp.activity.v1.UpsertLevelTierRequest
|
||||
114, // 168: hyapp.activity.v1.AdminAchievementService.ListAchievementDefinitions:input_type -> hyapp.activity.v1.ListAchievementsRequest
|
||||
123, // 169: hyapp.activity.v1.AdminAchievementService.UpsertAchievementDefinition:input_type -> hyapp.activity.v1.UpsertAchievementDefinitionRequest
|
||||
133, // 170: hyapp.activity.v1.AdminLuckyGiftService.GetLuckyGiftConfig:input_type -> hyapp.activity.v1.GetLuckyGiftConfigRequest
|
||||
135, // 171: hyapp.activity.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:input_type -> hyapp.activity.v1.UpsertLuckyGiftConfigRequest
|
||||
137, // 172: hyapp.activity.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:input_type -> hyapp.activity.v1.ListLuckyGiftConfigsRequest
|
||||
139, // 173: hyapp.activity.v1.AdminLuckyGiftService.ListLuckyGiftDraws:input_type -> hyapp.activity.v1.ListLuckyGiftDrawsRequest
|
||||
142, // 174: hyapp.activity.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:input_type -> hyapp.activity.v1.GetLuckyGiftDrawSummaryRequest
|
||||
2, // 175: hyapp.activity.v1.ActivityService.PingActivity:output_type -> hyapp.activity.v1.PingActivityResponse
|
||||
4, // 176: hyapp.activity.v1.ActivityService.GetActivityStatus:output_type -> hyapp.activity.v1.GetActivityStatusResponse
|
||||
7, // 177: hyapp.activity.v1.MessageInboxService.ListMessageTabs:output_type -> hyapp.activity.v1.ListMessageTabsResponse
|
||||
10, // 178: hyapp.activity.v1.MessageInboxService.ListInboxMessages:output_type -> hyapp.activity.v1.ListInboxMessagesResponse
|
||||
12, // 179: hyapp.activity.v1.MessageInboxService.MarkInboxMessageRead:output_type -> hyapp.activity.v1.MarkInboxMessageReadResponse
|
||||
14, // 180: hyapp.activity.v1.MessageInboxService.MarkInboxSectionRead:output_type -> hyapp.activity.v1.MarkInboxSectionReadResponse
|
||||
16, // 181: hyapp.activity.v1.MessageInboxService.DeleteInboxMessage:output_type -> hyapp.activity.v1.DeleteInboxMessageResponse
|
||||
18, // 182: hyapp.activity.v1.MessageInboxService.CreateInboxMessage:output_type -> hyapp.activity.v1.CreateInboxMessageResponse
|
||||
20, // 183: hyapp.activity.v1.MessageInboxService.CreateFanoutJob:output_type -> hyapp.activity.v1.CreateFanoutJobResponse
|
||||
22, // 184: hyapp.activity.v1.ActivityCronService.ProcessMessageFanoutBatch:output_type -> hyapp.activity.v1.CronBatchResponse
|
||||
22, // 185: hyapp.activity.v1.ActivityCronService.ProcessLevelRewardBatch:output_type -> hyapp.activity.v1.CronBatchResponse
|
||||
22, // 186: hyapp.activity.v1.ActivityCronService.ProcessAchievementRewardBatch:output_type -> hyapp.activity.v1.CronBatchResponse
|
||||
26, // 187: hyapp.activity.v1.TaskService.ListUserTasks:output_type -> hyapp.activity.v1.ListUserTasksResponse
|
||||
28, // 188: hyapp.activity.v1.TaskService.ClaimTaskReward:output_type -> hyapp.activity.v1.ClaimTaskRewardResponse
|
||||
30, // 189: hyapp.activity.v1.TaskService.ConsumeTaskEvent:output_type -> hyapp.activity.v1.ConsumeTaskEventResponse
|
||||
95, // 190: hyapp.activity.v1.GrowthLevelService.GetMyLevelOverview:output_type -> hyapp.activity.v1.GetMyLevelOverviewResponse
|
||||
97, // 191: hyapp.activity.v1.GrowthLevelService.GetLevelTrack:output_type -> hyapp.activity.v1.GetLevelTrackResponse
|
||||
100, // 192: hyapp.activity.v1.GrowthLevelService.ListLevelRewards:output_type -> hyapp.activity.v1.ListLevelRewardsResponse
|
||||
102, // 193: hyapp.activity.v1.GrowthLevelService.ConsumeLevelEvent:output_type -> hyapp.activity.v1.ConsumeLevelEventResponse
|
||||
115, // 194: hyapp.activity.v1.AchievementService.ListAchievements:output_type -> hyapp.activity.v1.ListAchievementsResponse
|
||||
117, // 195: hyapp.activity.v1.AchievementService.ConsumeAchievementEvent:output_type -> hyapp.activity.v1.ConsumeAchievementEventResponse
|
||||
120, // 196: hyapp.activity.v1.AchievementService.ListMyBadges:output_type -> hyapp.activity.v1.ListMyBadgesResponse
|
||||
122, // 197: hyapp.activity.v1.AchievementService.SetBadgeDisplay:output_type -> hyapp.activity.v1.SetBadgeDisplayResponse
|
||||
129, // 198: hyapp.activity.v1.LuckyGiftService.CheckLuckyGift:output_type -> hyapp.activity.v1.CheckLuckyGiftResponse
|
||||
132, // 199: hyapp.activity.v1.LuckyGiftService.ExecuteLuckyGiftDraw:output_type -> hyapp.activity.v1.ExecuteLuckyGiftDrawResponse
|
||||
33, // 200: hyapp.activity.v1.BroadcastService.EnsureBroadcastGroups:output_type -> hyapp.activity.v1.EnsureBroadcastGroupsResponse
|
||||
36, // 201: hyapp.activity.v1.BroadcastService.PublishRegionBroadcast:output_type -> hyapp.activity.v1.PublishBroadcastResponse
|
||||
36, // 202: hyapp.activity.v1.BroadcastService.PublishGlobalBroadcast:output_type -> hyapp.activity.v1.PublishBroadcastResponse
|
||||
38, // 203: hyapp.activity.v1.BroadcastService.RemoveRegionBroadcastMember:output_type -> hyapp.activity.v1.RemoveRegionBroadcastMemberResponse
|
||||
39, // 204: hyapp.activity.v1.BroadcastService.ProcessBroadcastOutboxBatch:output_type -> hyapp.activity.v1.ProcessBroadcastOutboxBatchResponse
|
||||
41, // 205: hyapp.activity.v1.RoomEventConsumerService.ConsumeRoomEvent:output_type -> hyapp.activity.v1.ConsumeRoomEventResponse
|
||||
44, // 206: hyapp.activity.v1.AdminTaskService.ListTaskDefinitions:output_type -> hyapp.activity.v1.ListTaskDefinitionsResponse
|
||||
46, // 207: hyapp.activity.v1.AdminTaskService.UpsertTaskDefinition:output_type -> hyapp.activity.v1.UpsertTaskDefinitionResponse
|
||||
48, // 208: hyapp.activity.v1.AdminTaskService.SetTaskDefinitionStatus:output_type -> hyapp.activity.v1.SetTaskDefinitionStatusResponse
|
||||
53, // 209: hyapp.activity.v1.RegistrationRewardService.GetRegistrationRewardEligibility:output_type -> hyapp.activity.v1.GetRegistrationRewardEligibilityResponse
|
||||
55, // 210: hyapp.activity.v1.RegistrationRewardService.IssueRegistrationReward:output_type -> hyapp.activity.v1.IssueRegistrationRewardResponse
|
||||
57, // 211: hyapp.activity.v1.AdminRegistrationRewardService.GetRegistrationRewardConfig:output_type -> hyapp.activity.v1.GetRegistrationRewardConfigResponse
|
||||
59, // 212: hyapp.activity.v1.AdminRegistrationRewardService.UpdateRegistrationRewardConfig:output_type -> hyapp.activity.v1.UpdateRegistrationRewardConfigResponse
|
||||
61, // 213: hyapp.activity.v1.AdminRegistrationRewardService.ListRegistrationRewardClaims:output_type -> hyapp.activity.v1.ListRegistrationRewardClaimsResponse
|
||||
67, // 214: hyapp.activity.v1.FirstRechargeRewardService.GetFirstRechargeRewardStatus:output_type -> hyapp.activity.v1.GetFirstRechargeRewardStatusResponse
|
||||
69, // 215: hyapp.activity.v1.FirstRechargeRewardService.ConsumeFirstRechargeReward:output_type -> hyapp.activity.v1.ConsumeFirstRechargeRewardResponse
|
||||
71, // 216: hyapp.activity.v1.AdminFirstRechargeRewardService.GetFirstRechargeRewardConfig:output_type -> hyapp.activity.v1.GetFirstRechargeRewardConfigResponse
|
||||
73, // 217: hyapp.activity.v1.AdminFirstRechargeRewardService.UpdateFirstRechargeRewardConfig:output_type -> hyapp.activity.v1.UpdateFirstRechargeRewardConfigResponse
|
||||
75, // 218: hyapp.activity.v1.AdminFirstRechargeRewardService.ListFirstRechargeRewardClaims:output_type -> hyapp.activity.v1.ListFirstRechargeRewardClaimsResponse
|
||||
80, // 219: hyapp.activity.v1.SevenDayCheckInService.GetSevenDayCheckInStatus:output_type -> hyapp.activity.v1.GetSevenDayCheckInStatusResponse
|
||||
82, // 220: hyapp.activity.v1.SevenDayCheckInService.SignSevenDayCheckIn:output_type -> hyapp.activity.v1.SignSevenDayCheckInResponse
|
||||
84, // 221: hyapp.activity.v1.AdminSevenDayCheckInService.GetSevenDayCheckInConfig:output_type -> hyapp.activity.v1.GetSevenDayCheckInConfigResponse
|
||||
86, // 222: hyapp.activity.v1.AdminSevenDayCheckInService.UpdateSevenDayCheckInConfig:output_type -> hyapp.activity.v1.UpdateSevenDayCheckInConfigResponse
|
||||
89, // 223: hyapp.activity.v1.AdminSevenDayCheckInService.ListSevenDayCheckInClaims:output_type -> hyapp.activity.v1.ListSevenDayCheckInClaimsResponse
|
||||
110, // 224: hyapp.activity.v1.AdminGrowthLevelService.ListLevelConfig:output_type -> hyapp.activity.v1.ListLevelConfigResponse
|
||||
104, // 225: hyapp.activity.v1.AdminGrowthLevelService.UpsertLevelTrack:output_type -> hyapp.activity.v1.UpsertLevelTrackResponse
|
||||
106, // 226: hyapp.activity.v1.AdminGrowthLevelService.UpsertLevelRule:output_type -> hyapp.activity.v1.UpsertLevelRuleResponse
|
||||
108, // 227: hyapp.activity.v1.AdminGrowthLevelService.UpsertLevelTier:output_type -> hyapp.activity.v1.UpsertLevelTierResponse
|
||||
115, // 228: hyapp.activity.v1.AdminAchievementService.ListAchievementDefinitions:output_type -> hyapp.activity.v1.ListAchievementsResponse
|
||||
124, // 229: hyapp.activity.v1.AdminAchievementService.UpsertAchievementDefinition:output_type -> hyapp.activity.v1.UpsertAchievementDefinitionResponse
|
||||
134, // 230: hyapp.activity.v1.AdminLuckyGiftService.GetLuckyGiftConfig:output_type -> hyapp.activity.v1.GetLuckyGiftConfigResponse
|
||||
136, // 231: hyapp.activity.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:output_type -> hyapp.activity.v1.UpsertLuckyGiftConfigResponse
|
||||
138, // 232: hyapp.activity.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:output_type -> hyapp.activity.v1.ListLuckyGiftConfigsResponse
|
||||
140, // 233: hyapp.activity.v1.AdminLuckyGiftService.ListLuckyGiftDraws:output_type -> hyapp.activity.v1.ListLuckyGiftDrawsResponse
|
||||
143, // 234: hyapp.activity.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:output_type -> hyapp.activity.v1.GetLuckyGiftDrawSummaryResponse
|
||||
175, // [175:235] is the sub-list for method output_type
|
||||
115, // [115:175] is the sub-list for method input_type
|
||||
115, // [115:115] is the sub-list for extension type_name
|
||||
115, // [115:115] is the sub-list for extension extendee
|
||||
0, // [0:115] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proto_activity_v1_activity_proto_init() }
|
||||
@ -12948,7 +13249,7 @@ func file_proto_activity_v1_activity_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_activity_v1_activity_proto_rawDesc), len(file_proto_activity_v1_activity_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 141,
|
||||
NumMessages: 144,
|
||||
NumExtensions: 0,
|
||||
NumServices: 19,
|
||||
},
|
||||
|
||||
@ -1272,6 +1272,35 @@ message ListLuckyGiftDrawsResponse {
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
message LuckyGiftDrawSummary {
|
||||
string pool_id = 1;
|
||||
int64 total_draws = 2;
|
||||
int64 unique_users = 3;
|
||||
int64 unique_rooms = 4;
|
||||
int64 total_spent_coins = 5;
|
||||
int64 total_reward_coins = 6;
|
||||
int64 base_reward_coins = 7;
|
||||
int64 room_atmosphere_reward_coins = 8;
|
||||
int64 activity_subsidy_coins = 9;
|
||||
int64 actual_rtp_ppm = 10;
|
||||
int64 pending_draws = 11;
|
||||
int64 granted_draws = 12;
|
||||
int64 failed_draws = 13;
|
||||
}
|
||||
|
||||
message GetLuckyGiftDrawSummaryRequest {
|
||||
RequestMeta meta = 1;
|
||||
string gift_id = 2;
|
||||
int64 user_id = 3;
|
||||
string room_id = 4;
|
||||
string status = 5;
|
||||
string pool_id = 6;
|
||||
}
|
||||
|
||||
message GetLuckyGiftDrawSummaryResponse {
|
||||
LuckyGiftDrawSummary summary = 1;
|
||||
}
|
||||
|
||||
// ActivityService 只暴露同步查询,不把事件消费伪装成 RPC。
|
||||
service ActivityService {
|
||||
rpc PingActivity(PingActivityRequest) returns (PingActivityResponse);
|
||||
@ -1405,4 +1434,5 @@ service AdminLuckyGiftService {
|
||||
rpc UpsertLuckyGiftConfig(UpsertLuckyGiftConfigRequest) returns (UpsertLuckyGiftConfigResponse);
|
||||
rpc ListLuckyGiftConfigs(ListLuckyGiftConfigsRequest) returns (ListLuckyGiftConfigsResponse);
|
||||
rpc ListLuckyGiftDraws(ListLuckyGiftDrawsRequest) returns (ListLuckyGiftDrawsResponse);
|
||||
rpc GetLuckyGiftDrawSummary(GetLuckyGiftDrawSummaryRequest) returns (GetLuckyGiftDrawSummaryResponse);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.26.1
|
||||
// - protoc v7.35.0
|
||||
// source: proto/activity/v1/activity.proto
|
||||
|
||||
package activityv1
|
||||
@ -3342,10 +3342,11 @@ var AdminAchievementService_ServiceDesc = grpc.ServiceDesc{
|
||||
}
|
||||
|
||||
const (
|
||||
AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/GetLuckyGiftConfig"
|
||||
AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/UpsertLuckyGiftConfig"
|
||||
AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/ListLuckyGiftConfigs"
|
||||
AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/ListLuckyGiftDraws"
|
||||
AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/GetLuckyGiftConfig"
|
||||
AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/UpsertLuckyGiftConfig"
|
||||
AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/ListLuckyGiftConfigs"
|
||||
AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/ListLuckyGiftDraws"
|
||||
AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName = "/hyapp.activity.v1.AdminLuckyGiftService/GetLuckyGiftDrawSummary"
|
||||
)
|
||||
|
||||
// AdminLuckyGiftServiceClient is the client API for AdminLuckyGiftService service.
|
||||
@ -3358,6 +3359,7 @@ type AdminLuckyGiftServiceClient interface {
|
||||
UpsertLuckyGiftConfig(ctx context.Context, in *UpsertLuckyGiftConfigRequest, opts ...grpc.CallOption) (*UpsertLuckyGiftConfigResponse, error)
|
||||
ListLuckyGiftConfigs(ctx context.Context, in *ListLuckyGiftConfigsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigsResponse, error)
|
||||
ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error)
|
||||
GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error)
|
||||
}
|
||||
|
||||
type adminLuckyGiftServiceClient struct {
|
||||
@ -3408,6 +3410,16 @@ func (c *adminLuckyGiftServiceClient) ListLuckyGiftDraws(ctx context.Context, in
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminLuckyGiftServiceClient) GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetLuckyGiftDrawSummaryResponse)
|
||||
err := c.cc.Invoke(ctx, AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AdminLuckyGiftServiceServer is the server API for AdminLuckyGiftService service.
|
||||
// All implementations must embed UnimplementedAdminLuckyGiftServiceServer
|
||||
// for forward compatibility.
|
||||
@ -3418,6 +3430,7 @@ type AdminLuckyGiftServiceServer interface {
|
||||
UpsertLuckyGiftConfig(context.Context, *UpsertLuckyGiftConfigRequest) (*UpsertLuckyGiftConfigResponse, error)
|
||||
ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error)
|
||||
ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error)
|
||||
GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error)
|
||||
mustEmbedUnimplementedAdminLuckyGiftServiceServer()
|
||||
}
|
||||
|
||||
@ -3440,6 +3453,9 @@ func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftConfigs(context.Con
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftDraws not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetLuckyGiftDrawSummary not implemented")
|
||||
}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) mustEmbedUnimplementedAdminLuckyGiftServiceServer() {}
|
||||
func (UnimplementedAdminLuckyGiftServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
@ -3533,6 +3549,24 @@ func _AdminLuckyGiftService_ListLuckyGiftDraws_Handler(srv interface{}, ctx cont
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminLuckyGiftService_GetLuckyGiftDrawSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetLuckyGiftDrawSummaryRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftDrawSummary(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftDrawSummary(ctx, req.(*GetLuckyGiftDrawSummaryRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// AdminLuckyGiftService_ServiceDesc is the grpc.ServiceDesc for AdminLuckyGiftService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -3556,6 +3590,10 @@ var AdminLuckyGiftService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ListLuckyGiftDraws",
|
||||
Handler: _AdminLuckyGiftService_ListLuckyGiftDraws_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetLuckyGiftDrawSummary",
|
||||
Handler: _AdminLuckyGiftService_GetLuckyGiftDrawSummary_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/activity/v1/activity.proto",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.26.1
|
||||
// protoc v7.35.0
|
||||
// source: proto/events/room/v1/events.proto
|
||||
|
||||
package roomeventsv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.26.1
|
||||
// protoc v7.35.0
|
||||
// source: proto/game/v1/game.proto
|
||||
|
||||
package gamev1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.26.1
|
||||
// - protoc v7.35.0
|
||||
// source: proto/game/v1/game.proto
|
||||
|
||||
package gamev1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.26.1
|
||||
// protoc v7.35.0
|
||||
// source: proto/room/v1/room.proto
|
||||
|
||||
package roomv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.26.1
|
||||
// - protoc v7.35.0
|
||||
// source: proto/room/v1/room.proto
|
||||
|
||||
package roomv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.26.1
|
||||
// protoc v7.35.0
|
||||
// source: proto/user/v1/auth.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.26.1
|
||||
// - protoc v7.35.0
|
||||
// source: proto/user/v1/auth.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.26.1
|
||||
// protoc v7.35.0
|
||||
// source: proto/user/v1/host.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.26.1
|
||||
// - protoc v7.35.0
|
||||
// source: proto/user/v1/host.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.26.1
|
||||
// protoc v7.35.0
|
||||
// source: proto/user/v1/user.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.26.1
|
||||
// - protoc v7.35.0
|
||||
// source: proto/user/v1/user.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v5.26.1
|
||||
// protoc v7.35.0
|
||||
// source: proto/wallet/v1/wallet.proto
|
||||
|
||||
package walletv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v5.26.1
|
||||
// - protoc v7.35.0
|
||||
// source: proto/wallet/v1/wallet.proto
|
||||
|
||||
package walletv1
|
||||
|
||||
382
docs/flutter对接/Google Play充值Flutter对接.md
Normal file
382
docs/flutter对接/Google Play充值Flutter对接.md
Normal file
@ -0,0 +1,382 @@
|
||||
# Flutter Google Play 充值对接
|
||||
|
||||
本文描述 Android App 接入 Google Play 一次性金币充值的 Flutter 调用流程。Flutter 只负责购买流程和提交 Google purchase token;订单校验、到账金额、入账、幂等和消费确认都以后端 `wallet-service` 为准。
|
||||
|
||||
所有 gateway HTTP 接口都返回统一外壳:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "ok",
|
||||
"request_id": "server-generated-request-id",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
Flutter 只有在 `code == "OK"` 时读取 `data`。其他错误必须记录 `request_id`,方便后端排查。
|
||||
|
||||
## 1. 当前实现状态
|
||||
|
||||
已实现:
|
||||
|
||||
- `GET /api/v1/wallet/recharge/products`:App 端充值商品列表。
|
||||
- `GET /api/v1/wallet/me/balances`:刷新金币余额。
|
||||
- 钱包余额 IM 通知:`WalletBalanceChanged`。
|
||||
|
||||
后端待补齐:
|
||||
|
||||
- `POST /api/v1/wallet/payments/google/confirm`:提交 Google purchase token,后端校验并入账。
|
||||
- Google Play RTDN / Pub/Sub 回调:只做补偿通知,不替代客户端确认接口。
|
||||
- `payment_orders` 和 Google provider 订单审计表。
|
||||
|
||||
Flutter 可以先按本文契约接商品列表和本地 Google Billing 流程;确认支付接口需要等后端补齐后联调。
|
||||
|
||||
## 2. Flutter 依赖
|
||||
|
||||
建议使用 Flutter 官方维护的 `in_app_purchase` 插件接 Google Play Billing。
|
||||
|
||||
`pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
in_app_purchase: <compatible_version>
|
||||
```
|
||||
|
||||
Android 真机联调要求:
|
||||
|
||||
- App 使用正式包名和签名,包名必须与 Play Console App 一致。
|
||||
- 测试账号加入 Play Console license testing 或内部测试轨道。
|
||||
- Play Console 中的一次性商品必须激活,并覆盖测试账号所在地区。
|
||||
|
||||
## 3. 获取充值商品列表
|
||||
|
||||
`GET /api/v1/wallet/recharge/products`
|
||||
|
||||
登录接口,需要 `Authorization: Bearer <access_token>`。gateway 会根据当前登录用户资料里的 `region_id` 过滤商品;Flutter 不传用户 ID 和区域 ID。
|
||||
|
||||
请求头:
|
||||
|
||||
| Header | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `Authorization` | 是 | App 登录 access token。 |
|
||||
| `X-App-Code` | 否 | App 编码,默认 `lalu`。 |
|
||||
| `X-App-Platform` | 是 | 固定传 `android`。也可以用 query `platform=android` 覆盖。 |
|
||||
|
||||
示例:
|
||||
|
||||
```http
|
||||
GET /api/v1/wallet/recharge/products
|
||||
Authorization: Bearer <access_token>
|
||||
X-App-Code: lalu
|
||||
X-App-Platform: android
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "ok",
|
||||
"request_id": "req_abc",
|
||||
"data": {
|
||||
"channels": ["google"],
|
||||
"products": [
|
||||
{
|
||||
"product_id": 11,
|
||||
"product_code": "iap_android_11",
|
||||
"product_name": "1500 Coins",
|
||||
"description": "coin pack",
|
||||
"platform": "android",
|
||||
"channel": "google",
|
||||
"currency_code": "USDT",
|
||||
"coin_amount": 1500,
|
||||
"amount_minor": 1500000,
|
||||
"amount_micro": 1500000,
|
||||
"policy_version": "",
|
||||
"region_ids": [2002],
|
||||
"resource_asset_type": "COIN",
|
||||
"status": "active",
|
||||
"enabled": true,
|
||||
"sort_order": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段规则:
|
||||
|
||||
| 字段 | Flutter 用途 |
|
||||
| --- | --- |
|
||||
| `product_id` | 后端本地商品 ID,提交确认支付时原样带回。 |
|
||||
| `product_code` | 当前作为 Google Play productId 使用。必须与 Play Console 商品 ID 完全一致。 |
|
||||
| `channel` | Android Google 支付固定为 `google`。 |
|
||||
| `coin_amount` | 到账金币数,展示和到账都以后端为准。 |
|
||||
| `amount_micro` / `amount_minor` | 后台配置金额,仅用于后台审计;Google 支付价格展示必须用 Google `ProductDetails`。 |
|
||||
| `resource_asset_type` | 当前固定为 `COIN`。 |
|
||||
| `enabled` / `status` | 只展示 `enabled=true && status=active` 的商品。 |
|
||||
|
||||
注意:当前后端 `product_code` 是自动生成值,例如 `iap_android_11`。如果 Play Console 已经创建了其他商品 ID,例如 `coins_150000_usd499`,后端需要先新增 `google_product_id` 或 `provider_product_id` 字段,Flutter 不要自行硬编码映射。
|
||||
|
||||
## 4. 查询 Google ProductDetails
|
||||
|
||||
Flutter 拿到后端商品列表后,只对 `channel=google` 且启用的商品查询 Google Play:
|
||||
|
||||
```dart
|
||||
final Set<String> googleProductIds = backendProducts
|
||||
.where((item) => item.channel == 'google' && item.enabled)
|
||||
.map((item) => item.productCode)
|
||||
.where((value) => value.isNotEmpty)
|
||||
.toSet();
|
||||
|
||||
final ProductDetailsResponse response =
|
||||
await InAppPurchase.instance.queryProductDetails(googleProductIds);
|
||||
```
|
||||
|
||||
展示合并规则:
|
||||
|
||||
- 金币数量用后端 `coin_amount`。
|
||||
- 商品是否展示以后端上架状态和 Google 查询结果共同决定。
|
||||
- 价格文案用 Google `ProductDetails.price`,不要用后端 `amount_micro`。
|
||||
- 如果后端商品存在但 Google 查不到 `ProductDetails`,该商品不展示,并记录客户端日志。
|
||||
|
||||
合并后的本地模型建议保留:
|
||||
|
||||
```dart
|
||||
class GoogleRechargeSku {
|
||||
GoogleRechargeSku({
|
||||
required this.productId,
|
||||
required this.googleProductId,
|
||||
required this.coinAmount,
|
||||
required this.productDetails,
|
||||
});
|
||||
|
||||
final int productId;
|
||||
final String googleProductId;
|
||||
final int coinAmount;
|
||||
final ProductDetails productDetails;
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 发起购买
|
||||
|
||||
金币充值是 consumable。为了避免客户端先消费导致后端未入账,Android 不要自动 consume;后端校验并入账后由后端调用 Google consume。
|
||||
|
||||
```dart
|
||||
final PurchaseParam purchaseParam = PurchaseParam(
|
||||
productDetails: sku.productDetails,
|
||||
);
|
||||
|
||||
await InAppPurchase.instance.buyConsumable(
|
||||
purchaseParam: purchaseParam,
|
||||
autoConsume: false,
|
||||
);
|
||||
```
|
||||
|
||||
Flutter 必须监听购买流:
|
||||
|
||||
```dart
|
||||
late final StreamSubscription<List<PurchaseDetails>> _purchaseSub;
|
||||
|
||||
void startPurchaseListener() {
|
||||
_purchaseSub = InAppPurchase.instance.purchaseStream.listen(
|
||||
_handlePurchases,
|
||||
onError: (Object error, StackTrace stackTrace) {
|
||||
// 记录错误并恢复按钮状态。
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
状态处理:
|
||||
|
||||
| `PurchaseStatus` | Flutter 行为 |
|
||||
| --- | --- |
|
||||
| `pending` | 展示处理中,不提交后端入账。 |
|
||||
| `purchased` | 提交后端确认接口。 |
|
||||
| `restored` | Android 一次性消耗品通常不依赖 restored;可以按补单逻辑提交后端去重。 |
|
||||
| `error` | 展示失败,恢复按钮。 |
|
||||
| `canceled` | 用户取消,不提示错误。 |
|
||||
|
||||
## 6. 确认 Google 支付
|
||||
|
||||
后端待实现接口:
|
||||
|
||||
`POST /api/v1/wallet/payments/google/confirm`
|
||||
|
||||
登录接口。Flutter 在 `PurchaseStatus.purchased` 后调用。接口必须可重复调用;同一个 purchase token 重复提交只能返回同一笔入账结果。
|
||||
|
||||
请求头:
|
||||
|
||||
| Header | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `Authorization` | 是 | App 登录 access token。 |
|
||||
| `X-App-Code` | 否 | App 编码,默认 `lalu`。 |
|
||||
| `X-App-Platform` | 是 | 固定传 `android`。 |
|
||||
|
||||
请求体建议契约:
|
||||
|
||||
```json
|
||||
{
|
||||
"command_id": "google-pay-<stable-id>",
|
||||
"product_id": 11,
|
||||
"product_code": "iap_android_11",
|
||||
"package_name": "com.example.hyapp",
|
||||
"purchase_token": "google_purchase_token",
|
||||
"order_id": "GPA.1111-2222-3333-44444",
|
||||
"purchase_time_ms": 1710000000000
|
||||
}
|
||||
```
|
||||
|
||||
字段规则:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `command_id` | 是 | 客户端幂等 ID。同一个 purchase token 重试必须使用同一个值。 |
|
||||
| `product_id` | 是 | 后端商品列表返回的本地商品 ID。 |
|
||||
| `product_code` | 是 | 后端商品列表返回的 Google productId。 |
|
||||
| `package_name` | 是 | 当前 Android 包名。 |
|
||||
| `purchase_token` | 是 | `PurchaseDetails.verificationData.serverVerificationData`。 |
|
||||
| `order_id` | 否 | 插件能拿到时传;后端最终以 Google API 查询结果为准。 |
|
||||
| `purchase_time_ms` | 否 | 客户端购买时间,仅用于日志。 |
|
||||
|
||||
`command_id` 建议由 token 派生,确保重试稳定:
|
||||
|
||||
```dart
|
||||
final String commandId = 'google-pay-${sha256Of(purchaseToken).substring(0, 24)}';
|
||||
```
|
||||
|
||||
成功响应建议契约:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "ok",
|
||||
"request_id": "req_abc",
|
||||
"data": {
|
||||
"payment_order_id": "pay_google_xxx",
|
||||
"transaction_id": "wtx_xxx",
|
||||
"status": "credited",
|
||||
"product_id": 11,
|
||||
"product_code": "iap_android_11",
|
||||
"coin_amount": 1500,
|
||||
"balance": {
|
||||
"asset_type": "COIN",
|
||||
"available_amount": 12800,
|
||||
"frozen_amount": 0,
|
||||
"version": 9
|
||||
},
|
||||
"idempotent_replay": false,
|
||||
"consume_state": "consumed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Flutter 成功处理:
|
||||
|
||||
- `status=credited`:展示充值成功,刷新 `/wallet/me/balances`。
|
||||
- `idempotent_replay=true`:按成功处理,不重复提示异常。
|
||||
- `consume_state=consume_pending`:仍按到账成功处理;后端会补偿 consume。
|
||||
- 后端成功后调用 `InAppPurchase.instance.completePurchase(purchaseDetails)`,结束客户端 pending 状态。
|
||||
|
||||
失败处理:
|
||||
|
||||
- `UNAUTHORIZED`:登录失效,走登录态恢复。
|
||||
- `INVALID_ARGUMENT`:商品 ID、token、包名等参数错误,记录 `request_id`。
|
||||
- `UPSTREAM_ERROR` / `INTERNAL_ERROR`:保留订单为待确认,稍后重试。
|
||||
- provider 校验失败:不入账,不 complete purchase;记录 `request_id` 并触发补单或客服入口。
|
||||
|
||||
## 7. 补单规则
|
||||
|
||||
Flutter 必须做补单,因为用户可能在支付成功后断网、杀进程或后端短时失败。
|
||||
|
||||
触发时机:
|
||||
|
||||
- App 启动后。
|
||||
- 登录成功后。
|
||||
- 进入钱包页后。
|
||||
- 购买确认接口返回 5xx 或网络失败后延迟重试。
|
||||
|
||||
处理规则:
|
||||
|
||||
1. 查询 Google Play 当前未完成购买。
|
||||
2. 对每个 purchase token 查本地 pending 队列。
|
||||
3. 重新调用 `/api/v1/wallet/payments/google/confirm`。
|
||||
4. 后端返回 `credited` 后 complete purchase。
|
||||
|
||||
本地 pending 记录建议保存:
|
||||
|
||||
```json
|
||||
{
|
||||
"product_id": 11,
|
||||
"product_code": "iap_android_11",
|
||||
"purchase_token_hash": "sha256-token",
|
||||
"purchase_token": "secure-local-token",
|
||||
"command_id": "google-pay-xxxx",
|
||||
"created_at_ms": 1710000000000,
|
||||
"last_error": "network timeout"
|
||||
}
|
||||
```
|
||||
|
||||
`purchase_token` 属于敏感数据。客户端日志不要打印明文 token;本地持久化也应尽量缩短保留时间。
|
||||
|
||||
## 8. 余额刷新
|
||||
|
||||
充值确认成功后,Flutter 需要主动刷新余额:
|
||||
|
||||
`GET /api/v1/wallet/me/balances?asset_type=COIN`
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "ok",
|
||||
"request_id": "req_abc",
|
||||
"data": {
|
||||
"balances": [
|
||||
{
|
||||
"asset_type": "COIN",
|
||||
"available_amount": 12800,
|
||||
"frozen_amount": 0,
|
||||
"version": 9
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
后端也会通过 IM 私信发送 `WalletBalanceChanged`,但 Flutter 不应只依赖 IM 通知。充值确认成功后主动刷新一次,IM 只作为实时补偿。
|
||||
|
||||
## 9. Flutter 页面流程
|
||||
|
||||
推荐顺序:
|
||||
|
||||
1. 进入钱包页,调用 `/wallet/recharge/products`。
|
||||
2. 用返回的 `product_code` 查询 Google `ProductDetails`。
|
||||
3. 合并后展示充值档位。
|
||||
4. 用户点击档位,调用 `buyConsumable(autoConsume: false)`。
|
||||
5. purchase stream 收到 `purchased`。
|
||||
6. 调用 `/wallet/payments/google/confirm`。
|
||||
7. 后端返回 `credited`。
|
||||
8. 调用 `completePurchase`。
|
||||
9. 刷新 `/wallet/me/balances`。
|
||||
10. 清理本地 pending 记录。
|
||||
|
||||
## 10. 不允许的做法
|
||||
|
||||
- 不允许 Flutter 根据本地配置决定到账金币数。
|
||||
- 不允许 Flutter 只看 Google 支付成功就本地加余额。
|
||||
- 不允许客户端自动 consume 后再提交后端。
|
||||
- 不允许把 `purchase_token`、Google 原始订单响应打印到普通日志。
|
||||
- 不允许用价格文案匹配商品;必须用 `product_code` / Google productId 匹配。
|
||||
|
||||
## 11. 与当前 Flutter 代码的差异
|
||||
|
||||
当前 `hyapp-flutter` 钱包页充值档位仍是本地假数据,后续需要替换为:
|
||||
|
||||
- 新增 wallet recharge product API model。
|
||||
- 在 `WalletLogic.onInit` 拉取 `/wallet/recharge/products`。
|
||||
- 查询 Google `ProductDetails` 并合并展示。
|
||||
- `topUp()` 从占位提示改为 Google Billing 购买。
|
||||
- 新增 purchase stream listener 和 pending purchase 补单队列。
|
||||
268
docs/统计服务技术方案.md
Normal file
268
docs/统计服务技术方案.md
Normal file
@ -0,0 +1,268 @@
|
||||
# 统计服务技术方案
|
||||
|
||||
## 目标
|
||||
|
||||
`statistics-service` 是后台数据大屏的统计读模型服务。它只消费各 owner service 已提交并发布到 RocketMQ 的业务事实,写入本服务 MySQL 聚合表,再向 `server/admin` 提供内部查询 API。
|
||||
|
||||
硬性规则:
|
||||
|
||||
- 统计查询不查全表。
|
||||
- 统计查询不回查 owner service 明细库。
|
||||
- 统计服务不直连 `hyapp_room`、`hyapp_wallet`、`hyapp_user`、`hyapp_game` 扫描业务表或 outbox。
|
||||
- 所有数据大屏指标只能查询 `hyapp_statistics` 聚合表。
|
||||
- 所有统计时间范围统一使用 UTC `[start_ms, end_ms)`。
|
||||
|
||||
## 服务边界
|
||||
|
||||
| 服务 | 职责 |
|
||||
| --- | --- |
|
||||
| `user-service` | 用户注册、用户主数据 owner;写 `user_outbox.UserRegistered` 并发布 MQ |
|
||||
| `wallet-service` | 账务和充值 owner;写 `wallet_outbox.WalletRechargeRecorded` 并发布 MQ |
|
||||
| `room-service` | 房间状态 owner;写 `room_outbox.RoomUserJoined`、`RoomGiftSent` 并发布 MQ |
|
||||
| `game-service` | 游戏订单 owner;写 `game_outbox.GameOrderSettled` 并发布 MQ |
|
||||
| `statistics-service` | 消费 MQ 事实,写统计幂等表和聚合表,提供内部统计查询 |
|
||||
| `server/admin` | 后台管理 HTTP API;调用 `statistics-service` 查询数据大屏 |
|
||||
| `gateway-service` | App HTTP 入口;默认不暴露后台数据大屏统计 |
|
||||
| RocketMQ/TDMQ | 事件 fanout 和消费位点,不拥有业务状态 |
|
||||
| MySQL `hyapp_statistics` | 统计服务自己的幂等事实和聚合读模型 |
|
||||
|
||||
`statistics-service` 不是业务事实 owner。任何缺失指标都必须先补 owner service 事件,再补统计消费逻辑;不能在统计服务里反查业务表绕过事件边界。
|
||||
|
||||
## 总体架构
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
User["user-service\nuser_outbox"] --> MQ["RocketMQ / TDMQ"]
|
||||
Wallet["wallet-service\nwallet_outbox"] --> MQ
|
||||
Room["room-service\nroom_outbox"] --> MQ
|
||||
Game["game-service\ngame_outbox"] --> MQ
|
||||
|
||||
MQ --> Stats["statistics-service\nconsumer groups"]
|
||||
Stats --> StatsDB["hyapp_statistics\nidempotency + aggregate tables"]
|
||||
Admin["server/admin"] --> StatsAPI["statistics-service\n/internal/v1/statistics/overview"]
|
||||
StatsAPI --> StatsDB
|
||||
Admin --> Dashboard["数据大屏"]
|
||||
```
|
||||
|
||||
## 事件事实
|
||||
|
||||
统计事件只从 MQ 进入。每个事件必须有稳定 `event_id`,统计服务按 `app_code + source + event_id` 幂等。
|
||||
|
||||
| Source | Topic | Event | Owner 语义 | 统计用途 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `user` | `hyapp_user_outbox` | `UserRegistered` | 用户注册事实 | 新增用户、留存 cohort |
|
||||
| `wallet` | `hyapp_wallet_outbox` | `WalletRechargeRecorded` | 充值到账事实 | 总充值、首充、付费用户、渠道分桶、ARPU、ARPPU、UP 值 |
|
||||
| `room` | `hyapp_room_outbox` | `RoomUserJoined` | 用户进房成功 | 活跃用户、房间国家维度活跃 |
|
||||
| `room` | `hyapp_room_outbox` | `RoomGiftSent` | 送礼已扣费且落房间表现 | 礼物消耗、幸运礼物流水、幸运礼物付费用户 |
|
||||
| `game` | `hyapp_game_outbox` | `GameOrderSettled` | 游戏订单已结算 | 游戏流水、返奖、退款、玩家数、游戏排行 |
|
||||
|
||||
事件字段边界:
|
||||
|
||||
- `app_code` 必须由 owner service 写入事件,统计服务只做默认值兜底,不做租户推断。
|
||||
- `occurred_at_ms` 是业务事实发生时间,必须使用 epoch milliseconds。
|
||||
- 国家维度只能来自事件字段:注册区域、充值目标区域、房间 `visible_region_id`、游戏事件区域。
|
||||
- `request_id` 只用于链路追踪,不能作为统计幂等键。
|
||||
- `event_id` 是统计幂等键,重复消息必须被本地幂等表收敛。
|
||||
|
||||
## 口径定义
|
||||
|
||||
### 用户与活跃
|
||||
|
||||
| 指标 | 口径 |
|
||||
| --- | --- |
|
||||
| 新增用户 | `UserRegistered` 按 UTC 日、`app_code`、国家维度聚合 |
|
||||
| 活跃用户 | `RoomUserJoined` 和 `GameOrderSettled` 写入 `stat_user_day_activity`,按用户去重 |
|
||||
| 次日留存 | 注册 cohort 中,注册日 + 1 天存在活跃记录的用户数 / 注册用户数 |
|
||||
| 7 日留存 | 注册 cohort 中,注册日 + 7 天存在活跃记录的用户数 / 注册用户数 |
|
||||
| 30 日留存 | 注册 cohort 中,注册日 + 30 天存在活跃记录的用户数 / 注册用户数 |
|
||||
|
||||
留存只 join `stat_user_registration` 和 `stat_user_day_activity`,不回查用户表、登录表或房间 presence。
|
||||
|
||||
### 充值
|
||||
|
||||
| 指标 | 口径 |
|
||||
| --- | --- |
|
||||
| 总充值 | `WalletRechargeRecorded.recharge_usd_minor` 累加 |
|
||||
| 新增充值 | `recharge_sequence == 1` 的充值金额累加 |
|
||||
| 付费用户 | `stat_recharge_day_payers` 按 UTC 日用户去重 |
|
||||
| 币商充值 | `recharge_type` 为空或非 `mifapay/google` 时归入币商充值 |
|
||||
| MifaPay 充值 | `recharge_type == mifapay` |
|
||||
| Google 充值 | `recharge_type == google` |
|
||||
| ARPU | 总充值 / 活跃用户 |
|
||||
| ARPPU | 总充值 / 付费用户 |
|
||||
| UP 值 | 当前与 ARPPU 同口径,即总充值 / 付费用户 |
|
||||
|
||||
充值统计以到账事实为准。支付创建、支付处理中、支付失败、币商库存进货都不能直接计入玩家充值。
|
||||
|
||||
### 礼物与幸运礼物
|
||||
|
||||
| 指标 | 口径 |
|
||||
| --- | --- |
|
||||
| 礼物消耗 | `RoomGiftSent.gift_value` 累加 |
|
||||
| 幸运礼物流水 | `RoomGiftSent.pool_id` 非空时的 `gift_value` 累加 |
|
||||
| 幸运礼物付费用户 | `pool_id` 非空的送礼用户按 UTC 日去重 |
|
||||
|
||||
房间礼物事件必须在 wallet 扣费成功后产生。统计服务不读取钱包流水来反推礼物消耗。
|
||||
|
||||
### 游戏
|
||||
|
||||
| 指标 | 口径 |
|
||||
| --- | --- |
|
||||
| 游戏流水 | `GameOrderSettled.op_type in (debit, bet)` 的 `coin_amount` 累加 |
|
||||
| 游戏返奖 | `op_type in (credit, payout)` 的 `coin_amount` 累加 |
|
||||
| 游戏退款 | `op_type in (refund, reverse)` 的 `coin_amount` 累加 |
|
||||
| 游戏利润 | 游戏流水 - 游戏返奖 - 游戏退款 |
|
||||
| 游戏利润率 | 游戏利润 / 游戏流水 |
|
||||
| 游戏参与人数 | 投注事件按用户、游戏、UTC 日去重 |
|
||||
| 游戏排行 | `stat_game_day_country` 按游戏流水倒序 |
|
||||
|
||||
游戏统计以游戏订单结算事实为准。统计服务不调用游戏厂商接口、不读取游戏明细表临时汇总。
|
||||
|
||||
## 聚合表设计
|
||||
|
||||
所有聚合表位于 `hyapp_statistics`,由 `statistics-service` 迁移和写入。
|
||||
|
||||
| 表 | 作用 | 主键/幂等边界 |
|
||||
| --- | --- | --- |
|
||||
| `statistics_event_consumption` | 消费幂等表 | `(app_code, source, event_id)` |
|
||||
| `stat_app_day_country` | App/UTC 日/国家总览 | `(app_code, stat_day, country_id)` |
|
||||
| `stat_user_day_activity` | 用户日活跃去重 | `(app_code, stat_day, user_id)` |
|
||||
| `stat_user_registration` | 注册 cohort | `(app_code, user_id)` |
|
||||
| `stat_recharge_day_payers` | 日付费用户去重 | `(app_code, stat_day, user_id)` |
|
||||
| `stat_lucky_gift_day_payers` | 幸运礼物日付费用户去重 | `(app_code, stat_day, country_id, user_id)` |
|
||||
| `stat_game_day_country` | 游戏日聚合和排行 | `(app_code, stat_day, country_id, platform_code, game_id)` |
|
||||
| `stat_game_day_players` | 游戏玩家去重 | `(app_code, stat_day, country_id, platform_code, game_id, user_id)` |
|
||||
|
||||
写入原则:
|
||||
|
||||
- 先写 `statistics_event_consumption`,再更新聚合表。
|
||||
- 已消费事件直接跳过,保证 MQ 重投不重复累加。
|
||||
- 唯一用户类指标使用 `INSERT IGNORE` 到去重表,再用 affected rows 增量更新聚合表。
|
||||
- 聚合表只追加或增量更新当前统计事实;修正历史数据必须通过重新投递事实或明确的重算任务,不允许在线查询扫明细补算。
|
||||
|
||||
## 查询契约
|
||||
|
||||
`statistics-service` 当前提供内部 HTTP 查询 API:
|
||||
|
||||
```text
|
||||
GET /internal/v1/statistics/overview?app_code=lalu&start_ms=<UTC ms>&end_ms=<UTC ms>&country_id=<optional>
|
||||
```
|
||||
|
||||
返回范围:
|
||||
|
||||
- 总览:新增、活跃、付费、充值渠道、礼物、幸运礼物、游戏流水/返奖/退款/利润。
|
||||
- ARPU/ARPPU/UP 值。
|
||||
- 次留、7 留、30 留。
|
||||
- 游戏排行。
|
||||
|
||||
调用边界:
|
||||
|
||||
- `server/admin` 调这个内部 API 后再返回后台前端。
|
||||
- `gateway-service` 默认不调用这个接口。
|
||||
- 查询参数必须是 UTC epoch milliseconds。
|
||||
- `country_id` 为空表示汇总全部国家;大于 0 表示指定国家维度。
|
||||
|
||||
## 部署方案
|
||||
|
||||
当前低成本部署使用:
|
||||
|
||||
```text
|
||||
CVM / Docker Compose
|
||||
TencentDB for MySQL
|
||||
TencentDB Redis
|
||||
TDMQ for RocketMQ
|
||||
statistics-service
|
||||
```
|
||||
|
||||
组件职责:
|
||||
|
||||
| 组件 | 用途 |
|
||||
| --- | --- |
|
||||
| CVM / Docker Compose | 部署 Go 服务和 RocketMQ worker 配置 |
|
||||
| TencentDB for MySQL | 保存 owner service 业务库和 `hyapp_statistics` 聚合库 |
|
||||
| TencentDB Redis | 房间 lease、路由和热状态 |
|
||||
| TDMQ for RocketMQ | owner outbox 到下游消费者的事件总线 |
|
||||
| `statistics-service` | 独立 consumer group 消费统计事件并提供内部查询 |
|
||||
|
||||
无 Kubernetes 可以运行。需要保证:
|
||||
|
||||
- 每个服务实例配置唯一 `node_id`。
|
||||
- RocketMQ topic 和 consumer group 在线上预先创建或由部署流程管理。
|
||||
- `statistics-service` 多实例消费时必须使用同一个统计 consumer group,依赖本地幂等表防重复。
|
||||
- MySQL DSN 使用 `loc=UTC`,容器 `TZ=UTC`。
|
||||
- `statistics-service` 查询端口只暴露给内网或 admin 服务,不对公网开放。
|
||||
|
||||
## 本地验证流程
|
||||
|
||||
本地端口:
|
||||
|
||||
| 服务 | 端口 |
|
||||
| --- | --- |
|
||||
| `statistics-service` 查询 HTTP | `13010` |
|
||||
| `statistics-service` health HTTP | `13110` |
|
||||
| MySQL | `23306` |
|
||||
| RocketMQ namesrv | `19876` 或 compose 当前映射 |
|
||||
| RocketMQ broker | `19009` / `19011` 或 compose 当前映射 |
|
||||
|
||||
基础验证:
|
||||
|
||||
```bash
|
||||
make proto
|
||||
make test
|
||||
docker compose config
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
真实链路验证:
|
||||
|
||||
1. 启动 MySQL、Redis、RocketMQ、owner service、`statistics-service`。
|
||||
2. 创建 topic 和 consumer group。
|
||||
3. 清空 `hyapp_statistics` 聚合表。
|
||||
4. 将已有 owner outbox 的目标事件标记为 `pending`。
|
||||
5. 启动 owner outbox worker 发布 MQ。
|
||||
6. 等待 `statistics_event_consumption` 和聚合表增长。
|
||||
7. 调用 `/internal/v1/statistics/overview` 检查返回。
|
||||
|
||||
典型检查 SQL:
|
||||
|
||||
```sql
|
||||
SELECT source, event_type, COUNT(*)
|
||||
FROM hyapp_statistics.statistics_event_consumption
|
||||
GROUP BY source, event_type
|
||||
ORDER BY source, event_type;
|
||||
|
||||
SELECT COUNT(*) AS rows_count,
|
||||
SUM(active_users) AS active_users,
|
||||
SUM(paid_users) AS paid_users,
|
||||
SUM(recharge_usd_minor) AS recharge_usd_minor,
|
||||
SUM(game_turnover) AS game_turnover,
|
||||
SUM(game_payout) AS game_payout,
|
||||
SUM(game_refund) AS game_refund,
|
||||
SUM(gift_coin_spent) AS gift_coin_spent
|
||||
FROM hyapp_statistics.stat_app_day_country;
|
||||
```
|
||||
|
||||
## 当前限制
|
||||
|
||||
- 历史库如果没有 `UserRegistered` outbox,留存 cohort 会按当前事实为 0。
|
||||
- 历史库如果没有 `game_outbox.GameOrderSettled`,游戏统计会按当前事实为 0。
|
||||
- MifaPay/Google 需要 owner 支付成功链路写出 `WalletRechargeRecorded` 且带 `recharge_type`,统计服务才能分桶。
|
||||
- 老版本 `RoomUserJoined` 如果没有 `visible_region_id`,历史活跃会落到国家 0;新事件必须带房间 `visible_region_id`。
|
||||
- 当前 `up_value_usd_minor` 与 ARPPU 同口径;产品调整定义时必须同步修改契约、字段和测试。
|
||||
|
||||
## 扩展规则
|
||||
|
||||
新增统计指标时按固定顺序推进:
|
||||
|
||||
1. 明确 owner service 和事实事件。
|
||||
2. 在 owner service 同一事务内写 outbox。
|
||||
3. 发布到 RocketMQ topic。
|
||||
4. 在 `statistics-service` 新增 consumer 解析和幂等消费。
|
||||
5. 新增或扩展聚合表。
|
||||
6. 扩展查询 API。
|
||||
7. 补 UTC `[start_ms, end_ms)` 边界测试。
|
||||
8. 用本地 MQ 真实链路验证。
|
||||
|
||||
不要从“数据大屏缺一个字段”直接跳到“查某个业务表”。统计系统的可扩展性来自稳定事件事实和聚合表,不来自临时 SQL。
|
||||
@ -28,6 +28,7 @@ type Client interface {
|
||||
UpsertLuckyGiftConfig(ctx context.Context, req *activityv1.UpsertLuckyGiftConfigRequest) (*activityv1.UpsertLuckyGiftConfigResponse, error)
|
||||
ListLuckyGiftConfigs(ctx context.Context, req *activityv1.ListLuckyGiftConfigsRequest) (*activityv1.ListLuckyGiftConfigsResponse, error)
|
||||
ListLuckyGiftDraws(ctx context.Context, req *activityv1.ListLuckyGiftDrawsRequest) (*activityv1.ListLuckyGiftDrawsResponse, error)
|
||||
GetLuckyGiftDrawSummary(ctx context.Context, req *activityv1.GetLuckyGiftDrawSummaryRequest) (*activityv1.GetLuckyGiftDrawSummaryResponse, error)
|
||||
RemoveRegionBroadcastMember(ctx context.Context, req *activityv1.RemoveRegionBroadcastMemberRequest) (*activityv1.RemoveRegionBroadcastMemberResponse, error)
|
||||
ListLevelConfig(ctx context.Context, req *activityv1.ListLevelConfigRequest) (*activityv1.ListLevelConfigResponse, error)
|
||||
UpsertLevelTrack(ctx context.Context, req *activityv1.UpsertLevelTrackRequest) (*activityv1.UpsertLevelTrackResponse, error)
|
||||
@ -131,6 +132,10 @@ func (c *GRPCClient) ListLuckyGiftDraws(ctx context.Context, req *activityv1.Lis
|
||||
return c.luckyGiftClient.ListLuckyGiftDraws(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetLuckyGiftDrawSummary(ctx context.Context, req *activityv1.GetLuckyGiftDrawSummaryRequest) (*activityv1.GetLuckyGiftDrawSummaryResponse, error) {
|
||||
return c.luckyGiftClient.GetLuckyGiftDrawSummary(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) RemoveRegionBroadcastMember(ctx context.Context, req *activityv1.RemoveRegionBroadcastMemberRequest) (*activityv1.RemoveRegionBroadcastMemberResponse, error) {
|
||||
return c.broadcastClient.RemoveRegionBroadcastMember(ctx, req)
|
||||
}
|
||||
|
||||
@ -132,6 +132,21 @@ func (AppVersion) TableName() string {
|
||||
return "admin_app_versions"
|
||||
}
|
||||
|
||||
type AppExploreTab struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
AppCode string `gorm:"size:32;uniqueIndex:uk_admin_app_explore_tabs_tab;index:idx_admin_app_explore_tabs_app_sort,not null;default:lalu" json:"appCode"`
|
||||
Tab string `gorm:"size:80;uniqueIndex:uk_admin_app_explore_tabs_tab,not null" json:"tab"`
|
||||
H5URL string `gorm:"column:h5_url;size:2048;not null" json:"h5Url"`
|
||||
Enabled bool `gorm:"index:idx_admin_app_explore_tabs_app_sort,not null;default:true" json:"enabled"`
|
||||
SortOrder int `gorm:"index:idx_admin_app_explore_tabs_app_sort,not null;default:0" json:"sortOrder"`
|
||||
CreatedAtMS int64 `gorm:"column:created_at_ms;autoCreateTime:milli" json:"createdAtMs"`
|
||||
UpdatedAtMS int64 `gorm:"column:updated_at_ms;autoUpdateTime:milli" json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
func (AppExploreTab) TableName() string {
|
||||
return "admin_app_explore_tabs"
|
||||
}
|
||||
|
||||
type RefreshToken struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"index;not null" json:"userId"`
|
||||
|
||||
@ -47,6 +47,55 @@ func (h *Handler) UpdateH5Links(c *gin.Context) {
|
||||
response.OK(c, gin.H{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
func (h *Handler) CreateH5Link(c *gin.Context) {
|
||||
var request h5LinkPayload
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
response.BadRequest(c, "参数不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.CreateH5Link(request)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
shared.OperationLog(c, h.audit, "create-app-h5-link", "admin_app_configs", "success", fmt.Sprintf("h5_key=%s", item.Key))
|
||||
response.Created(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateH5Link(c *gin.Context) {
|
||||
key := strings.TrimSpace(c.Param("key"))
|
||||
if key == "" {
|
||||
response.BadRequest(c, "h5 config key is invalid")
|
||||
return
|
||||
}
|
||||
var request h5LinkPayload
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
response.BadRequest(c, "参数不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.UpdateH5Link(key, request)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
shared.OperationLog(c, h.audit, "update-app-h5-link", "admin_app_configs", "success", fmt.Sprintf("h5_key=%s", item.Key))
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteH5Link(c *gin.Context) {
|
||||
key := strings.TrimSpace(c.Param("key"))
|
||||
if key == "" {
|
||||
response.BadRequest(c, "h5 config key is invalid")
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteH5Link(key); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
shared.OperationLog(c, h.audit, "delete-app-h5-link", "admin_app_configs", "success", fmt.Sprintf("h5_key=%s", key))
|
||||
response.OK(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
func (h *Handler) ListBanners(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
items, err := h.service.ListBanners(appctx.FromContext(c.Request.Context()), repository.AppBannerListOptions{
|
||||
@ -170,6 +219,75 @@ func (h *Handler) DeleteAppVersion(c *gin.Context) {
|
||||
response.OK(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
func (h *Handler) ListExploreTabs(c *gin.Context) {
|
||||
var enabled *bool
|
||||
switch strings.ToLower(strings.TrimSpace(c.Query("enabled"))) {
|
||||
case "true", "1", "enabled":
|
||||
value := true
|
||||
enabled = &value
|
||||
case "false", "0", "disabled":
|
||||
value := false
|
||||
enabled = &value
|
||||
}
|
||||
|
||||
items, err := h.service.ListExploreTabs(appctx.FromContext(c.Request.Context()), repository.AppExploreTabListOptions{
|
||||
Keyword: strings.TrimSpace(c.Query("keyword")),
|
||||
Enabled: enabled,
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取 Explore 配置失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
func (h *Handler) CreateExploreTab(c *gin.Context) {
|
||||
var request exploreTabRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
response.BadRequest(c, "参数不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.CreateExploreTab(appctx.FromContext(c.Request.Context()), request)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
shared.OperationLog(c, h.audit, "create-app-explore-tab", "admin_app_explore_tabs", "success", fmt.Sprintf("explore_tab_id=%d", item.ID))
|
||||
response.Created(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateExploreTab(c *gin.Context) {
|
||||
id, ok := exploreTabID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request exploreTabRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
response.BadRequest(c, "参数不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.UpdateExploreTab(appctx.FromContext(c.Request.Context()), id, request)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
shared.OperationLog(c, h.audit, "update-app-explore-tab", "admin_app_explore_tabs", "success", fmt.Sprintf("explore_tab_id=%d", item.ID))
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteExploreTab(c *gin.Context) {
|
||||
id, ok := exploreTabID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.service.DeleteExploreTab(appctx.FromContext(c.Request.Context()), id); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
shared.OperationLog(c, h.audit, "delete-app-explore-tab", "admin_app_explore_tabs", "success", fmt.Sprintf("explore_tab_id=%d", id))
|
||||
response.OK(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
func bannerID(c *gin.Context) (uint, bool) {
|
||||
value, err := strconv.ParseUint(c.Param("banner_id"), 10, 64)
|
||||
if err != nil || value == 0 {
|
||||
@ -188,6 +306,15 @@ func appVersionID(c *gin.Context) (uint, bool) {
|
||||
return uint(value), true
|
||||
}
|
||||
|
||||
func exploreTabID(c *gin.Context) (uint, bool) {
|
||||
value, err := strconv.ParseUint(c.Param("tab_id"), 10, 64)
|
||||
if err != nil || value == 0 {
|
||||
response.BadRequest(c, "explore tab id is invalid")
|
||||
return 0, false
|
||||
}
|
||||
return uint(value), true
|
||||
}
|
||||
|
||||
func parseOptionalInt64(values ...string) int64 {
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
|
||||
@ -5,8 +5,9 @@ type updateH5LinksRequest struct {
|
||||
}
|
||||
|
||||
type h5LinkPayload struct {
|
||||
Key string `json:"key" binding:"required"`
|
||||
URL string `json:"url"`
|
||||
Key string `json:"key" binding:"required"`
|
||||
Label string `json:"label" binding:"required"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
}
|
||||
|
||||
type bannerRequest struct {
|
||||
@ -33,3 +34,10 @@ type appVersionRequest struct {
|
||||
DownloadURL string `json:"downloadUrl" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type exploreTabRequest struct {
|
||||
Tab string `json:"tab" binding:"required"`
|
||||
H5URL string `json:"h5Url" binding:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
@ -12,7 +12,15 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
}
|
||||
|
||||
protected.GET("/admin/app-config/h5-links", middleware.RequirePermission("app-config:view"), h.ListH5Links)
|
||||
protected.POST("/admin/app-config/h5-links", middleware.RequirePermission("app-config:update"), h.CreateH5Link)
|
||||
protected.PUT("/admin/app-config/h5-links", middleware.RequirePermission("app-config:update"), h.UpdateH5Links)
|
||||
protected.PUT("/admin/app-config/h5-links/:key", middleware.RequirePermission("app-config:update"), h.UpdateH5Link)
|
||||
protected.DELETE("/admin/app-config/h5-links/:key", middleware.RequirePermission("app-config:update"), h.DeleteH5Link)
|
||||
|
||||
protected.GET("/admin/app-config/explore-tabs", middleware.RequirePermission("app-config:view"), h.ListExploreTabs)
|
||||
protected.POST("/admin/app-config/explore-tabs", middleware.RequirePermission("app-config:update"), h.CreateExploreTab)
|
||||
protected.PUT("/admin/app-config/explore-tabs/:tab_id", middleware.RequirePermission("app-config:update"), h.UpdateExploreTab)
|
||||
protected.DELETE("/admin/app-config/explore-tabs/:tab_id", middleware.RequirePermission("app-config:update"), h.DeleteExploreTab)
|
||||
|
||||
protected.GET("/admin/app-config/banners", middleware.RequirePermission("app-config:view"), h.ListBanners)
|
||||
protected.POST("/admin/app-config/banners", middleware.RequirePermission("app-config:update"), h.CreateBanner)
|
||||
|
||||
@ -25,23 +25,10 @@ const (
|
||||
bannerStatusExpired = "expired"
|
||||
)
|
||||
|
||||
var h5LinkDefinitions = []H5LinkDefinition{
|
||||
{Key: "host-center", Label: "Host Center"},
|
||||
{Key: "bd-center", Label: "BD Center"},
|
||||
{Key: "bd-leader-center", Label: "BD Leader Center"},
|
||||
{Key: "agency-center", Label: "Agency Center"},
|
||||
{Key: "invite-user", Label: "Invite User"},
|
||||
}
|
||||
|
||||
type AppConfigService struct {
|
||||
store *repository.Store
|
||||
}
|
||||
|
||||
type H5LinkDefinition struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type H5Link struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
@ -82,6 +69,17 @@ type AppVersion struct {
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
type ExploreTab struct {
|
||||
ID uint `json:"id"`
|
||||
AppCode string `json:"appCode"`
|
||||
Tab string `json:"tab"`
|
||||
H5URL string `json:"h5Url"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
func NewService(store *repository.Store) *AppConfigService {
|
||||
return &AppConfigService{store: store}
|
||||
}
|
||||
@ -92,22 +90,9 @@ func (s *AppConfigService) ListH5Links() ([]H5Link, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configByKey := make(map[string]model.AppConfig, len(configs))
|
||||
items := make([]H5Link, 0, len(configs))
|
||||
for _, config := range configs {
|
||||
configByKey[config.Key] = config
|
||||
}
|
||||
|
||||
items := make([]H5Link, 0, len(h5LinkDefinitions))
|
||||
for _, definition := range h5LinkDefinitions {
|
||||
item := H5Link{
|
||||
Key: definition.Key,
|
||||
Label: definition.Label,
|
||||
}
|
||||
if config, ok := configByKey[definition.Key]; ok {
|
||||
item.URL = config.Value
|
||||
item.UpdatedAtMs = config.UpdatedAtMS
|
||||
}
|
||||
items = append(items, item)
|
||||
items = append(items, h5LinkFromModel(config))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@ -117,29 +102,19 @@ func (s *AppConfigService) UpdateH5Links(request updateH5LinksRequest) ([]H5Link
|
||||
return nil, errors.New("items are required")
|
||||
}
|
||||
|
||||
allowed := h5LinkDefinitionSet()
|
||||
items := make([]model.AppConfig, 0, len(request.Items))
|
||||
seen := make(map[string]struct{}, len(request.Items))
|
||||
for _, item := range request.Items {
|
||||
key := strings.TrimSpace(item.Key)
|
||||
if _, ok := allowed[key]; !ok {
|
||||
return nil, fmt.Errorf("invalid h5 link key: %s", key)
|
||||
config, err := h5LinkModelFromPayload(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := config.Key
|
||||
if _, ok := seen[key]; ok {
|
||||
return nil, fmt.Errorf("duplicate h5 link key: %s", key)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
|
||||
link := strings.TrimSpace(item.URL)
|
||||
if err := validateH5URL(link); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, model.AppConfig{
|
||||
Group: h5LinkGroup,
|
||||
Key: key,
|
||||
Value: link,
|
||||
Description: allowed[key],
|
||||
})
|
||||
items = append(items, config)
|
||||
}
|
||||
|
||||
if err := s.store.UpsertAppConfigs(items); err != nil {
|
||||
@ -148,6 +123,39 @@ func (s *AppConfigService) UpdateH5Links(request updateH5LinksRequest) ([]H5Link
|
||||
return s.ListH5Links()
|
||||
}
|
||||
|
||||
func (s *AppConfigService) CreateH5Link(req h5LinkPayload) (H5Link, error) {
|
||||
item, err := h5LinkModelFromPayload(req)
|
||||
if err != nil {
|
||||
return H5Link{}, err
|
||||
}
|
||||
if err := s.store.CreateAppConfig(&item); err != nil {
|
||||
return H5Link{}, err
|
||||
}
|
||||
return h5LinkFromModel(item), nil
|
||||
}
|
||||
|
||||
func (s *AppConfigService) UpdateH5Link(key string, req h5LinkPayload) (H5Link, error) {
|
||||
item, err := s.store.GetAppConfig(h5LinkGroup, key)
|
||||
if err != nil {
|
||||
return H5Link{}, err
|
||||
}
|
||||
req.Key = item.Key
|
||||
updated, err := h5LinkModelFromPayload(req)
|
||||
if err != nil {
|
||||
return H5Link{}, err
|
||||
}
|
||||
item.Value = updated.Value
|
||||
item.Description = updated.Description
|
||||
if err := s.store.UpdateAppConfig(&item); err != nil {
|
||||
return H5Link{}, err
|
||||
}
|
||||
return h5LinkFromModel(item), nil
|
||||
}
|
||||
|
||||
func (s *AppConfigService) DeleteH5Link(key string) error {
|
||||
return s.store.DeleteAppConfig(h5LinkGroup, key)
|
||||
}
|
||||
|
||||
func (s *AppConfigService) ListBanners(appCode string, options repository.AppBannerListOptions) ([]AppBanner, error) {
|
||||
options.AppCode = appctx.Normalize(appCode)
|
||||
options.Status = normalizeBannerStatus(options.Status)
|
||||
@ -265,12 +273,52 @@ func (s *AppConfigService) DeleteAppVersion(appCode string, id uint) error {
|
||||
return s.store.DeleteAppVersion(appctx.Normalize(appCode), id)
|
||||
}
|
||||
|
||||
func h5LinkDefinitionSet() map[string]string {
|
||||
out := make(map[string]string, len(h5LinkDefinitions))
|
||||
for _, definition := range h5LinkDefinitions {
|
||||
out[definition.Key] = definition.Label
|
||||
func (s *AppConfigService) ListExploreTabs(appCode string, options repository.AppExploreTabListOptions) ([]ExploreTab, error) {
|
||||
options.AppCode = appctx.Normalize(appCode)
|
||||
options.Keyword = strings.TrimSpace(options.Keyword)
|
||||
items, err := s.store.ListAppExploreTabs(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out
|
||||
out := make([]ExploreTab, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, exploreTabFromModel(item))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *AppConfigService) CreateExploreTab(appCode string, req exploreTabRequest) (ExploreTab, error) {
|
||||
item, err := exploreTabModelFromRequest(appctx.Normalize(appCode), req)
|
||||
if err != nil {
|
||||
return ExploreTab{}, err
|
||||
}
|
||||
if err := s.store.CreateAppExploreTab(&item); err != nil {
|
||||
return ExploreTab{}, err
|
||||
}
|
||||
return exploreTabFromModel(item), nil
|
||||
}
|
||||
|
||||
func (s *AppConfigService) UpdateExploreTab(appCode string, id uint, req exploreTabRequest) (ExploreTab, error) {
|
||||
item, err := s.store.GetAppExploreTab(appctx.Normalize(appCode), id)
|
||||
if err != nil {
|
||||
return ExploreTab{}, err
|
||||
}
|
||||
updated, err := exploreTabModelFromRequest(item.AppCode, req)
|
||||
if err != nil {
|
||||
return ExploreTab{}, err
|
||||
}
|
||||
item.Tab = updated.Tab
|
||||
item.H5URL = updated.H5URL
|
||||
item.Enabled = updated.Enabled
|
||||
item.SortOrder = updated.SortOrder
|
||||
if err := s.store.UpdateAppExploreTab(&item); err != nil {
|
||||
return ExploreTab{}, err
|
||||
}
|
||||
return exploreTabFromModel(item), nil
|
||||
}
|
||||
|
||||
func (s *AppConfigService) DeleteExploreTab(appCode string, id uint) error {
|
||||
return s.store.DeleteAppExploreTab(appctx.Normalize(appCode), id)
|
||||
}
|
||||
|
||||
func validateH5URL(value string) error {
|
||||
@ -288,6 +336,60 @@ func validateH5URL(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func h5LinkModelFromPayload(req h5LinkPayload) (model.AppConfig, error) {
|
||||
key := strings.TrimSpace(req.Key)
|
||||
label := strings.TrimSpace(req.Label)
|
||||
url := strings.TrimSpace(req.URL)
|
||||
if err := validateH5Key(key); err != nil {
|
||||
return model.AppConfig{}, err
|
||||
}
|
||||
if label == "" || utf8.RuneCountInString(label) > 80 {
|
||||
return model.AppConfig{}, errors.New("h5 config label is invalid")
|
||||
}
|
||||
if url == "" {
|
||||
return model.AppConfig{}, errors.New("h5 link is required")
|
||||
}
|
||||
if err := validateH5URL(url); err != nil {
|
||||
return model.AppConfig{}, err
|
||||
}
|
||||
return model.AppConfig{
|
||||
Group: h5LinkGroup,
|
||||
Key: key,
|
||||
Value: url,
|
||||
Description: label,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateH5Key(value string) error {
|
||||
if value == "" || len(value) > 80 {
|
||||
return errors.New("h5 config key is invalid")
|
||||
}
|
||||
for _, char := range value {
|
||||
switch {
|
||||
case char >= 'a' && char <= 'z':
|
||||
case char >= 'A' && char <= 'Z':
|
||||
case char >= '0' && char <= '9':
|
||||
case char == '-' || char == '_' || char == '.' || char == ':':
|
||||
default:
|
||||
return errors.New("h5 config key is invalid")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func h5LinkFromModel(config model.AppConfig) H5Link {
|
||||
label := strings.TrimSpace(config.Description)
|
||||
if label == "" {
|
||||
label = config.Key
|
||||
}
|
||||
return H5Link{
|
||||
Key: config.Key,
|
||||
Label: label,
|
||||
URL: config.Value,
|
||||
UpdatedAtMs: config.UpdatedAtMS,
|
||||
}
|
||||
}
|
||||
|
||||
func bannerModelFromRequest(appCode string, req bannerRequest) (model.AppBanner, error) {
|
||||
item := model.AppBanner{
|
||||
AppCode: appctx.Normalize(appCode),
|
||||
@ -436,6 +538,39 @@ func appVersionFromModel(item model.AppVersion) AppVersion {
|
||||
}
|
||||
}
|
||||
|
||||
func exploreTabModelFromRequest(appCode string, req exploreTabRequest) (model.AppExploreTab, error) {
|
||||
item := model.AppExploreTab{
|
||||
AppCode: appctx.Normalize(appCode),
|
||||
Tab: strings.TrimSpace(req.Tab),
|
||||
H5URL: strings.TrimSpace(req.H5URL),
|
||||
Enabled: req.Enabled,
|
||||
SortOrder: req.SortOrder,
|
||||
}
|
||||
if item.Tab == "" || utf8.RuneCountInString(item.Tab) > 40 {
|
||||
return model.AppExploreTab{}, errors.New("explore tab is invalid")
|
||||
}
|
||||
if item.H5URL == "" || len(item.H5URL) > 2048 {
|
||||
return model.AppExploreTab{}, errors.New("explore h5 url is invalid")
|
||||
}
|
||||
if err := validateH5URL(item.H5URL); err != nil {
|
||||
return model.AppExploreTab{}, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func exploreTabFromModel(item model.AppExploreTab) ExploreTab {
|
||||
return ExploreTab{
|
||||
ID: item.ID,
|
||||
AppCode: item.AppCode,
|
||||
Tab: item.Tab,
|
||||
H5URL: item.H5URL,
|
||||
Enabled: item.Enabled,
|
||||
SortOrder: item.SortOrder,
|
||||
CreatedAtMs: item.CreatedAtMS,
|
||||
UpdatedAtMs: item.UpdatedAtMS,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeBannerType(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
@ -5,6 +5,31 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestH5LinkModelFromPayloadValidatesDynamicConfig(t *testing.T) {
|
||||
item, err := h5LinkModelFromPayload(h5LinkPayload{
|
||||
Key: "host-center.v2",
|
||||
Label: "Host Center",
|
||||
URL: "https://h5.example.com/host",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("h5 config should be valid: %v", err)
|
||||
}
|
||||
if item.Group != h5LinkGroup || item.Key != "host-center.v2" || item.Description != "Host Center" || item.Value != "https://h5.example.com/host" {
|
||||
t.Fatalf("h5 config model mismatch: %+v", item)
|
||||
}
|
||||
}
|
||||
|
||||
func TestH5LinkModelFromPayloadRejectsInvalidKey(t *testing.T) {
|
||||
_, err := h5LinkModelFromPayload(h5LinkPayload{
|
||||
Key: "host center",
|
||||
Label: "Host Center",
|
||||
URL: "https://h5.example.com/host",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid key to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBannerModelFromRequestRequiresRoomSmallImageForRoomScope(t *testing.T) {
|
||||
_, err := bannerModelFromRequest("lalu", bannerRequest{
|
||||
CoverURL: "https://cdn.example.com/banner.png",
|
||||
@ -65,3 +90,29 @@ func TestBannerModelFromRequestExpiresEndedActiveBanner(t *testing.T) {
|
||||
t.Fatalf("non-room banner must not retain room small image: %+v", item)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExploreTabModelFromRequestValidatesRequiredFields(t *testing.T) {
|
||||
item, err := exploreTabModelFromRequest("lalu", exploreTabRequest{
|
||||
Enabled: true,
|
||||
H5URL: "https://h5.example.com/explore/live",
|
||||
SortOrder: 3,
|
||||
Tab: "Live",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("explore tab should be valid: %v", err)
|
||||
}
|
||||
if item.AppCode != "lalu" || item.Tab != "Live" || item.H5URL != "https://h5.example.com/explore/live" || !item.Enabled || item.SortOrder != 3 {
|
||||
t.Fatalf("explore tab model mismatch: %+v", item)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExploreTabModelFromRequestRejectsWhitespaceURL(t *testing.T) {
|
||||
_, err := exploreTabModelFromRequest("lalu", exploreTabRequest{
|
||||
Enabled: true,
|
||||
H5URL: "https://h5.example.com/explore live",
|
||||
Tab: "Live",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected whitespace h5 url to fail")
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,6 +103,22 @@ type drawDTO struct {
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
}
|
||||
|
||||
type drawSummaryDTO struct {
|
||||
PoolID string `json:"pool_id"`
|
||||
TotalDraws int64 `json:"total_draws"`
|
||||
UniqueUsers int64 `json:"unique_users"`
|
||||
UniqueRooms int64 `json:"unique_rooms"`
|
||||
TotalSpentCoins int64 `json:"total_spent_coins"`
|
||||
TotalRewardCoins int64 `json:"total_reward_coins"`
|
||||
BaseRewardCoins int64 `json:"base_reward_coins"`
|
||||
RoomAtmosphereRewardCoins int64 `json:"room_atmosphere_reward_coins"`
|
||||
ActivitySubsidyCoins int64 `json:"activity_subsidy_coins"`
|
||||
ActualRTPPPM int64 `json:"actual_rtp_ppm"`
|
||||
PendingDraws int64 `json:"pending_draws"`
|
||||
GrantedDraws int64 `json:"granted_draws"`
|
||||
FailedDraws int64 `json:"failed_draws"`
|
||||
}
|
||||
|
||||
func (h *Handler) GetLuckyGiftConfig(c *gin.Context) {
|
||||
resp, err := h.activity.GetLuckyGiftConfig(c.Request.Context(), &activityv1.GetLuckyGiftConfigRequest{
|
||||
Meta: h.meta(c),
|
||||
@ -176,6 +192,22 @@ func (h *Handler) ListLuckyGiftDraws(c *gin.Context) {
|
||||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||||
}
|
||||
|
||||
func (h *Handler) GetLuckyGiftDrawSummary(c *gin.Context) {
|
||||
resp, err := h.activity.GetLuckyGiftDrawSummary(c.Request.Context(), &activityv1.GetLuckyGiftDrawSummaryRequest{
|
||||
Meta: h.meta(c),
|
||||
PoolId: strings.TrimSpace(c.Query("pool_id")),
|
||||
GiftId: strings.TrimSpace(c.Query("gift_id")),
|
||||
UserId: parseOptionalInt64(c.Query("user_id")),
|
||||
RoomId: strings.TrimSpace(c.Query("room_id")),
|
||||
Status: strings.TrimSpace(c.Query("status")),
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取幸运礼物抽奖汇总失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, drawSummaryFromProto(resp.GetSummary()))
|
||||
}
|
||||
|
||||
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
|
||||
return &activityv1.RequestMeta{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
@ -330,6 +362,27 @@ func drawFromProto(draw *activityv1.LuckyGiftDrawResult) drawDTO {
|
||||
}
|
||||
}
|
||||
|
||||
func drawSummaryFromProto(summary *activityv1.LuckyGiftDrawSummary) drawSummaryDTO {
|
||||
if summary == nil {
|
||||
return drawSummaryDTO{}
|
||||
}
|
||||
return drawSummaryDTO{
|
||||
PoolID: summary.GetPoolId(),
|
||||
TotalDraws: summary.GetTotalDraws(),
|
||||
UniqueUsers: summary.GetUniqueUsers(),
|
||||
UniqueRooms: summary.GetUniqueRooms(),
|
||||
TotalSpentCoins: summary.GetTotalSpentCoins(),
|
||||
TotalRewardCoins: summary.GetTotalRewardCoins(),
|
||||
BaseRewardCoins: summary.GetBaseRewardCoins(),
|
||||
RoomAtmosphereRewardCoins: summary.GetRoomAtmosphereRewardCoins(),
|
||||
ActivitySubsidyCoins: summary.GetActivitySubsidyCoins(),
|
||||
ActualRTPPPM: summary.GetActualRtpPpm(),
|
||||
PendingDraws: summary.GetPendingDraws(),
|
||||
GrantedDraws: summary.GetGrantedDraws(),
|
||||
FailedDraws: summary.GetFailedDraws(),
|
||||
}
|
||||
}
|
||||
|
||||
func parseOptionalInt64(value string) int64 {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
|
||||
@ -15,4 +15,5 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
protected.PUT("/admin/activity/lucky-gifts/config", middleware.RequirePermission("lucky-gift:update"), h.UpsertLuckyGiftConfig)
|
||||
protected.GET("/admin/activity/lucky-gifts/configs", middleware.RequirePermission("lucky-gift:view"), h.ListLuckyGiftConfigs)
|
||||
protected.GET("/admin/activity/lucky-gifts/draws", middleware.RequirePermission("lucky-gift:view"), h.ListLuckyGiftDraws)
|
||||
protected.GET("/admin/activity/lucky-gifts/draw-summary", middleware.RequirePermission("lucky-gift:view"), h.GetLuckyGiftDrawSummary)
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ type resourceDTO struct {
|
||||
PriceType string `json:"priceType"`
|
||||
CoinPrice int64 `json:"coinPrice"`
|
||||
GiftPointAmount int64 `json:"giftPointAmount"`
|
||||
BadgeForm string `json:"badgeForm,omitempty"`
|
||||
UsageScopes []string `json:"usageScopes"`
|
||||
AssetURL string `json:"assetUrl"`
|
||||
PreviewURL string `json:"previewUrl"`
|
||||
@ -178,6 +179,7 @@ func resourceFromProto(item *walletv1.Resource) resourceDTO {
|
||||
PriceType: item.GetPriceType(),
|
||||
CoinPrice: item.GetCoinPrice(),
|
||||
GiftPointAmount: item.GetGiftPointAmount(),
|
||||
BadgeForm: badgeFormForResource(item.GetResourceType(), item.GetMetadataJson()),
|
||||
UsageScopes: item.GetUsageScopes(),
|
||||
AssetURL: item.GetAssetUrl(),
|
||||
PreviewURL: item.GetPreviewUrl(),
|
||||
|
||||
@ -78,6 +78,10 @@ func (h *Handler) CreateResource(c *gin.Context) {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := validateResourceBadgeForm(req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := h.wallet.CreateResource(c.Request.Context(), req.createProto(c))
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
@ -178,6 +182,10 @@ func (h *Handler) UpdateResource(c *gin.Context) {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := validateResourceBadgeForm(req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
protoReq := req.updateProto(c, resourceID)
|
||||
resp, err := h.wallet.UpdateResource(c.Request.Context(), protoReq)
|
||||
if err != nil {
|
||||
|
||||
@ -70,3 +70,29 @@ func TestEmojiPackMetadataKeepsLimitedRegions(t *testing.T) {
|
||||
t.Fatalf("unexpected region ids: %+v", payload.RegionIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadgeResourceMetadataUsesSelectedForm(t *testing.T) {
|
||||
metadata := resourceMetadataJSON(resourceTypeBadge, "long")
|
||||
payload := badgeMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(metadata), &payload); err != nil {
|
||||
t.Fatalf("badge metadata json mismatch: %v", err)
|
||||
}
|
||||
if payload.BadgeForm != badgeFormStrip || payload.DefaultSlot != "profile_strip" {
|
||||
t.Fatalf("long badge metadata mismatch: %+v", payload)
|
||||
}
|
||||
if got := badgeFormFromMetadata(resourceMetadataJSON(resourceTypeBadge, "short")); got != badgeFormTile {
|
||||
t.Fatalf("short badge form mismatch: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBadgeResourceRequiresBadgeForm(t *testing.T) {
|
||||
if err := validateResourceBadgeForm(resourceRequest{ResourceType: resourceTypeBadge}); err == nil {
|
||||
t.Fatalf("badge form should be required")
|
||||
}
|
||||
if err := validateResourceBadgeForm(resourceRequest{ResourceType: resourceTypeBadge, BadgeForm: badgeFormTile}); err != nil {
|
||||
t.Fatalf("valid badge form rejected: %v", err)
|
||||
}
|
||||
if err := validateResourceBadgeForm(resourceRequest{ResourceType: "gift", BadgeForm: ""}); err != nil {
|
||||
t.Fatalf("non-badge resource should not require badge form: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ const dayMillis int64 = 24 * 60 * 60 * 1000
|
||||
|
||||
const (
|
||||
resourceTypeCoin = "coin"
|
||||
resourceTypeBadge = "badge"
|
||||
resourceTypeEmojiPack = "emoji_pack"
|
||||
)
|
||||
|
||||
@ -26,6 +27,11 @@ const (
|
||||
resourcePriceTypeFree = "free"
|
||||
)
|
||||
|
||||
const (
|
||||
badgeFormStrip = "strip"
|
||||
badgeFormTile = "tile"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultEmojiPackCategory = "默认"
|
||||
emojiPackPricingFree = "free"
|
||||
@ -41,6 +47,7 @@ type resourceRequest struct {
|
||||
PriceType string `json:"priceType"`
|
||||
CoinPrice int64 `json:"coinPrice"`
|
||||
GiftPointAmount int64 `json:"giftPointAmount"`
|
||||
BadgeForm string `json:"badgeForm"`
|
||||
ManagerGrantEnabled *bool `json:"managerGrantEnabled"`
|
||||
AssetURL string `json:"assetUrl"`
|
||||
PreviewURL string `json:"previewUrl"`
|
||||
@ -65,6 +72,11 @@ type emojiPackMetadataPayload struct {
|
||||
RegionScope string `json:"region_scope"`
|
||||
}
|
||||
|
||||
type badgeMetadataPayload struct {
|
||||
BadgeForm string `json:"badge_form"`
|
||||
DefaultSlot string `json:"default_slot,omitempty"`
|
||||
}
|
||||
|
||||
type resourceGroupRequest struct {
|
||||
GroupCode string `json:"groupCode"`
|
||||
Name string `json:"name"`
|
||||
@ -130,6 +142,7 @@ func (r resourceRequest) createProto(c *gin.Context) *walletv1.CreateResourceReq
|
||||
resourceType := normalizeResourceType(r.ResourceType)
|
||||
walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount)
|
||||
priceType, coinPrice, giftPointAmount := resourcePricing(r.PriceType, r.CoinPrice)
|
||||
metadataJSON := resourceMetadataJSON(resourceType, r.BadgeForm)
|
||||
return &walletv1.CreateResourceRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
@ -149,7 +162,7 @@ func (r resourceRequest) createProto(c *gin.Context) *walletv1.CreateResourceReq
|
||||
AssetUrl: strings.TrimSpace(r.AssetURL),
|
||||
PreviewUrl: strings.TrimSpace(r.PreviewURL),
|
||||
AnimationUrl: strings.TrimSpace(r.AnimationURL),
|
||||
MetadataJson: "{}",
|
||||
MetadataJson: metadataJSON,
|
||||
SortOrder: r.SortOrder,
|
||||
OperatorUserId: actorID(c),
|
||||
}
|
||||
@ -159,6 +172,7 @@ func (r resourceRequest) updateProto(c *gin.Context, resourceID int64) *walletv1
|
||||
resourceType := normalizeResourceType(r.ResourceType)
|
||||
walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount)
|
||||
priceType, coinPrice, giftPointAmount := resourcePricing(r.PriceType, r.CoinPrice)
|
||||
metadataJSON := resourceMetadataJSON(resourceType, r.BadgeForm)
|
||||
return &walletv1.UpdateResourceRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
@ -179,7 +193,7 @@ func (r resourceRequest) updateProto(c *gin.Context, resourceID int64) *walletv1
|
||||
AssetUrl: strings.TrimSpace(r.AssetURL),
|
||||
PreviewUrl: strings.TrimSpace(r.PreviewURL),
|
||||
AnimationUrl: strings.TrimSpace(r.AnimationURL),
|
||||
MetadataJson: "{}",
|
||||
MetadataJson: metadataJSON,
|
||||
SortOrder: r.SortOrder,
|
||||
OperatorUserId: actorID(c),
|
||||
}
|
||||
@ -379,6 +393,16 @@ func validateResourcePricing(req resourceRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateResourceBadgeForm(req resourceRequest) error {
|
||||
if normalizeResourceType(req.ResourceType) != resourceTypeBadge {
|
||||
return nil
|
||||
}
|
||||
if normalizeBadgeForm(req.BadgeForm) == "" {
|
||||
return fmt.Errorf("请选择徽章属性")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceGrantStrategy(resourceType string) string {
|
||||
switch resourceType {
|
||||
case resourceTypeCoin:
|
||||
@ -392,6 +416,52 @@ func resourceGrantStrategy(resourceType string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func resourceMetadataJSON(resourceType string, badgeForm string) string {
|
||||
if resourceType != resourceTypeBadge {
|
||||
return "{}"
|
||||
}
|
||||
payload := badgeMetadataPayload{BadgeForm: normalizeBadgeForm(badgeForm)}
|
||||
switch payload.BadgeForm {
|
||||
case badgeFormStrip:
|
||||
payload.DefaultSlot = "profile_strip"
|
||||
case badgeFormTile:
|
||||
payload.DefaultSlot = "honor_wall"
|
||||
default:
|
||||
return "{}"
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func normalizeBadgeForm(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case badgeFormStrip, "long":
|
||||
return badgeFormStrip
|
||||
case badgeFormTile, "short":
|
||||
return badgeFormTile
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func badgeFormFromMetadata(metadataJSON string) string {
|
||||
payload := badgeMetadataPayload{}
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
|
||||
return ""
|
||||
}
|
||||
return normalizeBadgeForm(payload.BadgeForm)
|
||||
}
|
||||
|
||||
func badgeFormForResource(resourceType string, metadataJSON string) string {
|
||||
if normalizeResourceType(resourceType) != resourceTypeBadge {
|
||||
return ""
|
||||
}
|
||||
return badgeFormFromMetadata(metadataJSON)
|
||||
}
|
||||
|
||||
func emojiPackMetadataJSON(regionIDs []int64, category string, pricingType string) (string, error) {
|
||||
normalizedRegionIDs := normalizeRegionIDs(regionIDs)
|
||||
metadata := emojiPackMetadataPayload{
|
||||
|
||||
@ -15,12 +15,36 @@ type AppVersionListOptions struct {
|
||||
Keyword string
|
||||
}
|
||||
|
||||
type AppExploreTabListOptions struct {
|
||||
AppCode string
|
||||
Keyword string
|
||||
Enabled *bool
|
||||
}
|
||||
|
||||
func (s *Store) ListAppConfigs(group string) ([]model.AppConfig, error) {
|
||||
var items []model.AppConfig
|
||||
err := s.db.Where("`group` = ?", strings.TrimSpace(group)).Order("`key` ASC").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (s *Store) GetAppConfig(group string, key string) (model.AppConfig, error) {
|
||||
var item model.AppConfig
|
||||
err := s.db.Where("`group` = ? AND `key` = ?", strings.TrimSpace(group), strings.TrimSpace(key)).First(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *Store) CreateAppConfig(item *model.AppConfig) error {
|
||||
return s.db.Create(item).Error
|
||||
}
|
||||
|
||||
func (s *Store) UpdateAppConfig(item *model.AppConfig) error {
|
||||
return s.db.Save(item).Error
|
||||
}
|
||||
|
||||
func (s *Store) DeleteAppConfig(group string, key string) error {
|
||||
return s.db.Where("`group` = ? AND `key` = ?", strings.TrimSpace(group), strings.TrimSpace(key)).Delete(&model.AppConfig{}).Error
|
||||
}
|
||||
|
||||
func (s *Store) UpsertAppConfigs(items []model.AppConfig) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
@ -77,3 +101,36 @@ func (s *Store) UpdateAppVersion(item *model.AppVersion) error {
|
||||
func (s *Store) DeleteAppVersion(appCode string, id uint) error {
|
||||
return s.db.Where("app_code = ? AND id = ?", strings.TrimSpace(appCode), id).Delete(&model.AppVersion{}).Error
|
||||
}
|
||||
|
||||
func (s *Store) ListAppExploreTabs(options AppExploreTabListOptions) ([]model.AppExploreTab, error) {
|
||||
db := s.db.Where("app_code = ?", strings.TrimSpace(options.AppCode))
|
||||
if options.Enabled != nil {
|
||||
db = db.Where("enabled = ?", *options.Enabled)
|
||||
}
|
||||
if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
db = db.Where("tab LIKE ? OR h5_url LIKE ?", like, like)
|
||||
}
|
||||
|
||||
var items []model.AppExploreTab
|
||||
err := db.Order("sort_order ASC, id DESC").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (s *Store) GetAppExploreTab(appCode string, id uint) (model.AppExploreTab, error) {
|
||||
var item model.AppExploreTab
|
||||
err := s.db.Where("app_code = ? AND id = ?", strings.TrimSpace(appCode), id).First(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *Store) CreateAppExploreTab(item *model.AppExploreTab) error {
|
||||
return s.db.Create(item).Error
|
||||
}
|
||||
|
||||
func (s *Store) UpdateAppExploreTab(item *model.AppExploreTab) error {
|
||||
return s.db.Save(item).Error
|
||||
}
|
||||
|
||||
func (s *Store) DeleteAppExploreTab(appCode string, id uint) error {
|
||||
return s.db.Where("app_code = ? AND id = ?", strings.TrimSpace(appCode), id).Delete(&model.AppExploreTab{}).Error
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ func (s *Store) AutoMigrate() error {
|
||||
&model.AppConfig{},
|
||||
&model.AppBanner{},
|
||||
&model.AppVersion{},
|
||||
&model.AppExploreTab{},
|
||||
&model.RefreshToken{},
|
||||
&model.LoginLog{},
|
||||
&model.OperationLog{},
|
||||
|
||||
@ -224,8 +224,9 @@ func (s *Store) seedMenus() error {
|
||||
{ParentID: &roomsID, Title: "房间配置", Code: "room-config", Path: "/rooms/config", Icon: "settings", PermissionCode: "room-config:view", Sort: 67, Visible: true},
|
||||
{ParentID: &appConfigID, Title: "H5配置", Code: "app-config-h5", Path: "/app-config/h5", Icon: "settings", PermissionCode: "app-config:view", Sort: 66, Visible: true},
|
||||
{ParentID: &appConfigID, Title: "BANNER配置", Code: "app-config-banners", Path: "/app-config/banners", Icon: "image", PermissionCode: "app-config:view", Sort: 67, Visible: true},
|
||||
{ParentID: &appConfigID, Title: "内购配置", Code: "payment-recharge-products", Path: "/app-config/recharge-products", Icon: "wallet", PermissionCode: "payment-product:view", Sort: 68, Visible: true},
|
||||
{ParentID: &appConfigID, Title: "版本管理", Code: "app-config-versions", Path: "/app-config/versions", Icon: "settings", PermissionCode: "app-version:view", Sort: 69, Visible: true},
|
||||
{ParentID: &appConfigID, Title: "Explore配置", Code: "app-config-explore", Path: "/app-config/explore", Icon: "explore", PermissionCode: "app-config:view", Sort: 68, Visible: true},
|
||||
{ParentID: &appConfigID, Title: "内购配置", Code: "payment-recharge-products", Path: "/app-config/recharge-products", Icon: "wallet", PermissionCode: "payment-product:view", Sort: 69, Visible: true},
|
||||
{ParentID: &appConfigID, Title: "版本管理", Code: "app-config-versions", Path: "/app-config/versions", Icon: "settings", PermissionCode: "app-version:view", Sort: 70, Visible: true},
|
||||
{ParentID: &resourceID, Title: "资源列表", Code: "resource-list", Path: "/resources", Icon: "inventory", PermissionCode: "resource:view", Sort: 67, Visible: true},
|
||||
{ParentID: &resourceID, Title: "资源组列表", Code: "resource-group-list", Path: "/resource-groups", Icon: "category", PermissionCode: "resource-group:view", Sort: 68, Visible: true},
|
||||
{ParentID: &resourceID, Title: "礼物列表", Code: "gift-list", Path: "/gifts", Icon: "gift", PermissionCode: "gift:view", Sort: 69, Visible: true},
|
||||
|
||||
39
server/admin/migrations/022_app_explore_config.sql
Normal file
39
server/admin/migrations/022_app_explore_config.sql
Normal file
@ -0,0 +1,39 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- Explore tab 是 App 运行时配置,后台保存全量配置,gateway 只下发启用项。
|
||||
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_app_explore_tabs (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键 ID',
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
tab VARCHAR(80) NOT NULL COMMENT 'Explore tab 展示文案',
|
||||
h5_url VARCHAR(2048) NOT NULL COMMENT 'Explore tab H5 链接',
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否启用',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
UNIQUE KEY uk_admin_app_explore_tabs_tab (app_code, tab),
|
||||
INDEX idx_admin_app_explore_tabs_app_sort (app_code, enabled, sort_order, id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台 App Explore tab 配置表';
|
||||
|
||||
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
|
||||
SELECT parent.id, 'Explore配置', 'app-config-explore', '/app-config/explore', 'explore', 'app-config:view', 68, TRUE, @now_ms, @now_ms
|
||||
FROM admin_menus parent
|
||||
WHERE parent.code = 'app-config'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
parent_id = VALUES(parent_id),
|
||||
title = VALUES(title),
|
||||
path = VALUES(path),
|
||||
icon = VALUES(icon),
|
||||
permission_code = VALUES(permission_code),
|
||||
sort = VALUES(sort),
|
||||
visible = VALUES(visible),
|
||||
updated_at_ms = @now_ms;
|
||||
|
||||
UPDATE admin_menus
|
||||
SET sort = 69, updated_at_ms = @now_ms
|
||||
WHERE code = 'payment-recharge-products' AND sort < 69;
|
||||
|
||||
UPDATE admin_menus
|
||||
SET sort = 70, updated_at_ms = @now_ms
|
||||
WHERE code = 'app-config-versions' AND sort < 70;
|
||||
@ -12,7 +12,7 @@ health_http_addr: ":13106"
|
||||
mysql_dsn: "hyapp:hyapp@tcp(mysql:3306)/hyapp_activity?parseTime=true&charset=utf8mb4&loc=UTC"
|
||||
user_service_addr: "user-service:13005"
|
||||
wallet_service_addr: "wallet-service:13004"
|
||||
mysql_auto_migrate: false
|
||||
mysql_auto_migrate: true
|
||||
first_recharge_reward_worker:
|
||||
enabled: false
|
||||
red_packet_broadcast_worker:
|
||||
|
||||
@ -12,7 +12,7 @@ health_http_addr: ":13106"
|
||||
mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_activity?parseTime=true&charset=utf8mb4&loc=UTC"
|
||||
user_service_addr: "127.0.0.1:13005"
|
||||
wallet_service_addr: "127.0.0.1:13004"
|
||||
mysql_auto_migrate: false
|
||||
mysql_auto_migrate: true
|
||||
first_recharge_reward_worker:
|
||||
enabled: false
|
||||
red_packet_broadcast_worker:
|
||||
|
||||
@ -71,6 +71,12 @@ func New(cfg config.Config) (*App, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.MySQLAutoMigrate {
|
||||
if err := repository.Migrate(startupCtx); err != nil {
|
||||
_ = repository.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", cfg.GRPCAddr)
|
||||
if err != nil {
|
||||
|
||||
@ -127,6 +127,22 @@ type DrawResult struct {
|
||||
CreatedAtMS int64
|
||||
}
|
||||
|
||||
type DrawSummary struct {
|
||||
PoolID string
|
||||
TotalDraws int64
|
||||
UniqueUsers int64
|
||||
UniqueRooms int64
|
||||
TotalSpentCoins int64
|
||||
TotalRewardCoins int64
|
||||
BaseRewardCoins int64
|
||||
RoomAtmosphereRewardCoins int64
|
||||
ActivitySubsidyCoins int64
|
||||
ActualRTPPPM int64
|
||||
PendingDraws int64
|
||||
GrantedDraws int64
|
||||
FailedDraws int64
|
||||
}
|
||||
|
||||
type AdminConfigQuery struct {
|
||||
PoolID string
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ type Repository interface {
|
||||
CheckLuckyGift(ctx context.Context, cmd domain.CheckCommand) (domain.CheckResult, error)
|
||||
ExecuteLuckyGiftDraw(ctx context.Context, cmd domain.DrawCommand, nowMS int64) (domain.DrawResult, error)
|
||||
ListLuckyGiftDraws(ctx context.Context, query domain.DrawQuery) ([]domain.DrawResult, int64, error)
|
||||
GetLuckyGiftDrawSummary(ctx context.Context, query domain.DrawQuery) (domain.DrawSummary, error)
|
||||
}
|
||||
|
||||
// Service 承载幸运礼物查询、抽奖命令和后台规则配置。
|
||||
@ -108,10 +109,7 @@ func (s *Service) ListDraws(ctx context.Context, query domain.DrawQuery) ([]doma
|
||||
if err := s.requireRepository(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
query.PoolID = normalizePoolID(query.PoolID)
|
||||
query.GiftID = strings.TrimSpace(query.GiftID)
|
||||
query.RoomID = strings.TrimSpace(query.RoomID)
|
||||
query.Status = strings.TrimSpace(query.Status)
|
||||
query = normalizeDrawQuery(query)
|
||||
if query.Page <= 0 {
|
||||
query.Page = 1
|
||||
}
|
||||
@ -124,9 +122,24 @@ func (s *Service) ListDraws(ctx context.Context, query domain.DrawQuery) ([]doma
|
||||
return s.repository.ListLuckyGiftDraws(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) GetDrawSummary(ctx context.Context, query domain.DrawQuery) (domain.DrawSummary, error) {
|
||||
if err := s.requireRepository(); err != nil {
|
||||
return domain.DrawSummary{}, err
|
||||
}
|
||||
return s.repository.GetLuckyGiftDrawSummary(ctx, normalizeDrawQuery(query))
|
||||
}
|
||||
|
||||
func (s *Service) requireRepository() error {
|
||||
if s == nil || s.repository == nil {
|
||||
return xerr.New(xerr.Unavailable, "lucky gift repository is not configured")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeDrawQuery(query domain.DrawQuery) domain.DrawQuery {
|
||||
query.PoolID = normalizePoolID(query.PoolID)
|
||||
query.GiftID = strings.TrimSpace(query.GiftID)
|
||||
query.RoomID = strings.TrimSpace(query.RoomID)
|
||||
query.Status = strings.TrimSpace(query.Status)
|
||||
return query
|
||||
}
|
||||
|
||||
@ -68,3 +68,7 @@ func (r *fakeLuckyGiftRepository) ExecuteLuckyGiftDraw(context.Context, domain.D
|
||||
func (r *fakeLuckyGiftRepository) ListLuckyGiftDraws(context.Context, domain.DrawQuery) ([]domain.DrawResult, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (r *fakeLuckyGiftRepository) GetLuckyGiftDrawSummary(context.Context, domain.DrawQuery) (domain.DrawSummary, error) {
|
||||
return domain.DrawSummary{}, nil
|
||||
}
|
||||
|
||||
@ -594,30 +594,7 @@ func (r *Repository) ListLuckyGiftDraws(ctx context.Context, query domain.DrawQu
|
||||
if r == nil || r.db == nil {
|
||||
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
appCode := appcode.FromContext(ctx)
|
||||
where := []string{"app_code = ?"}
|
||||
args := []any{appCode}
|
||||
if query.PoolID != "" {
|
||||
where = append(where, "pool_id = ?")
|
||||
args = append(args, query.PoolID)
|
||||
}
|
||||
if query.GiftID != "" {
|
||||
where = append(where, "gift_id = ?")
|
||||
args = append(args, query.GiftID)
|
||||
}
|
||||
if query.UserID > 0 {
|
||||
where = append(where, "user_id = ?")
|
||||
args = append(args, query.UserID)
|
||||
}
|
||||
if query.RoomID != "" {
|
||||
where = append(where, "room_id = ?")
|
||||
args = append(args, query.RoomID)
|
||||
}
|
||||
if query.Status != "" {
|
||||
where = append(where, "reward_status = ?")
|
||||
args = append(args, query.Status)
|
||||
}
|
||||
whereSQL := strings.Join(where, " AND ")
|
||||
whereSQL, args := luckyDrawWhereClause(appcode.FromContext(ctx), query)
|
||||
var total int64
|
||||
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM lucky_draw_records WHERE `+whereSQL, args...).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
@ -652,6 +629,74 @@ func (r *Repository) ListLuckyGiftDraws(ctx context.Context, query domain.DrawQu
|
||||
return items, total, rows.Err()
|
||||
}
|
||||
|
||||
func (r *Repository) GetLuckyGiftDrawSummary(ctx context.Context, query domain.DrawQuery) (domain.DrawSummary, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return domain.DrawSummary{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
whereSQL, args := luckyDrawWhereClause(appcode.FromContext(ctx), query)
|
||||
var summary domain.DrawSummary
|
||||
summary.PoolID = query.PoolID
|
||||
if err := r.db.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
COUNT(*),
|
||||
COUNT(DISTINCT user_id),
|
||||
COUNT(DISTINCT room_id),
|
||||
COALESCE(SUM(coin_spent), 0),
|
||||
COALESCE(SUM(effective_reward_coins), 0),
|
||||
COALESCE(SUM(base_reward_coins), 0),
|
||||
COALESCE(SUM(room_atmosphere_reward_coins), 0),
|
||||
COALESCE(SUM(activity_subsidy_coins), 0),
|
||||
COALESCE(SUM(CASE WHEN reward_status = 'pending' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN reward_status = 'granted' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN reward_status = 'failed' THEN 1 ELSE 0 END), 0)
|
||||
FROM lucky_draw_records
|
||||
WHERE `+whereSQL, args...).Scan(
|
||||
&summary.TotalDraws,
|
||||
&summary.UniqueUsers,
|
||||
&summary.UniqueRooms,
|
||||
&summary.TotalSpentCoins,
|
||||
&summary.TotalRewardCoins,
|
||||
&summary.BaseRewardCoins,
|
||||
&summary.RoomAtmosphereRewardCoins,
|
||||
&summary.ActivitySubsidyCoins,
|
||||
&summary.PendingDraws,
|
||||
&summary.GrantedDraws,
|
||||
&summary.FailedDraws,
|
||||
); err != nil {
|
||||
return domain.DrawSummary{}, err
|
||||
}
|
||||
if summary.TotalSpentCoins > 0 {
|
||||
summary.ActualRTPPPM = summary.TotalRewardCoins * luckyPPMScale / summary.TotalSpentCoins
|
||||
}
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func luckyDrawWhereClause(appCode string, query domain.DrawQuery) (string, []any) {
|
||||
where := []string{"app_code = ?"}
|
||||
args := []any{appCode}
|
||||
if query.PoolID != "" {
|
||||
where = append(where, "pool_id = ?")
|
||||
args = append(args, query.PoolID)
|
||||
}
|
||||
if query.GiftID != "" {
|
||||
where = append(where, "gift_id = ?")
|
||||
args = append(args, query.GiftID)
|
||||
}
|
||||
if query.UserID > 0 {
|
||||
where = append(where, "user_id = ?")
|
||||
args = append(args, query.UserID)
|
||||
}
|
||||
if query.RoomID != "" {
|
||||
where = append(where, "room_id = ?")
|
||||
args = append(args, query.RoomID)
|
||||
}
|
||||
if query.Status != "" {
|
||||
where = append(where, "reward_status = ?")
|
||||
args = append(args, query.Status)
|
||||
}
|
||||
return strings.Join(where, " AND "), args
|
||||
}
|
||||
|
||||
type luckyQueryer interface {
|
||||
QueryRowContext(context.Context, string, ...any) *sql.Row
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
@ -36,6 +37,17 @@ func Open(ctx context.Context, dsn string) (*Repository, error) {
|
||||
return &Repository{db: db}, nil
|
||||
}
|
||||
|
||||
// Migrate performs narrow, idempotent local schema fixes for activity-service.
|
||||
func (r *Repository) Migrate(ctx context.Context) error {
|
||||
if r == nil || r.db == nil {
|
||||
return xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||
}
|
||||
if err := r.ensureLuckyDrawPoolID(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 释放 MySQL 连接池。
|
||||
func (r *Repository) Close() error {
|
||||
if r == nil || r.db == nil {
|
||||
@ -54,6 +66,79 @@ func (r *Repository) Ping(ctx context.Context) error {
|
||||
return r.db.PingContext(ctx)
|
||||
}
|
||||
|
||||
func (r *Repository) ensureLuckyDrawPoolID(ctx context.Context) error {
|
||||
const table = "lucky_draw_records"
|
||||
hasPoolID, err := r.columnExists(ctx, table, "pool_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasPoolID {
|
||||
if _, err := r.db.ExecContext(ctx, `
|
||||
ALTER TABLE lucky_draw_records
|
||||
ADD COLUMN pool_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '幸运礼物奖池 ID' AFTER anchor_id`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := r.db.ExecContext(ctx, `UPDATE lucky_draw_records SET pool_id = gift_id WHERE pool_id = ''`); err != nil {
|
||||
return err
|
||||
}
|
||||
hasPoolIndex, err := r.indexExists(ctx, table, "idx_lucky_draw_pool")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasPoolIndex {
|
||||
if _, err := r.db.ExecContext(ctx, `CREATE INDEX idx_lucky_draw_pool ON lucky_draw_records (app_code, pool_id, created_at_ms)`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) columnExists(ctx context.Context, table string, column string) (bool, error) {
|
||||
database, err := r.currentDatabase(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var count int
|
||||
if err := r.db.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
|
||||
database, table, column,
|
||||
).Scan(&count); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *Repository) indexExists(ctx context.Context, table string, index string) (bool, error) {
|
||||
database, err := r.currentDatabase(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var count int
|
||||
if err := r.db.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME = ?`,
|
||||
database, table, index,
|
||||
).Scan(&count); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *Repository) currentDatabase(ctx context.Context) (string, error) {
|
||||
var database sql.NullString
|
||||
if err := r.db.QueryRowContext(ctx, `SELECT DATABASE()`).Scan(&database); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !database.Valid || strings.TrimSpace(database.String) == "" {
|
||||
return "", fmt.Errorf("mysql database is not selected")
|
||||
}
|
||||
return database.String, nil
|
||||
}
|
||||
|
||||
// GetActivity 从 MySQL 活动配置表读取同步查询投影。
|
||||
func (r *Repository) GetActivity(ctx context.Context, activityID string) (activitydomain.Activity, error) {
|
||||
if r == nil || r.db == nil {
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"hyapp/internal/testutil/mysqlschema"
|
||||
)
|
||||
|
||||
func TestMigrateAddsLuckyDrawPoolIDToLegacyTable(t *testing.T) {
|
||||
schema := mysqlschema.New(t, mysqlschema.Config{
|
||||
EnvVar: "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
|
||||
InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
|
||||
DatabasePrefix: "hyapp_activity_migrate_test",
|
||||
})
|
||||
if _, err := schema.DB.ExecContext(context.Background(), `ALTER TABLE lucky_draw_records DROP INDEX idx_lucky_draw_pool`); err != nil {
|
||||
t.Fatalf("drop legacy pool index: %v", err)
|
||||
}
|
||||
if _, err := schema.DB.ExecContext(context.Background(), `ALTER TABLE lucky_draw_records DROP COLUMN pool_id`); err != nil {
|
||||
t.Fatalf("drop legacy pool column: %v", err)
|
||||
}
|
||||
|
||||
repository, err := Open(context.Background(), schema.DSN)
|
||||
if err != nil {
|
||||
t.Fatalf("open repository: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repository.Close() })
|
||||
if err := repository.Migrate(context.Background()); err != nil {
|
||||
t.Fatalf("Migrate failed: %v", err)
|
||||
}
|
||||
if hasColumn, err := repository.columnExists(context.Background(), "lucky_draw_records", "pool_id"); err != nil || !hasColumn {
|
||||
t.Fatalf("pool_id column missing after migrate: has=%v err=%v", hasColumn, err)
|
||||
}
|
||||
if hasIndex, err := repository.indexExists(context.Background(), "lucky_draw_records", "idx_lucky_draw_pool"); err != nil || !hasIndex {
|
||||
t.Fatalf("idx_lucky_draw_pool missing after migrate: has=%v err=%v", hasIndex, err)
|
||||
}
|
||||
}
|
||||
@ -129,6 +129,21 @@ func (s *AdminLuckyGiftServer) ListLuckyGiftDraws(ctx context.Context, req *acti
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *AdminLuckyGiftServer) GetLuckyGiftDrawSummary(ctx context.Context, req *activityv1.GetLuckyGiftDrawSummaryRequest) (*activityv1.GetLuckyGiftDrawSummaryResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
summary, err := s.svc.GetDrawSummary(ctx, domain.DrawQuery{
|
||||
GiftID: req.GetGiftId(),
|
||||
PoolID: req.GetPoolId(),
|
||||
UserID: req.GetUserId(),
|
||||
RoomID: req.GetRoomId(),
|
||||
Status: req.GetStatus(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return &activityv1.GetLuckyGiftDrawSummaryResponse{Summary: luckyDrawSummaryToProto(summary)}, nil
|
||||
}
|
||||
|
||||
func luckyConfigFromProto(config *activityv1.LuckyGiftConfig) domain.Config {
|
||||
if config == nil {
|
||||
return domain.Config{}
|
||||
@ -270,3 +285,21 @@ func luckyDrawResultToProto(result domain.DrawResult) *activityv1.LuckyGiftDrawR
|
||||
CreatedAtMs: result.CreatedAtMS,
|
||||
}
|
||||
}
|
||||
|
||||
func luckyDrawSummaryToProto(summary domain.DrawSummary) *activityv1.LuckyGiftDrawSummary {
|
||||
return &activityv1.LuckyGiftDrawSummary{
|
||||
PoolId: summary.PoolID,
|
||||
TotalDraws: summary.TotalDraws,
|
||||
UniqueUsers: summary.UniqueUsers,
|
||||
UniqueRooms: summary.UniqueRooms,
|
||||
TotalSpentCoins: summary.TotalSpentCoins,
|
||||
TotalRewardCoins: summary.TotalRewardCoins,
|
||||
BaseRewardCoins: summary.BaseRewardCoins,
|
||||
RoomAtmosphereRewardCoins: summary.RoomAtmosphereRewardCoins,
|
||||
ActivitySubsidyCoins: summary.ActivitySubsidyCoins,
|
||||
ActualRtpPpm: summary.ActualRTPPPM,
|
||||
PendingDraws: summary.PendingDraws,
|
||||
GrantedDraws: summary.GrantedDraws,
|
||||
FailedDraws: summary.FailedDraws,
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ grpc_client:
|
||||
retry_backoff_multiplier: 2
|
||||
retryable_status_codes: ["UNAVAILABLE"]
|
||||
app_config:
|
||||
# H5 地址由后台 APP配置/H5配置 写入 hyapp_admin.admin_app_configs,gateway 只读下发给 App。
|
||||
# App 运行时配置由后台 APP配置写入 hyapp_admin,gateway 只读下发给 App。
|
||||
mysql_dsn: "hyapp:hyapp@tcp(mysql:3306)/hyapp_admin?parseTime=true&charset=utf8mb4&loc=UTC"
|
||||
leaderboard:
|
||||
# 活动用户榜单只读 wallet_transactions 的送礼事实;时间窗口统一按 UTC 计算。
|
||||
|
||||
@ -49,7 +49,7 @@ grpc_client:
|
||||
retry_backoff_multiplier: 2
|
||||
retryable_status_codes: ["UNAVAILABLE"]
|
||||
app_config:
|
||||
# H5 地址由后台 APP配置/H5配置 写入 hyapp_admin.admin_app_configs,gateway 只读下发给 App。
|
||||
# App 运行时配置由后台 APP配置写入 hyapp_admin,gateway 只读下发给 App。
|
||||
mysql_dsn: "TENCENT_MYSQL_USER:TENCENT_MYSQL_PASSWORD@tcp(TENCENT_MYSQL_HOST:3306)/hyapp_admin?parseTime=true&charset=utf8mb4&loc=UTC"
|
||||
leaderboard:
|
||||
# 活动用户榜单只读钱包送礼事实;线上建议配置只读账号。
|
||||
|
||||
@ -52,7 +52,7 @@ grpc_client:
|
||||
retry_backoff_multiplier: 2
|
||||
retryable_status_codes: ["UNAVAILABLE"]
|
||||
app_config:
|
||||
# H5 地址由后台 APP配置/H5配置 写入 hyapp_admin.admin_app_configs,gateway 只读下发给 App。
|
||||
# App 运行时配置由后台 APP配置写入 hyapp_admin,gateway 只读下发给 App。
|
||||
mysql_dsn: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_admin?parseTime=true&charset=utf8mb4&loc=UTC"
|
||||
leaderboard:
|
||||
# 活动用户榜单只读 wallet_transactions 的送礼事实;时间窗口统一按 UTC 计算。
|
||||
|
||||
@ -13,7 +13,12 @@ import (
|
||||
|
||||
const h5LinkGroup = "h5-links"
|
||||
|
||||
const listH5LinksSQL = "SELECT `key`, COALESCE(value, ''), updated_at_ms FROM admin_app_configs WHERE `group` = ?"
|
||||
const listH5LinksSQL = "SELECT `key`, COALESCE(description, ''), COALESCE(value, ''), updated_at_ms FROM admin_app_configs WHERE `group` = ? ORDER BY `key` ASC"
|
||||
const listExploreTabsSQL = `
|
||||
SELECT id, app_code, tab, h5_url, enabled, sort_order, updated_at_ms
|
||||
FROM admin_app_explore_tabs
|
||||
WHERE app_code = ? AND enabled = TRUE
|
||||
ORDER BY sort_order ASC, id DESC`
|
||||
const listAppBannersSQL = `
|
||||
SELECT id, app_code, cover_url, room_small_image_url, banner_type, display_scope, param, platform, sort_order, region_id, country_code, description, starts_at_ms, ends_at_ms, updated_at_ms
|
||||
FROM admin_app_banners
|
||||
@ -39,6 +44,17 @@ type H5Link struct {
|
||||
UpdatedAtMs int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
// ExploreTab 是后台 APP配置/Explore配置 中启用的 H5 tab。
|
||||
type ExploreTab struct {
|
||||
ID uint `json:"id"`
|
||||
AppCode string `json:"app_code"`
|
||||
Tab string `json:"tab"`
|
||||
H5URL string `json:"h5_url"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
UpdatedAtMs int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
// BannerQuery 是 App banner 的公开筛选条件。
|
||||
type BannerQuery struct {
|
||||
AppCode string
|
||||
@ -89,24 +105,12 @@ type Version struct {
|
||||
// Reader 是 HTTP 层读取 H5 配置的最小依赖。
|
||||
type Reader interface {
|
||||
ListH5Links(ctx context.Context) ([]H5Link, error)
|
||||
ListExploreTabs(ctx context.Context, appCode string) ([]ExploreTab, error)
|
||||
ListBanners(ctx context.Context, query BannerQuery) ([]Banner, error)
|
||||
LatestVersion(ctx context.Context, query VersionQuery) (Version, error)
|
||||
}
|
||||
|
||||
type h5LinkDefinition struct {
|
||||
Key string
|
||||
Label string
|
||||
}
|
||||
|
||||
var h5LinkDefinitions = []h5LinkDefinition{
|
||||
{Key: "host-center", Label: "Host Center"},
|
||||
{Key: "bd-center", Label: "BD Center"},
|
||||
{Key: "bd-leader-center", Label: "BD Leader Center"},
|
||||
{Key: "agency-center", Label: "Agency Center"},
|
||||
{Key: "invite-user", Label: "Invite User"},
|
||||
}
|
||||
|
||||
// MySQLReader 从 hyapp_admin.admin_app_configs 读取后台 H5 配置。
|
||||
// MySQLReader 从 hyapp_admin 读取后台 App 配置。
|
||||
type MySQLReader struct {
|
||||
db *sql.DB
|
||||
}
|
||||
@ -130,7 +134,7 @@ func (r *MySQLReader) Close() error {
|
||||
return r.db.Close()
|
||||
}
|
||||
|
||||
// ListH5Links 返回固定 H5 入口集合;未配置的入口保留空 URL,方便客户端按 key 稳定渲染。
|
||||
// ListH5Links 返回后台动态维护的 H5 入口集合。
|
||||
func (r *MySQLReader) ListH5Links(ctx context.Context) ([]H5Link, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errors.New("app config reader is not configured")
|
||||
@ -142,40 +146,49 @@ func (r *MySQLReader) ListH5Links(ctx context.Context) ([]H5Link, error) {
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
configs := make(map[string]H5Link, len(h5LinkDefinitions))
|
||||
items := make([]H5Link, 0)
|
||||
for rows.Next() {
|
||||
var key string
|
||||
var url string
|
||||
var updatedAtMS int64
|
||||
if err := rows.Scan(&key, &url, &updatedAtMS); err != nil {
|
||||
var item H5Link
|
||||
if err := rows.Scan(&item.Key, &item.Label, &item.URL, &item.UpdatedAtMs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key = strings.TrimSpace(key)
|
||||
link := H5Link{
|
||||
Key: key,
|
||||
URL: strings.TrimSpace(url),
|
||||
item.Key = strings.TrimSpace(item.Key)
|
||||
item.Label = strings.TrimSpace(item.Label)
|
||||
if item.Label == "" {
|
||||
item.Label = item.Key
|
||||
}
|
||||
link.UpdatedAtMs = updatedAtMS
|
||||
configs[key] = link
|
||||
item.URL = strings.TrimSpace(item.URL)
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
items := make([]H5Link, 0, len(h5LinkDefinitions))
|
||||
for _, definition := range h5LinkDefinitions {
|
||||
item := H5Link{
|
||||
Key: definition.Key,
|
||||
Label: definition.Label,
|
||||
}
|
||||
if config, ok := configs[definition.Key]; ok {
|
||||
item.URL = config.URL
|
||||
item.UpdatedAtMs = config.UpdatedAtMs
|
||||
// ListExploreTabs 返回当前 App 启用的 Explore H5 tabs;后台关闭的 tab 不下发给客户端。
|
||||
func (r *MySQLReader) ListExploreTabs(ctx context.Context, appCode string) ([]ExploreTab, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errors.New("app config reader is not configured")
|
||||
}
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, listExploreTabsSQL, normalizeAppCode(appCode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]ExploreTab, 0)
|
||||
for rows.Next() {
|
||||
var item ExploreTab
|
||||
if err := rows.Scan(&item.ID, &item.AppCode, &item.Tab, &item.H5URL, &item.Enabled, &item.SortOrder, &item.UpdatedAtMs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
@ -261,10 +274,11 @@ func (r *MySQLReader) LatestVersion(ctx context.Context, query VersionQuery) (Ve
|
||||
|
||||
// StaticReader 给测试和临时环境提供内存版 H5 配置读取。
|
||||
type StaticReader struct {
|
||||
Links []H5Link
|
||||
Banners []Banner
|
||||
Version Version
|
||||
Err error
|
||||
Links []H5Link
|
||||
ExploreTabs []ExploreTab
|
||||
Banners []Banner
|
||||
Version Version
|
||||
Err error
|
||||
}
|
||||
|
||||
// ListH5Links 返回预置 H5 配置。
|
||||
@ -278,6 +292,17 @@ func (r StaticReader) ListH5Links(context.Context) ([]H5Link, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ListExploreTabs 返回预置 Explore tab 配置。
|
||||
func (r StaticReader) ListExploreTabs(context.Context, string) ([]ExploreTab, error) {
|
||||
if r.Err != nil {
|
||||
return nil, r.Err
|
||||
}
|
||||
|
||||
out := make([]ExploreTab, len(r.ExploreTabs))
|
||||
copy(out, r.ExploreTabs)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ListBanners 返回预置 App banner 配置。
|
||||
func (r StaticReader) ListBanners(context.Context, BannerQuery) ([]Banner, error) {
|
||||
if r.Err != nil {
|
||||
|
||||
@ -110,7 +110,7 @@ type LoginRiskFastGuardConfig struct {
|
||||
|
||||
// AppConfigConfig 描述 gateway 读取后台 App 运行时配置的只读数据源。
|
||||
type AppConfigConfig struct {
|
||||
// MySQLDSN 指向后台管理库 hyapp_admin;gateway 只读 admin_app_configs。
|
||||
// MySQLDSN 指向后台管理库 hyapp_admin;gateway 只读 App 运行时配置表。
|
||||
MySQLDSN string `yaml:"mysql_dsn"`
|
||||
}
|
||||
|
||||
|
||||
@ -67,11 +67,12 @@ func (h *Handler) getAppBootstrap(writer http.ResponseWriter, request *http.Requ
|
||||
{Key: "me", Title: "我的", Enabled: true},
|
||||
},
|
||||
ConfigVersions: map[string]string{
|
||||
"countries": "v1",
|
||||
"banners": "v1",
|
||||
"h5_links": "v1",
|
||||
"resources": "v1",
|
||||
"gifts": "v1",
|
||||
"countries": "v1",
|
||||
"banners": "v1",
|
||||
"h5_links": "v1",
|
||||
"explore_tabs": "v1",
|
||||
"resources": "v1",
|
||||
"gifts": "v1",
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -94,6 +95,22 @@ func (h *Handler) listH5Links(writer http.ResponseWriter, request *http.Request)
|
||||
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
// listExploreTabs 返回后台 APP配置/Explore配置 中启用的 H5 tab。
|
||||
func (h *Handler) listExploreTabs(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.appConfigReader == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.appConfigReader.ListExploreTabs(request.Context(), appcode.FromContext(request.Context()))
|
||||
if err != nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
|
||||
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
|
||||
}
|
||||
|
||||
// listAppBanners 返回后台 APP配置/BANNER配置 中维护的指定显示范围 banner。
|
||||
func (h *Handler) listAppBanners(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.appConfigReader == nil {
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
// ConfigReader 是 gateway 对后台 App 配置的只读依赖。
|
||||
type ConfigReader interface {
|
||||
ListH5Links(ctx context.Context) ([]appconfig.H5Link, error)
|
||||
ListExploreTabs(ctx context.Context, appCode string) ([]appconfig.ExploreTab, error)
|
||||
ListBanners(ctx context.Context, query appconfig.BannerQuery) ([]appconfig.Banner, error)
|
||||
LatestVersion(ctx context.Context, query appconfig.VersionQuery) (appconfig.Version, error)
|
||||
}
|
||||
@ -51,6 +52,10 @@ func (h *Handler) ListH5Links(writer http.ResponseWriter, request *http.Request)
|
||||
h.listH5Links(writer, request)
|
||||
}
|
||||
|
||||
func (h *Handler) ListExploreTabs(writer http.ResponseWriter, request *http.Request) {
|
||||
h.listExploreTabs(writer, request)
|
||||
}
|
||||
|
||||
func (h *Handler) ListAppBanners(writer http.ResponseWriter, request *http.Request) {
|
||||
h.listAppBanners(writer, request)
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ type AppHandlers struct {
|
||||
ListLoginRiskBlockedCountries http.HandlerFunc
|
||||
GetAppBootstrap http.HandlerFunc
|
||||
ListH5Links http.HandlerFunc
|
||||
ListExploreTabs http.HandlerFunc
|
||||
ListAppBanners http.HandlerFunc
|
||||
GetAppVersion http.HandlerFunc
|
||||
GetResourceGroup http.HandlerFunc
|
||||
@ -239,6 +240,7 @@ func (r routes) registerAppRoutes() {
|
||||
r.public("/login-risk/blocked-countries", http.MethodGet, h.ListLoginRiskBlockedCountries)
|
||||
r.public("/app/bootstrap", "", h.GetAppBootstrap)
|
||||
r.public("/app/h5-links", "", h.ListH5Links)
|
||||
r.public("/app/explore-tabs", http.MethodGet, h.ListExploreTabs)
|
||||
r.public("/app/banners", "", h.ListAppBanners)
|
||||
r.public("/app/version", http.MethodGet, h.GetAppVersion)
|
||||
r.public("/resource-groups/{group_id}", "", h.GetResourceGroup)
|
||||
|
||||
@ -2217,6 +2217,53 @@ func TestListH5LinksRequiresConfigReader(t *testing.T) {
|
||||
assertEnvelope(t, recorder, http.StatusBadGateway, httpkit.CodeUpstreamError, "req-h5-links-missing")
|
||||
}
|
||||
|
||||
func TestListExploreTabsReturnsEnabledAdminConfig(t *testing.T) {
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
handler.SetAppConfigReader(appconfig.StaticReader{ExploreTabs: []appconfig.ExploreTab{
|
||||
{ID: 7, AppCode: "lalu", Tab: "推荐", H5URL: "https://h5.example.com/explore/recommend", Enabled: true, SortOrder: 1, UpdatedAtMs: 1700000000000},
|
||||
{ID: 8, AppCode: "lalu", Tab: "游戏", H5URL: "https://h5.example.com/explore/games", Enabled: true, SortOrder: 2, UpdatedAtMs: 1700000001000},
|
||||
}})
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/app/explore-tabs", nil)
|
||||
request.Header.Set("X-Request-ID", "req-explore-tabs")
|
||||
request.Header.Set("X-App-Package", "com.org.laluparty")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var response httpkit.ResponseEnvelope
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
data, ok := response.Data.(map[string]any)
|
||||
if response.Code != httpkit.CodeOK || !ok || data["total"].(float64) != 2 {
|
||||
t.Fatalf("unexpected explore response: %+v", response)
|
||||
}
|
||||
items, ok := data["items"].([]any)
|
||||
if !ok || len(items) != 2 {
|
||||
t.Fatalf("explore items shape mismatch: %+v", data)
|
||||
}
|
||||
first, ok := items[0].(map[string]any)
|
||||
if !ok || first["tab"] != "推荐" || first["h5_url"] != "https://h5.example.com/explore/recommend" || first["enabled"] != true {
|
||||
t.Fatalf("explore item mismatch: %+v", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListExploreTabsRequiresConfigReader(t *testing.T) {
|
||||
router := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}).Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/app/explore-tabs", nil)
|
||||
request.Header.Set("X-Request-ID", "req-explore-tabs-missing")
|
||||
request.Header.Set("X-App-Package", "com.org.laluparty")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
assertEnvelope(t, recorder, http.StatusBadGateway, httpkit.CodeUpstreamError, "req-explore-tabs-missing")
|
||||
}
|
||||
|
||||
func TestAppBootstrapIsPublicAndLightweight(t *testing.T) {
|
||||
router := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}).Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/app/bootstrap", nil)
|
||||
|
||||
@ -128,6 +128,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
||||
ListLoginRiskBlockedCountries: userAPI.ListLoginRiskBlockedCountries,
|
||||
GetAppBootstrap: appAPI.GetAppBootstrap,
|
||||
ListH5Links: appAPI.ListH5Links,
|
||||
ListExploreTabs: appAPI.ListExploreTabs,
|
||||
ListAppBanners: appAPI.ListAppBanners,
|
||||
GetAppVersion: appAPI.GetAppVersion,
|
||||
GetResourceGroup: resourceAPI.GetResourceGroup,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user