Merge remote-tracking branch 'origin/main' into codex/server-test-main-sync
This commit is contained in:
commit
f79329f6de
5
.gitignore
vendored
5
.gitignore
vendored
@ -27,4 +27,7 @@ node_modules/
|
||||
mysql-data/
|
||||
redis-data/
|
||||
|
||||
.waylog/
|
||||
.waylog/
|
||||
|
||||
.tmp/
|
||||
/tmp/
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## Non-negotiables
|
||||
|
||||
0. 任何开发,不需要历史兼容,目前只是在开发阶段
|
||||
|
||||
1. 写代码不要猜。先读当前实现、接口、配置和测试,再改。
|
||||
2. 不写流水账。文档、注释和总结都要解释边界、决策和可执行动作。
|
||||
3. 前端或客户端改动不要添加无意义描述文案。界面文字必须服务真实操作或状态。
|
||||
@ -10,6 +10,7 @@
|
||||
5. 不把连接态塞进 `room-service`。客户端长连接和消息投递属于腾讯云 IM。
|
||||
6. 代码永远保持高密度注释
|
||||
7. 写代码的时候碰到为提交修改不用关心,只去做自己的
|
||||
8. 注意关心代码历史兼容问题
|
||||
|
||||
## Architecture Boundaries
|
||||
|
||||
|
||||
20
README.md
20
README.md
@ -6,10 +6,8 @@ HY Voice Room 是语音房后端 monorepo。当前架构不再自研 IM 长连
|
||||
|
||||
核心边界:
|
||||
|
||||
任何开发,不需要历史兼容,目前只是在开发阶段
|
||||
|
||||
- `gateway-service` 是客户端 HTTP JSON 入口,负责鉴权、协议转换、签发腾讯云 IM UserSig,以及调用内部 gRPC 服务。
|
||||
- `room-service` 是房间业务状态 owner,负责 Room Cell、麦位、房间 presence、本地礼物榜、送礼落房间、语音房宝箱、snapshot、command log、outbox、Redis lease、RocketMQ room outbox relay,以及腾讯云 IM 群组/系统消息桥接。
|
||||
- `room-service` 是房间业务状态 owner,负责 Room Cell、麦位、房间 presence、本地礼物榜、送礼落房间、语音房火箭、snapshot、command log、outbox、Redis lease、RocketMQ room outbox relay,以及腾讯云 IM 群组/系统消息桥接。
|
||||
- `wallet-service` 当前提供 `DebitGift` 送礼扣费语义,余额、流水和幂等记录落 MySQL;`room-service` 的 `SendGift` 同步调用它。
|
||||
- `user-service` 当前承接登录、三方注册、密码、refresh session、用户主数据、默认短号和临时靓号基础能力。
|
||||
- `activity-service` 当前承接每日任务、活动/系统消息基础能力和房间事件 consumer;可通过 room outbox direct gRPC 或 RocketMQ `hyapp_room_outbox` 独立 consumer group 消费房间事实。
|
||||
@ -31,11 +29,15 @@ HY Voice Room 是语音房后端 monorepo。当前架构不再自研 IM 长连
|
||||
|
||||
## Time Model
|
||||
|
||||
项目全局时间模型固定为 UTC + epoch milliseconds:
|
||||
项目全局时间模型固定为 UTC + epoch milliseconds。除客户端展示外,App 后端所有按天、按周、按月归档或结算的业务口径都按 UTC,不按中国时区、服务器本地时区、用户设备时区或 IP 推断时区切分。
|
||||
|
||||
- 跨服务、数据库和 HTTP/gRPC 响应里的业务时间统一使用 Unix epoch milliseconds,字段名保持 `*_ms`,例如 `created_at_ms`、`expire_at_ms`、`start_at_ms`、`end_at_ms`、`next_refresh_at_ms`。
|
||||
- 后端业务自然日统一按 UTC 切日。每日任务的 `task_day` 是 UTC `YYYY-MM-DD`,`next_refresh_at_ms` 是下一次 UTC 00:00:00 的毫秒时间戳。
|
||||
- 后端业务自然日统一按 UTC 00:00:00 切日。`task_day`、`reward_day`、`checkin_day`、`stat_day`、`budget_day` 都是 UTC `YYYY-MM-DD`;`next_refresh_at_ms` 是下一次 UTC 00:00:00 的毫秒时间戳。
|
||||
- App 每日任务和七日签到按 UTC 日刷新。任务事件归属到 `occurred_at_ms` 所在 UTC 日;七日签到连续性只比较今天和昨天的 UTC 日期。
|
||||
- 日榜、周榜、月榜按 UTC 周期计算。`today/day/daily` 是 UTC 当天,`week/weekly` 从 UTC 周一 00:00 开始,`month/monthly` 从 UTC 月初 00:00 开始;后台用户送礼榜按 `[周期开始, 当前 UTC 时间)` 查询,房间礼物榜 Redis key 也按 UTC 周期开始时间生成。
|
||||
- 统计和账务时间范围统一使用 `[start_ms, end_ms)`,也就是包含开始、不包含结束。查询“昨天充值多少”时,调用方必须先把“昨天”转换成明确的 UTC 毫秒范围,再交给后端查询。
|
||||
- 定时任务由 `cron-service` 按 `interval` 轮询,不使用本地时区表达“几点运行”。任务运行记录、lease 和传给 owner service 的 `now_ms` 都是 UTC epoch milliseconds。
|
||||
- 工资和结算周期按 UTC 月锚定。主播工资周期键 `cycle_key` 是 UTC `YYYY-MM`;主播日结、半月结和月底清算由 cron 触发后在 `wallet-service` 内按这个周期键幂等结算。团队 BD/Admin 自动结算在后台按 UTC 每月 5 日判断,结算上一个 UTC 自然月。
|
||||
- MySQL DSN 必须使用 `loc=UTC`。Docker 环境设置 `TZ=UTC`,MySQL 使用 `--default-time-zone=+00:00`,避免依赖服务器所在地或容器本地时区。
|
||||
- 用户所在时区、设备时区或 IP 推断时区只能用于展示、风控或资料字段;不能作为结算、统计、任务刷新和幂等周期的业务切日来源。
|
||||
|
||||
@ -141,7 +143,7 @@ Authorization: Bearer <access_token>
|
||||
- `gateway-service` 创建房间时从 access token 取当前 `user_id` 写入 `RequestMeta.actor_user_id`;`room-service` 由该 actor 收敛 owner,外部 CreateRoom 请求体不接收 `owner_user_id`。
|
||||
- `X-Request-ID`/`request_id` 由后端生成,只做链路追踪;写操作的 `command_id` 由前端按用户动作生成,用于服务端幂等。
|
||||
- `room-service` 在 `CreateRoom` 成功后写 `room_outbox`,由 outbox worker 异步创建或补偿同名腾讯云 IM 群组。
|
||||
- `room-service` 在上下麦、禁言、踢人、送礼、宝箱进度和开箱等命令提交后只写房间状态、command log 和 outbox;腾讯云 IM 房间系统自定义消息由 outbox worker direct 投递,或由 IM bridge consumer 从 RocketMQ 消费后投递。
|
||||
- `room-service` 在上下麦、禁言、踢人、送礼、火箭进度和发射等命令提交后只写房间状态、command log 和 outbox;腾讯云 IM 房间系统自定义消息由 outbox worker direct 投递,或由 IM bridge consumer 从 RocketMQ 消费后投递。
|
||||
- `notice-service` 消费钱包余额变更 outbox 和房间踢人 outbox,向单个用户发送 `wallet_notice`、`room_notice` 私有自定义消息;owner service 不直接调用腾讯云 IM 单聊。
|
||||
- `gateway-service` 已提供 `/api/v1/tencent-im/callback`,支持腾讯云 IM 入群前和发言前回调,并分别回查 `VerifyRoomPresence` 和 `CheckSpeakPermission`。
|
||||
|
||||
@ -181,7 +183,7 @@ docs/flutter对接/Flutter App 通知与钱包余额对接.md
|
||||
|
||||
| Topic | Producer | Consumer Group | 用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-activity-room-outbox` | activity-service 消费 `RoomGiftSent`、`RoomTreasureCountdownStarted` 等房间事实 |
|
||||
| `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-activity-room-outbox` | activity-service 消费 `RoomGiftSent`、`RoomRocketIgnited` 等房间事实 |
|
||||
| `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` 并更新用户麦时读模型 |
|
||||
@ -193,9 +195,9 @@ docs/flutter对接/Flutter App 通知与钱包余额对接.md
|
||||
| `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 开箱命令 |
|
||||
| `hyapp_room_rocket_launch` | `hyapp-room-rocket-launch-producer` | `hyapp-room-rocket-launch` | 火箭点火后到点唤醒 room-service 发射命令 |
|
||||
|
||||
RocketMQ 不拥有业务状态。`room_outbox`、`wallet_outbox`、`user_outbox`、`game_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 会重新校验 `rocket_id`、等级、`launch_at_ms`、状态和 UTC 重置边界。
|
||||
|
||||
## Storage Model
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -524,11 +524,14 @@ message ListRegistrationRewardClaimsRequest {
|
||||
int64 user_id = 3;
|
||||
int32 page = 4;
|
||||
int32 page_size = 5;
|
||||
int64 claimed_start_ms = 6;
|
||||
int64 claimed_end_ms = 7;
|
||||
}
|
||||
|
||||
message ListRegistrationRewardClaimsResponse {
|
||||
repeated RegistrationRewardClaim claims = 1;
|
||||
int64 total = 2;
|
||||
int64 today_claimed_count = 3;
|
||||
}
|
||||
|
||||
// FirstRechargeRewardTier 是首冲奖励的一个金币充值档位;max_coin_amount=0 表示无上限。
|
||||
@ -647,6 +650,255 @@ message ListFirstRechargeRewardClaimsResponse {
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
// CumulativeRechargeRewardTier 是累充奖励的单个 USD 档位;threshold_usd_minor 以美分存储。
|
||||
message CumulativeRechargeRewardTier {
|
||||
int64 tier_id = 1;
|
||||
string tier_code = 2;
|
||||
string tier_name = 3;
|
||||
int64 threshold_usd_minor = 4;
|
||||
int64 resource_group_id = 5;
|
||||
string status = 6;
|
||||
int32 sort_order = 7;
|
||||
int64 created_at_ms = 8;
|
||||
int64 updated_at_ms = 9;
|
||||
}
|
||||
|
||||
// CumulativeRechargeRewardConfig 是当前 App 的累充奖励配置。
|
||||
message CumulativeRechargeRewardConfig {
|
||||
string app_code = 1;
|
||||
bool enabled = 2;
|
||||
repeated CumulativeRechargeRewardTier tiers = 3;
|
||||
int64 updated_by_admin_id = 4;
|
||||
int64 created_at_ms = 5;
|
||||
int64 updated_at_ms = 6;
|
||||
}
|
||||
|
||||
// CumulativeRechargeRewardGrant 是用户在一个 UTC 自然周内达成某个档位后的发放事实。
|
||||
message CumulativeRechargeRewardGrant {
|
||||
string grant_id = 1;
|
||||
string app_code = 2;
|
||||
string cycle_key = 3;
|
||||
int64 user_id = 4;
|
||||
string event_id = 5;
|
||||
string transaction_id = 6;
|
||||
string command_id = 7;
|
||||
int64 tier_id = 8;
|
||||
string tier_code = 9;
|
||||
int64 threshold_usd_minor = 10;
|
||||
int64 resource_group_id = 11;
|
||||
int64 reached_usd_minor = 12;
|
||||
int64 qualifying_usd_minor = 13;
|
||||
int64 recharge_coin_amount = 14;
|
||||
string recharge_type = 15;
|
||||
string status = 16;
|
||||
string wallet_command_id = 17;
|
||||
string wallet_grant_id = 18;
|
||||
string failure_reason = 19;
|
||||
int64 granted_at_ms = 20;
|
||||
int64 created_at_ms = 21;
|
||||
int64 updated_at_ms = 22;
|
||||
}
|
||||
|
||||
// CumulativeRechargeRewardProgress 是用户在当前 UTC 周期内的累充累计投影。
|
||||
message CumulativeRechargeRewardProgress {
|
||||
string app_code = 1;
|
||||
string cycle_key = 2;
|
||||
int64 user_id = 3;
|
||||
int64 total_usd_minor = 4;
|
||||
int64 total_coin_amount = 5;
|
||||
int64 first_recharged_at_ms = 6;
|
||||
int64 last_recharged_at_ms = 7;
|
||||
int64 updated_at_ms = 8;
|
||||
}
|
||||
|
||||
// CumulativeRechargeRewardStatus 是 App 累充奖励 H5 可直接展示的当前状态。
|
||||
message CumulativeRechargeRewardStatus {
|
||||
CumulativeRechargeRewardConfig config = 1;
|
||||
CumulativeRechargeRewardProgress progress = 2;
|
||||
repeated CumulativeRechargeRewardGrant grants = 3;
|
||||
string cycle_key = 4;
|
||||
int64 period_start_ms = 5;
|
||||
int64 period_end_ms = 6;
|
||||
int64 server_time_ms = 7;
|
||||
}
|
||||
|
||||
message GetCumulativeRechargeRewardStatusRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
}
|
||||
|
||||
message GetCumulativeRechargeRewardStatusResponse {
|
||||
CumulativeRechargeRewardStatus status = 1;
|
||||
}
|
||||
|
||||
// ConsumeCumulativeRechargeRewardRequest 是 wallet 充值事实消费入口;所有成功充值都会按 UTC 周期累计。
|
||||
message ConsumeCumulativeRechargeRewardRequest {
|
||||
RequestMeta meta = 1;
|
||||
string event_id = 2;
|
||||
string transaction_id = 3;
|
||||
string command_id = 4;
|
||||
int64 user_id = 5;
|
||||
int64 recharge_coin_amount = 6;
|
||||
int64 recharge_sequence = 7;
|
||||
string recharge_type = 8;
|
||||
int64 qualifying_usd_minor = 9;
|
||||
string payload_json = 10;
|
||||
int64 occurred_at_ms = 11;
|
||||
}
|
||||
|
||||
message ConsumeCumulativeRechargeRewardResponse {
|
||||
repeated CumulativeRechargeRewardGrant grants = 1;
|
||||
bool consumed = 2;
|
||||
string reason = 3;
|
||||
}
|
||||
|
||||
message GetCumulativeRechargeRewardConfigRequest {
|
||||
RequestMeta meta = 1;
|
||||
}
|
||||
|
||||
message GetCumulativeRechargeRewardConfigResponse {
|
||||
CumulativeRechargeRewardConfig config = 1;
|
||||
}
|
||||
|
||||
message UpdateCumulativeRechargeRewardConfigRequest {
|
||||
RequestMeta meta = 1;
|
||||
bool enabled = 2;
|
||||
repeated CumulativeRechargeRewardTier tiers = 3;
|
||||
int64 operator_admin_id = 4;
|
||||
}
|
||||
|
||||
message UpdateCumulativeRechargeRewardConfigResponse {
|
||||
CumulativeRechargeRewardConfig config = 1;
|
||||
}
|
||||
|
||||
message ListCumulativeRechargeRewardGrantsRequest {
|
||||
RequestMeta meta = 1;
|
||||
string status = 2;
|
||||
int64 user_id = 3;
|
||||
string cycle_key = 4;
|
||||
int32 page = 5;
|
||||
int32 page_size = 6;
|
||||
}
|
||||
|
||||
message ListCumulativeRechargeRewardGrantsResponse {
|
||||
repeated CumulativeRechargeRewardGrant grants = 1;
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
// RoomTurnoverRewardTier 是房间流水奖励的单个档位;达到 threshold_coin_spent 后可命中奖励。
|
||||
message RoomTurnoverRewardTier {
|
||||
int64 tier_id = 1;
|
||||
string tier_code = 2;
|
||||
string tier_name = 3;
|
||||
int64 threshold_coin_spent = 4;
|
||||
int64 reward_coin_amount = 5;
|
||||
string status = 6;
|
||||
int32 sort_order = 7;
|
||||
int64 created_at_ms = 8;
|
||||
int64 updated_at_ms = 9;
|
||||
}
|
||||
|
||||
// RoomTurnoverRewardConfig 是当前 App 的房间流水奖励配置;结算只命中最高有效档。
|
||||
message RoomTurnoverRewardConfig {
|
||||
string app_code = 1;
|
||||
bool enabled = 2;
|
||||
repeated RoomTurnoverRewardTier tiers = 3;
|
||||
int64 updated_by_admin_id = 4;
|
||||
int64 created_at_ms = 5;
|
||||
int64 updated_at_ms = 6;
|
||||
}
|
||||
|
||||
// RoomTurnoverRewardSettlement 是某个房间单个 UTC 周期的结算事实。
|
||||
message RoomTurnoverRewardSettlement {
|
||||
string settlement_id = 1;
|
||||
string app_code = 2;
|
||||
string room_id = 3;
|
||||
int64 owner_user_id = 4;
|
||||
int64 period_start_ms = 5;
|
||||
int64 period_end_ms = 6;
|
||||
int64 coin_spent = 7;
|
||||
int64 tier_id = 8;
|
||||
string tier_code = 9;
|
||||
int64 threshold_coin_spent = 10;
|
||||
int64 reward_coin_amount = 11;
|
||||
string status = 12;
|
||||
string wallet_command_id = 13;
|
||||
string wallet_transaction_id = 14;
|
||||
string failure_reason = 15;
|
||||
int64 settled_at_ms = 16;
|
||||
int64 created_at_ms = 17;
|
||||
int64 updated_at_ms = 18;
|
||||
}
|
||||
|
||||
// RoomTurnoverRewardStatus 给 App H5 返回当前 UTC 周期、房间流水、预计奖励和上一期结算。
|
||||
message RoomTurnoverRewardStatus {
|
||||
RoomTurnoverRewardConfig config = 1;
|
||||
string room_id = 2;
|
||||
int64 owner_user_id = 3;
|
||||
int64 period_start_ms = 4;
|
||||
int64 period_end_ms = 5;
|
||||
int64 current_coin_spent = 6;
|
||||
int64 expected_reward_coin_amount = 7;
|
||||
RoomTurnoverRewardTier matched_tier = 8;
|
||||
RoomTurnoverRewardSettlement latest_settlement = 9;
|
||||
int64 server_time_ms = 10;
|
||||
}
|
||||
|
||||
message GetRoomTurnoverRewardStatusRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
string room_id = 3;
|
||||
int64 owner_user_id = 4;
|
||||
}
|
||||
|
||||
message GetRoomTurnoverRewardStatusResponse {
|
||||
RoomTurnoverRewardStatus status = 1;
|
||||
}
|
||||
|
||||
message GetRoomTurnoverRewardConfigRequest {
|
||||
RequestMeta meta = 1;
|
||||
}
|
||||
|
||||
message GetRoomTurnoverRewardConfigResponse {
|
||||
RoomTurnoverRewardConfig config = 1;
|
||||
}
|
||||
|
||||
message UpdateRoomTurnoverRewardConfigRequest {
|
||||
RequestMeta meta = 1;
|
||||
bool enabled = 2;
|
||||
repeated RoomTurnoverRewardTier tiers = 3;
|
||||
int64 operator_admin_id = 4;
|
||||
}
|
||||
|
||||
message UpdateRoomTurnoverRewardConfigResponse {
|
||||
RoomTurnoverRewardConfig config = 1;
|
||||
}
|
||||
|
||||
message ListRoomTurnoverRewardSettlementsRequest {
|
||||
RequestMeta meta = 1;
|
||||
string status = 2;
|
||||
string room_id = 3;
|
||||
int64 owner_user_id = 4;
|
||||
int64 period_start_ms = 5;
|
||||
int32 page = 6;
|
||||
int32 page_size = 7;
|
||||
}
|
||||
|
||||
message ListRoomTurnoverRewardSettlementsResponse {
|
||||
repeated RoomTurnoverRewardSettlement settlements = 1;
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
message RetryRoomTurnoverRewardSettlementRequest {
|
||||
RequestMeta meta = 1;
|
||||
string settlement_id = 2;
|
||||
int64 operator_admin_id = 3;
|
||||
}
|
||||
|
||||
message RetryRoomTurnoverRewardSettlementResponse {
|
||||
RoomTurnoverRewardSettlement settlement = 1;
|
||||
}
|
||||
|
||||
// SevenDayCheckInReward 是七日签到配置中某一天的资源组奖励。
|
||||
message SevenDayCheckInReward {
|
||||
int32 day_index = 1;
|
||||
@ -1396,6 +1648,162 @@ message GetLuckyGiftDrawSummaryResponse {
|
||||
LuckyGiftDrawSummary summary = 1;
|
||||
}
|
||||
|
||||
// WeeklyStarGift 是周星周期内参与积分的指定礼物。
|
||||
message WeeklyStarGift {
|
||||
string gift_id = 1;
|
||||
int32 sort_order = 2;
|
||||
}
|
||||
|
||||
// WeeklyStarReward 是周星 Top 排名对应的资源组奖励。
|
||||
message WeeklyStarReward {
|
||||
int32 rank_no = 1;
|
||||
int64 resource_group_id = 2;
|
||||
}
|
||||
|
||||
// WeeklyStarCycle 是一个按 UTC epoch ms 生效的周星配置周期。
|
||||
message WeeklyStarCycle {
|
||||
string cycle_id = 1;
|
||||
string activity_code = 2;
|
||||
int64 region_id = 3;
|
||||
string title = 4;
|
||||
string status = 5;
|
||||
int64 start_ms = 6;
|
||||
int64 end_ms = 7;
|
||||
repeated WeeklyStarGift gifts = 8;
|
||||
repeated WeeklyStarReward rewards = 9;
|
||||
int64 created_by_admin_id = 10;
|
||||
int64 updated_by_admin_id = 11;
|
||||
int64 settled_at_ms = 12;
|
||||
int64 created_at_ms = 13;
|
||||
int64 updated_at_ms = 14;
|
||||
string app_code = 15;
|
||||
}
|
||||
|
||||
// WeeklyStarLeaderboardEntry 是一个周期内用户维度的积分排名。
|
||||
message WeeklyStarLeaderboardEntry {
|
||||
int32 rank_no = 1;
|
||||
int64 user_id = 2;
|
||||
int64 score = 3;
|
||||
int64 first_scored_at_ms = 4;
|
||||
int64 last_scored_at_ms = 5;
|
||||
}
|
||||
|
||||
// WeeklyStarSettlement 是周期结束后 Top 奖励发放的幂等记录。
|
||||
message WeeklyStarSettlement {
|
||||
string settlement_id = 1;
|
||||
string cycle_id = 2;
|
||||
int32 rank_no = 3;
|
||||
int64 user_id = 4;
|
||||
int64 score = 5;
|
||||
int64 resource_group_id = 6;
|
||||
string wallet_command_id = 7;
|
||||
string wallet_grant_id = 8;
|
||||
string status = 9;
|
||||
string failure_reason = 10;
|
||||
int32 attempt_count = 11;
|
||||
int64 created_at_ms = 12;
|
||||
int64 updated_at_ms = 13;
|
||||
}
|
||||
|
||||
message ListWeeklyStarCyclesRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 region_id = 2;
|
||||
string status = 3;
|
||||
int64 start_ms = 4;
|
||||
int64 end_ms = 5;
|
||||
int32 page = 6;
|
||||
int32 page_size = 7;
|
||||
}
|
||||
|
||||
message ListWeeklyStarCyclesResponse {
|
||||
repeated WeeklyStarCycle cycles = 1;
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
message GetWeeklyStarCycleRequest {
|
||||
RequestMeta meta = 1;
|
||||
string cycle_id = 2;
|
||||
}
|
||||
|
||||
message GetWeeklyStarCycleResponse {
|
||||
WeeklyStarCycle cycle = 1;
|
||||
}
|
||||
|
||||
message UpsertWeeklyStarCycleRequest {
|
||||
RequestMeta meta = 1;
|
||||
WeeklyStarCycle cycle = 2;
|
||||
int64 operator_admin_id = 3;
|
||||
}
|
||||
|
||||
message UpsertWeeklyStarCycleResponse {
|
||||
WeeklyStarCycle cycle = 1;
|
||||
bool created = 2;
|
||||
}
|
||||
|
||||
message SetWeeklyStarCycleStatusRequest {
|
||||
RequestMeta meta = 1;
|
||||
string cycle_id = 2;
|
||||
string status = 3;
|
||||
int64 operator_admin_id = 4;
|
||||
}
|
||||
|
||||
message SetWeeklyStarCycleStatusResponse {
|
||||
WeeklyStarCycle cycle = 1;
|
||||
}
|
||||
|
||||
message ListWeeklyStarLeaderboardRequest {
|
||||
RequestMeta meta = 1;
|
||||
string cycle_id = 2;
|
||||
int64 region_id = 3;
|
||||
int32 page_size = 4;
|
||||
string page_token = 5;
|
||||
}
|
||||
|
||||
message ListWeeklyStarLeaderboardResponse {
|
||||
WeeklyStarCycle cycle = 1;
|
||||
repeated WeeklyStarLeaderboardEntry entries = 2;
|
||||
string next_page_token = 3;
|
||||
int64 total = 4;
|
||||
}
|
||||
|
||||
message ListWeeklyStarSettlementsRequest {
|
||||
RequestMeta meta = 1;
|
||||
string cycle_id = 2;
|
||||
}
|
||||
|
||||
message ListWeeklyStarSettlementsResponse {
|
||||
repeated WeeklyStarSettlement settlements = 1;
|
||||
}
|
||||
|
||||
message GetWeeklyStarCurrentRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
int64 region_id = 3;
|
||||
}
|
||||
|
||||
message GetWeeklyStarCurrentResponse {
|
||||
WeeklyStarCycle cycle = 1;
|
||||
repeated WeeklyStarLeaderboardEntry top_entries = 2;
|
||||
WeeklyStarLeaderboardEntry my_entry = 3;
|
||||
int64 server_time_ms = 4;
|
||||
}
|
||||
|
||||
message ListWeeklyStarHistoryRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 region_id = 2;
|
||||
int32 limit = 3;
|
||||
}
|
||||
|
||||
message WeeklyStarHistoryCycle {
|
||||
WeeklyStarCycle cycle = 1;
|
||||
repeated WeeklyStarLeaderboardEntry top_entries = 2;
|
||||
}
|
||||
|
||||
message ListWeeklyStarHistoryResponse {
|
||||
repeated WeeklyStarHistoryCycle cycles = 1;
|
||||
int64 server_time_ms = 2;
|
||||
}
|
||||
|
||||
// ActivityService 只暴露同步查询,不把事件消费伪装成 RPC。
|
||||
service ActivityService {
|
||||
rpc PingActivity(PingActivityRequest) returns (PingActivityResponse);
|
||||
@ -1418,6 +1826,8 @@ service ActivityCronService {
|
||||
rpc ProcessMessageFanoutBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
rpc ProcessLevelRewardBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
rpc ProcessAchievementRewardBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
rpc ProcessRoomTurnoverRewardSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
rpc ProcessWeeklyStarSettlementBatch(CronBatchRequest) returns (CronBatchResponse);
|
||||
}
|
||||
|
||||
// TaskService 拥有 App 任务查询、事件进度消费和领奖状态。
|
||||
@ -1451,6 +1861,18 @@ service LuckyGiftService {
|
||||
rpc ExecuteLuckyGiftDraw(ExecuteLuckyGiftDrawRequest) returns (ExecuteLuckyGiftDrawResponse);
|
||||
}
|
||||
|
||||
// RoomTurnoverRewardService owns App reads for the weekly room turnover reward activity.
|
||||
service RoomTurnoverRewardService {
|
||||
rpc GetRoomTurnoverRewardStatus(GetRoomTurnoverRewardStatusRequest) returns (GetRoomTurnoverRewardStatusResponse);
|
||||
}
|
||||
|
||||
// WeeklyStarService owns App reads for the weekly specified-gift score activity.
|
||||
service WeeklyStarService {
|
||||
rpc GetWeeklyStarCurrent(GetWeeklyStarCurrentRequest) returns (GetWeeklyStarCurrentResponse);
|
||||
rpc ListWeeklyStarLeaderboard(ListWeeklyStarLeaderboardRequest) returns (ListWeeklyStarLeaderboardResponse);
|
||||
rpc ListWeeklyStarHistory(ListWeeklyStarHistoryRequest) returns (ListWeeklyStarHistoryResponse);
|
||||
}
|
||||
|
||||
// BroadcastService owns server-side Tencent IM global/region broadcast delivery.
|
||||
service BroadcastService {
|
||||
rpc EnsureBroadcastGroups(EnsureBroadcastGroupsRequest) returns (EnsureBroadcastGroupsResponse);
|
||||
@ -1498,6 +1920,38 @@ service AdminFirstRechargeRewardService {
|
||||
rpc ListFirstRechargeRewardClaims(ListFirstRechargeRewardClaimsRequest) returns (ListFirstRechargeRewardClaimsResponse);
|
||||
}
|
||||
|
||||
// CumulativeRechargeRewardService 拥有 App 累充奖励查询和 wallet 充值事实消费入口。
|
||||
service CumulativeRechargeRewardService {
|
||||
rpc GetCumulativeRechargeRewardStatus(GetCumulativeRechargeRewardStatusRequest) returns (GetCumulativeRechargeRewardStatusResponse);
|
||||
rpc ConsumeCumulativeRechargeReward(ConsumeCumulativeRechargeRewardRequest) returns (ConsumeCumulativeRechargeRewardResponse);
|
||||
}
|
||||
|
||||
// AdminCumulativeRechargeRewardService 是后台活动管理访问累充奖励配置和发放记录的唯一入口。
|
||||
service AdminCumulativeRechargeRewardService {
|
||||
rpc GetCumulativeRechargeRewardConfig(GetCumulativeRechargeRewardConfigRequest) returns (GetCumulativeRechargeRewardConfigResponse);
|
||||
rpc UpdateCumulativeRechargeRewardConfig(UpdateCumulativeRechargeRewardConfigRequest) returns (UpdateCumulativeRechargeRewardConfigResponse);
|
||||
rpc ListCumulativeRechargeRewardGrants(ListCumulativeRechargeRewardGrantsRequest) returns (ListCumulativeRechargeRewardGrantsResponse);
|
||||
}
|
||||
|
||||
// AdminRoomTurnoverRewardService 是后台活动管理访问房间流水奖励配置和结算记录的唯一入口。
|
||||
service AdminRoomTurnoverRewardService {
|
||||
rpc GetRoomTurnoverRewardConfig(GetRoomTurnoverRewardConfigRequest) returns (GetRoomTurnoverRewardConfigResponse);
|
||||
rpc UpdateRoomTurnoverRewardConfig(UpdateRoomTurnoverRewardConfigRequest) returns (UpdateRoomTurnoverRewardConfigResponse);
|
||||
rpc ListRoomTurnoverRewardSettlements(ListRoomTurnoverRewardSettlementsRequest) returns (ListRoomTurnoverRewardSettlementsResponse);
|
||||
rpc RetryRoomTurnoverRewardSettlement(RetryRoomTurnoverRewardSettlementRequest) returns (RetryRoomTurnoverRewardSettlementResponse);
|
||||
}
|
||||
|
||||
// AdminWeeklyStarService 是后台活动管理访问周星周期、榜单和结算结果的唯一入口。
|
||||
service AdminWeeklyStarService {
|
||||
rpc ListWeeklyStarCycles(ListWeeklyStarCyclesRequest) returns (ListWeeklyStarCyclesResponse);
|
||||
rpc CreateWeeklyStarCycle(UpsertWeeklyStarCycleRequest) returns (UpsertWeeklyStarCycleResponse);
|
||||
rpc GetWeeklyStarCycle(GetWeeklyStarCycleRequest) returns (GetWeeklyStarCycleResponse);
|
||||
rpc UpdateWeeklyStarCycle(UpsertWeeklyStarCycleRequest) returns (UpsertWeeklyStarCycleResponse);
|
||||
rpc SetWeeklyStarCycleStatus(SetWeeklyStarCycleStatusRequest) returns (SetWeeklyStarCycleStatusResponse);
|
||||
rpc ListWeeklyStarLeaderboard(ListWeeklyStarLeaderboardRequest) returns (ListWeeklyStarLeaderboardResponse);
|
||||
rpc ListWeeklyStarSettlements(ListWeeklyStarSettlementsRequest) returns (ListWeeklyStarSettlementsResponse);
|
||||
}
|
||||
|
||||
// SevenDayCheckInService 拥有 App 七日签到查询和签到命令。
|
||||
service SevenDayCheckInService {
|
||||
rpc GetSevenDayCheckInStatus(GetSevenDayCheckInStatusRequest) returns (GetSevenDayCheckInStatusResponse);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -63,6 +63,8 @@ message RoomUserJoined {
|
||||
// visible_region_id 是用户进房时所在房间的国家/区域桶,统计服务不能反查 room-service。
|
||||
int64 visible_region_id = 3;
|
||||
RoomEntryVehicleSnapshot entry_vehicle = 4;
|
||||
int64 country_id = 5;
|
||||
int64 region_id = 6;
|
||||
}
|
||||
|
||||
// RoomUserLeft 表达用户离房成功。
|
||||
@ -112,6 +114,8 @@ message RoomChatEnabledChanged {
|
||||
message RoomPasswordChanged {
|
||||
int64 actor_user_id = 1;
|
||||
bool locked = 2;
|
||||
// visible_region_id 是房间当前可见区域,activity-service 用它把锁房变化投到区域播报群。
|
||||
int64 visible_region_id = 3;
|
||||
}
|
||||
|
||||
// RoomAdminChanged 表达房间管理员集合变更。
|
||||
@ -151,6 +155,9 @@ message RoomGiftSent {
|
||||
int64 visible_region_id = 7;
|
||||
string command_id = 8;
|
||||
string pool_id = 9;
|
||||
int64 coin_spent = 10;
|
||||
int64 country_id = 11;
|
||||
int64 region_id = 12;
|
||||
}
|
||||
|
||||
// RoomHeatChanged 表达热度变化结果。
|
||||
@ -166,7 +173,7 @@ message RoomRankChanged {
|
||||
int64 gift_value = 3;
|
||||
}
|
||||
|
||||
message RoomTreasureRewardGrant {
|
||||
message RoomRocketRewardGrant {
|
||||
string reward_role = 1;
|
||||
int64 user_id = 2;
|
||||
string reward_item_id = 3;
|
||||
@ -177,15 +184,15 @@ message RoomTreasureRewardGrant {
|
||||
string status = 8;
|
||||
}
|
||||
|
||||
// RoomTreasureProgressChanged 表达送礼扣费成功后宝箱有效能量发生变化。
|
||||
message RoomTreasureProgressChanged {
|
||||
string box_id = 1;
|
||||
// RoomRocketFuelChanged 表达送礼扣费成功后火箭有效燃料发生变化。
|
||||
message RoomRocketFuelChanged {
|
||||
string rocket_id = 1;
|
||||
int32 level = 2;
|
||||
int64 added_energy = 3;
|
||||
int64 effective_added_energy = 4;
|
||||
int64 overflow_energy = 5;
|
||||
int64 current_progress = 6;
|
||||
int64 energy_threshold = 7;
|
||||
int64 added_fuel = 3;
|
||||
int64 effective_added_fuel = 4;
|
||||
int64 overflow_fuel = 5;
|
||||
int64 current_fuel = 6;
|
||||
int64 fuel_threshold = 7;
|
||||
string status = 8;
|
||||
int64 reset_at_ms = 9;
|
||||
int64 sender_user_id = 10;
|
||||
@ -195,37 +202,40 @@ message RoomTreasureProgressChanged {
|
||||
int64 visible_region_id = 14;
|
||||
}
|
||||
|
||||
// RoomTreasureCountdownStarted 表达宝箱满能量后进入倒计时,并作为区域/全局播报的事实源。
|
||||
message RoomTreasureCountdownStarted {
|
||||
string box_id = 1;
|
||||
// RoomRocketIgnited 表达火箭燃料满后进入倒计时,并作为区域/全局播报的事实源。
|
||||
message RoomRocketIgnited {
|
||||
string rocket_id = 1;
|
||||
int32 level = 2;
|
||||
int64 current_progress = 3;
|
||||
int64 energy_threshold = 4;
|
||||
int64 countdown_started_at_ms = 5;
|
||||
int64 open_at_ms = 6;
|
||||
int64 current_fuel = 3;
|
||||
int64 fuel_threshold = 4;
|
||||
int64 ignited_at_ms = 5;
|
||||
int64 launch_at_ms = 6;
|
||||
string broadcast_scope = 7;
|
||||
int64 visible_region_id = 8;
|
||||
int64 top1_user_id = 9;
|
||||
int64 igniter_user_id = 10;
|
||||
string command_id = 11;
|
||||
string room_short_id = 12;
|
||||
string rocket_cover_url = 13;
|
||||
int64 reset_at_ms = 14;
|
||||
}
|
||||
|
||||
// RoomTreasureOpened 表达宝箱已经打开,在线用户名单按开箱瞬间 Room Cell presence 结算。
|
||||
message RoomTreasureOpened {
|
||||
string box_id = 1;
|
||||
// RoomRocketLaunched 表达火箭已经发射,在线用户名单按发射瞬间 Room Cell presence 结算。
|
||||
message RoomRocketLaunched {
|
||||
string rocket_id = 1;
|
||||
int32 level = 2;
|
||||
int32 next_level = 3;
|
||||
int64 opened_at_ms = 4;
|
||||
int64 launched_at_ms = 4;
|
||||
int64 reset_at_ms = 5;
|
||||
int64 top1_user_id = 6;
|
||||
int64 igniter_user_id = 7;
|
||||
repeated int64 in_room_user_ids = 8;
|
||||
repeated RoomTreasureRewardGrant rewards = 9;
|
||||
repeated RoomRocketRewardGrant rewards = 9;
|
||||
}
|
||||
|
||||
// RoomTreasureRewardGranted 是客户端展示发奖弹窗和私有通知的最小事实。
|
||||
message RoomTreasureRewardGranted {
|
||||
string box_id = 1;
|
||||
// RoomRocketRewardGranted 是客户端展示发奖弹窗和私有通知的最小事实。
|
||||
message RoomRocketRewardGranted {
|
||||
string rocket_id = 1;
|
||||
int32 level = 2;
|
||||
repeated RoomTreasureRewardGrant rewards = 3;
|
||||
repeated RoomRocketRewardGrant rewards = 3;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -65,6 +65,8 @@ message SeatState {
|
||||
bool mic_muted = 9;
|
||||
// seat_status 是服务端派生的展示状态:empty/locked/occupied/publishing/muted。
|
||||
string seat_status = 10;
|
||||
// mic_heartbeat_at_ms 是当前 mic_session 最近一次服务端接受麦上心跳的 UTC epoch ms。
|
||||
int64 mic_heartbeat_at_ms = 11;
|
||||
}
|
||||
|
||||
// RankItem 表达房间内本地礼物榜项目。
|
||||
@ -100,8 +102,8 @@ message LuckyGiftDrawResult {
|
||||
int64 target_user_id = 20;
|
||||
}
|
||||
|
||||
// RoomTreasureRewardItem 是后台配置给客户端展示的宝箱奖励候选项。
|
||||
message RoomTreasureRewardItem {
|
||||
// RoomRocketRewardItem 是后台配置给客户端展示的火箭奖励候选项。
|
||||
message RoomRocketRewardItem {
|
||||
string reward_item_id = 1;
|
||||
int64 resource_group_id = 2;
|
||||
int64 weight = 3;
|
||||
@ -109,21 +111,21 @@ message RoomTreasureRewardItem {
|
||||
string icon_url = 5;
|
||||
}
|
||||
|
||||
// RoomTreasureLevel 暴露单个等级宝箱的物料、阈值和三类奖励池。
|
||||
message RoomTreasureLevel {
|
||||
// RoomRocketLevel 暴露单个等级火箭的物料、阈值和三类奖励池。
|
||||
message RoomRocketLevel {
|
||||
int32 level = 1;
|
||||
int64 energy_threshold = 2;
|
||||
int64 fuel_threshold = 2;
|
||||
string cover_url = 3;
|
||||
string animation_url = 4;
|
||||
string opening_animation_url = 5;
|
||||
string opened_image_url = 6;
|
||||
repeated RoomTreasureRewardItem in_room_rewards = 7;
|
||||
repeated RoomTreasureRewardItem top1_rewards = 8;
|
||||
repeated RoomTreasureRewardItem igniter_rewards = 9;
|
||||
string launch_animation_url = 5;
|
||||
string launched_image_url = 6;
|
||||
repeated RoomRocketRewardItem in_room_rewards = 7;
|
||||
repeated RoomRocketRewardItem top1_rewards = 8;
|
||||
repeated RoomRocketRewardItem igniter_rewards = 9;
|
||||
}
|
||||
|
||||
// RoomTreasureRewardGrant 是一次宝箱打开后给某个用户结算出的具体奖励。
|
||||
message RoomTreasureRewardGrant {
|
||||
// RoomRocketRewardGrant 是一次火箭发射后给某个用户结算出的具体奖励。
|
||||
message RoomRocketRewardGrant {
|
||||
string reward_role = 1;
|
||||
int64 user_id = 2;
|
||||
string reward_item_id = 3;
|
||||
@ -134,80 +136,95 @@ message RoomTreasureRewardGrant {
|
||||
string status = 8;
|
||||
}
|
||||
|
||||
// RoomTreasureState 是 Room Cell 持有并随快照恢复的当前宝箱状态。
|
||||
message RoomTreasureState {
|
||||
// RoomRocketPendingLaunch 是已经点火、等待延迟发射的单枚火箭。
|
||||
message RoomRocketPendingLaunch {
|
||||
string rocket_id = 1;
|
||||
int32 level = 2;
|
||||
int64 fuel_threshold = 3;
|
||||
int64 ignited_at_ms = 4;
|
||||
int64 launch_at_ms = 5;
|
||||
int64 reset_at_ms = 6;
|
||||
int64 top1_user_id = 7;
|
||||
int64 igniter_user_id = 8;
|
||||
int64 config_version = 9;
|
||||
string cover_url = 10;
|
||||
}
|
||||
|
||||
// RoomRocketState 是 Room Cell 持有并随快照恢复的当前火箭状态。
|
||||
message RoomRocketState {
|
||||
int32 current_level = 1;
|
||||
int64 current_progress = 2;
|
||||
int64 energy_threshold = 3;
|
||||
int64 current_fuel = 2;
|
||||
int64 fuel_threshold = 3;
|
||||
string status = 4;
|
||||
int64 countdown_started_at_ms = 5;
|
||||
int64 open_at_ms = 6;
|
||||
int64 opened_at_ms = 7;
|
||||
int64 ignited_at_ms = 5;
|
||||
int64 launch_at_ms = 6;
|
||||
int64 launched_at_ms = 7;
|
||||
int64 reset_at_ms = 8;
|
||||
int64 top1_user_id = 9;
|
||||
int64 igniter_user_id = 10;
|
||||
string box_id = 11;
|
||||
string rocket_id = 11;
|
||||
int64 config_version = 12;
|
||||
repeated RoomTreasureRewardGrant last_rewards = 13;
|
||||
repeated RoomRocketRewardGrant last_rewards = 13;
|
||||
repeated RoomRocketPendingLaunch pending_launches = 14;
|
||||
}
|
||||
|
||||
// RoomTreasureInfo 是 App 初始化宝箱 UI 的完整读模型。
|
||||
message RoomTreasureInfo {
|
||||
// RoomRocketInfo 是 App 初始化火箭 UI 的完整读模型。
|
||||
message RoomRocketInfo {
|
||||
bool enabled = 1;
|
||||
repeated RoomTreasureLevel levels = 2;
|
||||
RoomTreasureState state = 3;
|
||||
repeated RoomRocketLevel levels = 2;
|
||||
RoomRocketState state = 3;
|
||||
int64 server_time_ms = 4;
|
||||
string broadcast_scope = 5;
|
||||
int64 open_delay_ms = 6;
|
||||
int64 launch_delay_ms = 6;
|
||||
int64 broadcast_delay_ms = 7;
|
||||
string reward_stack_policy = 8;
|
||||
}
|
||||
|
||||
// RoomTreasureGiftEnergyRule 是后台配置的礼物能量覆盖规则。
|
||||
message RoomTreasureGiftEnergyRule {
|
||||
// RoomRocketGiftFuelRule 是后台配置的礼物燃料覆盖规则。
|
||||
message RoomRocketGiftFuelRule {
|
||||
string rule_id = 1;
|
||||
string gift_id = 2;
|
||||
string gift_type_code = 3;
|
||||
int64 multiplier_ppm = 4;
|
||||
int64 fixed_energy = 5;
|
||||
int64 fixed_fuel = 5;
|
||||
bool excluded = 6;
|
||||
}
|
||||
|
||||
// AdminRoomTreasureConfig 是后台管理读写的完整宝箱配置。
|
||||
message AdminRoomTreasureConfig {
|
||||
// AdminRoomRocketConfig 是后台管理读写的完整火箭配置。
|
||||
message AdminRoomRocketConfig {
|
||||
string app_code = 1;
|
||||
bool enabled = 2;
|
||||
int64 config_version = 3;
|
||||
string energy_source = 4;
|
||||
int64 open_delay_ms = 5;
|
||||
string fuel_source = 4;
|
||||
int64 launch_delay_ms = 5;
|
||||
bool broadcast_enabled = 6;
|
||||
string broadcast_scope = 7;
|
||||
int64 broadcast_delay_ms = 8;
|
||||
string reward_stack_policy = 9;
|
||||
repeated RoomTreasureLevel levels = 10;
|
||||
repeated RoomTreasureGiftEnergyRule gift_energy_rules = 11;
|
||||
repeated RoomRocketLevel levels = 10;
|
||||
repeated RoomRocketGiftFuelRule gift_fuel_rules = 11;
|
||||
int64 updated_by_admin_id = 12;
|
||||
int64 created_at_ms = 13;
|
||||
int64 updated_at_ms = 14;
|
||||
}
|
||||
|
||||
message AdminGetRoomTreasureConfigRequest {
|
||||
message AdminGetRoomRocketConfigRequest {
|
||||
RequestMeta meta = 1;
|
||||
}
|
||||
|
||||
message AdminGetRoomTreasureConfigResponse {
|
||||
AdminRoomTreasureConfig config = 1;
|
||||
message AdminGetRoomRocketConfigResponse {
|
||||
AdminRoomRocketConfig config = 1;
|
||||
int64 server_time_ms = 2;
|
||||
}
|
||||
|
||||
message AdminUpdateRoomTreasureConfigRequest {
|
||||
message AdminUpdateRoomRocketConfigRequest {
|
||||
RequestMeta meta = 1;
|
||||
AdminRoomTreasureConfig config = 2;
|
||||
AdminRoomRocketConfig config = 2;
|
||||
int64 admin_id = 3;
|
||||
}
|
||||
|
||||
message AdminUpdateRoomTreasureConfigResponse {
|
||||
AdminRoomTreasureConfig config = 1;
|
||||
message AdminUpdateRoomRocketConfigResponse {
|
||||
AdminRoomRocketConfig config = 1;
|
||||
int64 server_time_ms = 2;
|
||||
}
|
||||
|
||||
@ -262,6 +279,7 @@ message AdminRoomPin {
|
||||
int64 created_at_ms = 11;
|
||||
int64 updated_at_ms = 12;
|
||||
AdminRoomPinRoom room = 13;
|
||||
string pin_type = 14;
|
||||
}
|
||||
|
||||
message AdminListRoomPinsRequest {
|
||||
@ -271,6 +289,7 @@ message AdminListRoomPinsRequest {
|
||||
string keyword = 4;
|
||||
string status = 5;
|
||||
int64 visible_region_id = 6;
|
||||
string pin_type = 7;
|
||||
}
|
||||
|
||||
message AdminListRoomPinsResponse {
|
||||
@ -285,6 +304,9 @@ message AdminCreateRoomPinRequest {
|
||||
int64 weight = 3;
|
||||
int64 duration_days = 4;
|
||||
uint64 admin_id = 5;
|
||||
int64 pinned_at_ms = 6;
|
||||
int64 expires_at_ms = 7;
|
||||
string pin_type = 8;
|
||||
}
|
||||
|
||||
message AdminCreateRoomPinResponse {
|
||||
@ -333,8 +355,8 @@ message RoomSnapshot {
|
||||
int64 visible_region_id = 18;
|
||||
// locked 只表达房间是否需要入房密码;具体密码哈希不对外映射到 HTTP JSON。
|
||||
bool locked = 19;
|
||||
// treasure 是语音房宝箱当前状态;配置物料通过 GetRoomTreasure 单独读取,避免快照过大。
|
||||
RoomTreasureState treasure = 20;
|
||||
// rocket 是语音房火箭当前状态;配置物料通过 GetRoomRocket 单独读取,避免快照过大。
|
||||
RoomRocketState rocket = 20;
|
||||
// ban_states 保存 ban 的过期边界;ban_user_ids 仍只服务简单集合判断。
|
||||
repeated RoomBanState ban_states = 21;
|
||||
}
|
||||
@ -385,7 +407,7 @@ message SetRoomBackgroundResponse {
|
||||
}
|
||||
|
||||
// CreateRoomRequest 创建一个新的房间执行单元。
|
||||
// 必填语义:meta.room_id、meta.command_id、meta.actor_user_id、mode 和 room_name。
|
||||
// 必填语义:meta.room_id、meta.command_id、meta.actor_user_id、mode、room_name 和 room_avatar。
|
||||
// 同一个 meta.actor_user_id 只能作为 owner 创建一个房间;重复创建返回 Conflict。
|
||||
message CreateRoomRequest {
|
||||
RequestMeta meta = 1;
|
||||
@ -397,7 +419,7 @@ message CreateRoomRequest {
|
||||
int64 visible_region_id = 4;
|
||||
// room_name 是客户端创建房间时填写的展示名称,room-service 会写入 RoomSnapshot.room_ext["title"]。
|
||||
string room_name = 5;
|
||||
// room_avatar 是客户端可选上传的展示头像;为空时 room-service 写入默认系统头像。
|
||||
// room_avatar 是客户端上传的房间封面;创建时必填,room-service 会写入 RoomSnapshot.room_ext["cover_url"]。
|
||||
string room_avatar = 6;
|
||||
// room_description 是房间简介,只作为房间扩展资料保存,不参与 Room Cell 高频状态决策。
|
||||
string room_description = 7;
|
||||
@ -446,6 +468,8 @@ message JoinRoomRequest {
|
||||
// password 只用于本次入房校验;room-service 不把明文写入 command log 或快照。
|
||||
string password = 3;
|
||||
RoomEntryVehicleSnapshot entry_vehicle = 4;
|
||||
int64 actor_country_id = 5;
|
||||
int64 actor_region_id = 6;
|
||||
}
|
||||
|
||||
// JoinRoomResponse 返回加入后的房间快照。
|
||||
@ -523,6 +547,7 @@ message AdminListRoomsRequest {
|
||||
int64 visible_region_id = 6;
|
||||
string sort_by = 7;
|
||||
string sort_direction = 8;
|
||||
int64 owner_user_id = 9;
|
||||
}
|
||||
|
||||
message AdminListRoomsResponse {
|
||||
@ -632,6 +657,23 @@ message ConfirmMicPublishingResponse {
|
||||
RoomSnapshot room = 3;
|
||||
}
|
||||
|
||||
// MicHeartbeatRequest 刷新当前 publishing 麦位会话的服务端麦上心跳。
|
||||
message MicHeartbeatRequest {
|
||||
RequestMeta meta = 1;
|
||||
// target_user_id 为空时默认刷新 actor_user_id 自己的麦上会话。
|
||||
int64 target_user_id = 2;
|
||||
// mic_session_id 必须匹配当前麦位上的会话,避免旧心跳续住新会话。
|
||||
string mic_session_id = 3;
|
||||
}
|
||||
|
||||
// MicHeartbeatResponse 返回刷新后的麦位和房间快照。
|
||||
message MicHeartbeatResponse {
|
||||
CommandResult result = 1;
|
||||
int32 seat_no = 2;
|
||||
RoomSnapshot room = 3;
|
||||
int64 mic_heartbeat_at_ms = 4;
|
||||
}
|
||||
|
||||
// SetMicMuteRequest 修改麦位上的服务端可见静音态。
|
||||
message SetMicMuteRequest {
|
||||
RequestMeta meta = 1;
|
||||
@ -801,6 +843,8 @@ message SendGiftRequest {
|
||||
int64 target_agency_owner_user_id = 10;
|
||||
// sender_region_id 是 gateway 从 user-service 当前送礼用户资料注入的区域,用于 wallet 匹配礼物钻石比例。
|
||||
int64 sender_region_id = 11;
|
||||
// sender_country_id 是 gateway 从 user-service 当前送礼用户资料注入的真实国家,用于统计国家维度。
|
||||
int64 sender_country_id = 12;
|
||||
}
|
||||
|
||||
// SendGiftResponse 返回扣费成功并落到房间后的状态结果。
|
||||
@ -810,7 +854,7 @@ message SendGiftResponse {
|
||||
int64 room_heat = 3;
|
||||
repeated RankItem gift_rank = 4;
|
||||
RoomSnapshot room = 5;
|
||||
RoomTreasureState treasure = 6;
|
||||
RoomRocketState rocket = 6;
|
||||
// lucky_gift 是本次送礼的聚合幸运礼物结果;多目标时倍率和返奖金额按所有目标累加。
|
||||
LuckyGiftDrawResult lucky_gift = 7;
|
||||
// lucky_gifts 是每个收礼目标的独立抽奖结果;多目标客户端用 target_user_id 对齐明细表现。
|
||||
@ -981,16 +1025,16 @@ message GetRoomSnapshotResponse {
|
||||
bool is_followed = 3;
|
||||
}
|
||||
|
||||
// GetRoomTreasureRequest 主动读取当前房间宝箱配置物料和进度状态。
|
||||
// GetRoomRocketRequest 主动读取当前房间火箭配置物料和进度状态。
|
||||
// 该查询必须由 gateway 写入鉴权后的 viewer_user_id,客户端不能提交 viewer_user_id。
|
||||
message GetRoomTreasureRequest {
|
||||
message GetRoomRocketRequest {
|
||||
RequestMeta meta = 1;
|
||||
string room_id = 2;
|
||||
int64 viewer_user_id = 3;
|
||||
}
|
||||
|
||||
message GetRoomTreasureResponse {
|
||||
RoomTreasureInfo treasure = 1;
|
||||
message GetRoomRocketResponse {
|
||||
RoomRocketInfo rocket = 1;
|
||||
int64 server_time_ms = 2;
|
||||
}
|
||||
|
||||
@ -1081,7 +1125,7 @@ service RoomCommandService {
|
||||
rpc CloseRoom(CloseRoomRequest) returns (CloseRoomResponse);
|
||||
rpc AdminUpdateRoom(AdminUpdateRoomRequest) returns (AdminUpdateRoomResponse);
|
||||
rpc AdminDeleteRoom(AdminDeleteRoomRequest) returns (AdminDeleteRoomResponse);
|
||||
rpc AdminUpdateRoomTreasureConfig(AdminUpdateRoomTreasureConfigRequest) returns (AdminUpdateRoomTreasureConfigResponse);
|
||||
rpc AdminUpdateRoomRocketConfig(AdminUpdateRoomRocketConfigRequest) returns (AdminUpdateRoomRocketConfigResponse);
|
||||
rpc AdminUpdateRoomSeatConfig(AdminUpdateRoomSeatConfigRequest) returns (AdminUpdateRoomSeatConfigResponse);
|
||||
rpc AdminCreateRoomPin(AdminCreateRoomPinRequest) returns (AdminCreateRoomPinResponse);
|
||||
rpc AdminCancelRoomPin(AdminCancelRoomPinRequest) returns (AdminCancelRoomPinResponse);
|
||||
@ -1089,6 +1133,7 @@ service RoomCommandService {
|
||||
rpc MicDown(MicDownRequest) returns (MicDownResponse);
|
||||
rpc ChangeMicSeat(ChangeMicSeatRequest) returns (ChangeMicSeatResponse);
|
||||
rpc ConfirmMicPublishing(ConfirmMicPublishingRequest) returns (ConfirmMicPublishingResponse);
|
||||
rpc MicHeartbeat(MicHeartbeatRequest) returns (MicHeartbeatResponse);
|
||||
rpc SetMicMute(SetMicMuteRequest) returns (SetMicMuteResponse);
|
||||
rpc ApplyRTCEvent(ApplyRTCEventRequest) returns (ApplyRTCEventResponse);
|
||||
rpc SetMicSeatLock(SetMicSeatLockRequest) returns (SetMicSeatLockResponse);
|
||||
@ -1114,7 +1159,7 @@ service RoomGuardService {
|
||||
service RoomQueryService {
|
||||
rpc AdminListRooms(AdminListRoomsRequest) returns (AdminListRoomsResponse);
|
||||
rpc AdminGetRoom(AdminGetRoomRequest) returns (AdminGetRoomResponse);
|
||||
rpc AdminGetRoomTreasureConfig(AdminGetRoomTreasureConfigRequest) returns (AdminGetRoomTreasureConfigResponse);
|
||||
rpc AdminGetRoomRocketConfig(AdminGetRoomRocketConfigRequest) returns (AdminGetRoomRocketConfigResponse);
|
||||
rpc AdminGetRoomSeatConfig(AdminGetRoomSeatConfigRequest) returns (AdminGetRoomSeatConfigResponse);
|
||||
rpc AdminListRoomPins(AdminListRoomPinsRequest) returns (AdminListRoomPinsResponse);
|
||||
rpc ListRooms(ListRoomsRequest) returns (ListRoomsResponse);
|
||||
@ -1124,7 +1169,7 @@ service RoomQueryService {
|
||||
rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse);
|
||||
rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse);
|
||||
rpc ListRoomBackgrounds(ListRoomBackgroundsRequest) returns (ListRoomBackgroundsResponse);
|
||||
rpc GetRoomTreasure(GetRoomTreasureRequest) returns (GetRoomTreasureResponse);
|
||||
rpc GetRoomRocket(GetRoomRocketRequest) returns (GetRoomRocketResponse);
|
||||
rpc ListRoomOnlineUsers(ListRoomOnlineUsersRequest) returns (ListRoomOnlineUsersResponse);
|
||||
rpc ListRoomBannedUsers(ListRoomBannedUsersRequest) returns (ListRoomBannedUsersResponse);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v7.35.0
|
||||
// source: proto/room/v1/room.proto
|
||||
|
||||
@ -19,37 +19,38 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
RoomCommandService_CreateRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/CreateRoom"
|
||||
RoomCommandService_UpdateRoomProfile_FullMethodName = "/hyapp.room.v1.RoomCommandService/UpdateRoomProfile"
|
||||
RoomCommandService_SaveRoomBackground_FullMethodName = "/hyapp.room.v1.RoomCommandService/SaveRoomBackground"
|
||||
RoomCommandService_SetRoomBackground_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetRoomBackground"
|
||||
RoomCommandService_JoinRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/JoinRoom"
|
||||
RoomCommandService_RoomHeartbeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/RoomHeartbeat"
|
||||
RoomCommandService_LeaveRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/LeaveRoom"
|
||||
RoomCommandService_CloseRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/CloseRoom"
|
||||
RoomCommandService_AdminUpdateRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoom"
|
||||
RoomCommandService_AdminDeleteRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminDeleteRoom"
|
||||
RoomCommandService_AdminUpdateRoomTreasureConfig_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoomTreasureConfig"
|
||||
RoomCommandService_AdminUpdateRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoomSeatConfig"
|
||||
RoomCommandService_AdminCreateRoomPin_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCreateRoomPin"
|
||||
RoomCommandService_AdminCancelRoomPin_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCancelRoomPin"
|
||||
RoomCommandService_MicUp_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicUp"
|
||||
RoomCommandService_MicDown_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicDown"
|
||||
RoomCommandService_ChangeMicSeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/ChangeMicSeat"
|
||||
RoomCommandService_ConfirmMicPublishing_FullMethodName = "/hyapp.room.v1.RoomCommandService/ConfirmMicPublishing"
|
||||
RoomCommandService_SetMicMute_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicMute"
|
||||
RoomCommandService_ApplyRTCEvent_FullMethodName = "/hyapp.room.v1.RoomCommandService/ApplyRTCEvent"
|
||||
RoomCommandService_SetMicSeatLock_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicSeatLock"
|
||||
RoomCommandService_SetChatEnabled_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetChatEnabled"
|
||||
RoomCommandService_SetRoomPassword_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetRoomPassword"
|
||||
RoomCommandService_SetRoomAdmin_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetRoomAdmin"
|
||||
RoomCommandService_MuteUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/MuteUser"
|
||||
RoomCommandService_KickUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/KickUser"
|
||||
RoomCommandService_UnbanUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/UnbanUser"
|
||||
RoomCommandService_SystemEvictUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/SystemEvictUser"
|
||||
RoomCommandService_SendGift_FullMethodName = "/hyapp.room.v1.RoomCommandService/SendGift"
|
||||
RoomCommandService_FollowRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/FollowRoom"
|
||||
RoomCommandService_UnfollowRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/UnfollowRoom"
|
||||
RoomCommandService_CreateRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/CreateRoom"
|
||||
RoomCommandService_UpdateRoomProfile_FullMethodName = "/hyapp.room.v1.RoomCommandService/UpdateRoomProfile"
|
||||
RoomCommandService_SaveRoomBackground_FullMethodName = "/hyapp.room.v1.RoomCommandService/SaveRoomBackground"
|
||||
RoomCommandService_SetRoomBackground_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetRoomBackground"
|
||||
RoomCommandService_JoinRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/JoinRoom"
|
||||
RoomCommandService_RoomHeartbeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/RoomHeartbeat"
|
||||
RoomCommandService_LeaveRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/LeaveRoom"
|
||||
RoomCommandService_CloseRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/CloseRoom"
|
||||
RoomCommandService_AdminUpdateRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoom"
|
||||
RoomCommandService_AdminDeleteRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminDeleteRoom"
|
||||
RoomCommandService_AdminUpdateRoomRocketConfig_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoomRocketConfig"
|
||||
RoomCommandService_AdminUpdateRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoomSeatConfig"
|
||||
RoomCommandService_AdminCreateRoomPin_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCreateRoomPin"
|
||||
RoomCommandService_AdminCancelRoomPin_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCancelRoomPin"
|
||||
RoomCommandService_MicUp_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicUp"
|
||||
RoomCommandService_MicDown_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicDown"
|
||||
RoomCommandService_ChangeMicSeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/ChangeMicSeat"
|
||||
RoomCommandService_ConfirmMicPublishing_FullMethodName = "/hyapp.room.v1.RoomCommandService/ConfirmMicPublishing"
|
||||
RoomCommandService_MicHeartbeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicHeartbeat"
|
||||
RoomCommandService_SetMicMute_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicMute"
|
||||
RoomCommandService_ApplyRTCEvent_FullMethodName = "/hyapp.room.v1.RoomCommandService/ApplyRTCEvent"
|
||||
RoomCommandService_SetMicSeatLock_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicSeatLock"
|
||||
RoomCommandService_SetChatEnabled_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetChatEnabled"
|
||||
RoomCommandService_SetRoomPassword_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetRoomPassword"
|
||||
RoomCommandService_SetRoomAdmin_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetRoomAdmin"
|
||||
RoomCommandService_MuteUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/MuteUser"
|
||||
RoomCommandService_KickUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/KickUser"
|
||||
RoomCommandService_UnbanUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/UnbanUser"
|
||||
RoomCommandService_SystemEvictUser_FullMethodName = "/hyapp.room.v1.RoomCommandService/SystemEvictUser"
|
||||
RoomCommandService_SendGift_FullMethodName = "/hyapp.room.v1.RoomCommandService/SendGift"
|
||||
RoomCommandService_FollowRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/FollowRoom"
|
||||
RoomCommandService_UnfollowRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/UnfollowRoom"
|
||||
)
|
||||
|
||||
// RoomCommandServiceClient is the client API for RoomCommandService service.
|
||||
@ -68,7 +69,7 @@ type RoomCommandServiceClient interface {
|
||||
CloseRoom(ctx context.Context, in *CloseRoomRequest, opts ...grpc.CallOption) (*CloseRoomResponse, error)
|
||||
AdminUpdateRoom(ctx context.Context, in *AdminUpdateRoomRequest, opts ...grpc.CallOption) (*AdminUpdateRoomResponse, error)
|
||||
AdminDeleteRoom(ctx context.Context, in *AdminDeleteRoomRequest, opts ...grpc.CallOption) (*AdminDeleteRoomResponse, error)
|
||||
AdminUpdateRoomTreasureConfig(ctx context.Context, in *AdminUpdateRoomTreasureConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomTreasureConfigResponse, error)
|
||||
AdminUpdateRoomRocketConfig(ctx context.Context, in *AdminUpdateRoomRocketConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomRocketConfigResponse, error)
|
||||
AdminUpdateRoomSeatConfig(ctx context.Context, in *AdminUpdateRoomSeatConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomSeatConfigResponse, error)
|
||||
AdminCreateRoomPin(ctx context.Context, in *AdminCreateRoomPinRequest, opts ...grpc.CallOption) (*AdminCreateRoomPinResponse, error)
|
||||
AdminCancelRoomPin(ctx context.Context, in *AdminCancelRoomPinRequest, opts ...grpc.CallOption) (*AdminCancelRoomPinResponse, error)
|
||||
@ -76,6 +77,7 @@ type RoomCommandServiceClient interface {
|
||||
MicDown(ctx context.Context, in *MicDownRequest, opts ...grpc.CallOption) (*MicDownResponse, error)
|
||||
ChangeMicSeat(ctx context.Context, in *ChangeMicSeatRequest, opts ...grpc.CallOption) (*ChangeMicSeatResponse, error)
|
||||
ConfirmMicPublishing(ctx context.Context, in *ConfirmMicPublishingRequest, opts ...grpc.CallOption) (*ConfirmMicPublishingResponse, error)
|
||||
MicHeartbeat(ctx context.Context, in *MicHeartbeatRequest, opts ...grpc.CallOption) (*MicHeartbeatResponse, error)
|
||||
SetMicMute(ctx context.Context, in *SetMicMuteRequest, opts ...grpc.CallOption) (*SetMicMuteResponse, error)
|
||||
ApplyRTCEvent(ctx context.Context, in *ApplyRTCEventRequest, opts ...grpc.CallOption) (*ApplyRTCEventResponse, error)
|
||||
SetMicSeatLock(ctx context.Context, in *SetMicSeatLockRequest, opts ...grpc.CallOption) (*SetMicSeatLockResponse, error)
|
||||
@ -199,10 +201,10 @@ func (c *roomCommandServiceClient) AdminDeleteRoom(ctx context.Context, in *Admi
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *roomCommandServiceClient) AdminUpdateRoomTreasureConfig(ctx context.Context, in *AdminUpdateRoomTreasureConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomTreasureConfigResponse, error) {
|
||||
func (c *roomCommandServiceClient) AdminUpdateRoomRocketConfig(ctx context.Context, in *AdminUpdateRoomRocketConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomRocketConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AdminUpdateRoomTreasureConfigResponse)
|
||||
err := c.cc.Invoke(ctx, RoomCommandService_AdminUpdateRoomTreasureConfig_FullMethodName, in, out, cOpts...)
|
||||
out := new(AdminUpdateRoomRocketConfigResponse)
|
||||
err := c.cc.Invoke(ctx, RoomCommandService_AdminUpdateRoomRocketConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -279,6 +281,16 @@ func (c *roomCommandServiceClient) ConfirmMicPublishing(ctx context.Context, in
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *roomCommandServiceClient) MicHeartbeat(ctx context.Context, in *MicHeartbeatRequest, opts ...grpc.CallOption) (*MicHeartbeatResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(MicHeartbeatResponse)
|
||||
err := c.cc.Invoke(ctx, RoomCommandService_MicHeartbeat_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *roomCommandServiceClient) SetMicMute(ctx context.Context, in *SetMicMuteRequest, opts ...grpc.CallOption) (*SetMicMuteResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SetMicMuteResponse)
|
||||
@ -425,7 +437,7 @@ type RoomCommandServiceServer interface {
|
||||
CloseRoom(context.Context, *CloseRoomRequest) (*CloseRoomResponse, error)
|
||||
AdminUpdateRoom(context.Context, *AdminUpdateRoomRequest) (*AdminUpdateRoomResponse, error)
|
||||
AdminDeleteRoom(context.Context, *AdminDeleteRoomRequest) (*AdminDeleteRoomResponse, error)
|
||||
AdminUpdateRoomTreasureConfig(context.Context, *AdminUpdateRoomTreasureConfigRequest) (*AdminUpdateRoomTreasureConfigResponse, error)
|
||||
AdminUpdateRoomRocketConfig(context.Context, *AdminUpdateRoomRocketConfigRequest) (*AdminUpdateRoomRocketConfigResponse, error)
|
||||
AdminUpdateRoomSeatConfig(context.Context, *AdminUpdateRoomSeatConfigRequest) (*AdminUpdateRoomSeatConfigResponse, error)
|
||||
AdminCreateRoomPin(context.Context, *AdminCreateRoomPinRequest) (*AdminCreateRoomPinResponse, error)
|
||||
AdminCancelRoomPin(context.Context, *AdminCancelRoomPinRequest) (*AdminCancelRoomPinResponse, error)
|
||||
@ -433,6 +445,7 @@ type RoomCommandServiceServer interface {
|
||||
MicDown(context.Context, *MicDownRequest) (*MicDownResponse, error)
|
||||
ChangeMicSeat(context.Context, *ChangeMicSeatRequest) (*ChangeMicSeatResponse, error)
|
||||
ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error)
|
||||
MicHeartbeat(context.Context, *MicHeartbeatRequest) (*MicHeartbeatResponse, error)
|
||||
SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error)
|
||||
ApplyRTCEvent(context.Context, *ApplyRTCEventRequest) (*ApplyRTCEventResponse, error)
|
||||
SetMicSeatLock(context.Context, *SetMicSeatLockRequest) (*SetMicSeatLockResponse, error)
|
||||
@ -457,97 +470,100 @@ type RoomCommandServiceServer interface {
|
||||
type UnimplementedRoomCommandServiceServer struct{}
|
||||
|
||||
func (UnimplementedRoomCommandServiceServer) CreateRoom(context.Context, *CreateRoomRequest) (*CreateRoomResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateRoom not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateRoomProfile not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRoomProfile not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SaveRoomBackground(context.Context, *SaveRoomBackgroundRequest) (*SaveRoomBackgroundResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SaveRoomBackground not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SaveRoomBackground not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SetRoomBackground(context.Context, *SetRoomBackgroundRequest) (*SetRoomBackgroundResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetRoomBackground not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetRoomBackground not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method JoinRoom not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method JoinRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) RoomHeartbeat(context.Context, *RoomHeartbeatRequest) (*RoomHeartbeatResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method RoomHeartbeat not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RoomHeartbeat not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) LeaveRoom(context.Context, *LeaveRoomRequest) (*LeaveRoomResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method LeaveRoom not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method LeaveRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) CloseRoom(context.Context, *CloseRoomRequest) (*CloseRoomResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CloseRoom not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CloseRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminUpdateRoom(context.Context, *AdminUpdateRoomRequest) (*AdminUpdateRoomResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoom not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminUpdateRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminDeleteRoom(context.Context, *AdminDeleteRoomRequest) (*AdminDeleteRoomResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminDeleteRoom not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminDeleteRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminUpdateRoomTreasureConfig(context.Context, *AdminUpdateRoomTreasureConfigRequest) (*AdminUpdateRoomTreasureConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoomTreasureConfig not implemented")
|
||||
func (UnimplementedRoomCommandServiceServer) AdminUpdateRoomRocketConfig(context.Context, *AdminUpdateRoomRocketConfigRequest) (*AdminUpdateRoomRocketConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminUpdateRoomRocketConfig not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminUpdateRoomSeatConfig(context.Context, *AdminUpdateRoomSeatConfigRequest) (*AdminUpdateRoomSeatConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoomSeatConfig not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminUpdateRoomSeatConfig not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminCreateRoomPin(context.Context, *AdminCreateRoomPinRequest) (*AdminCreateRoomPinResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminCreateRoomPin not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminCreateRoomPin not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminCancelRoomPin(context.Context, *AdminCancelRoomPinRequest) (*AdminCancelRoomPinResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminCancelRoomPin not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminCancelRoomPin not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) MicUp(context.Context, *MicUpRequest) (*MicUpResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method MicUp not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method MicUp not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) MicDown(context.Context, *MicDownRequest) (*MicDownResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method MicDown not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method MicDown not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) ChangeMicSeat(context.Context, *ChangeMicSeatRequest) (*ChangeMicSeatResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ChangeMicSeat not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ChangeMicSeat not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ConfirmMicPublishing not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ConfirmMicPublishing not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) MicHeartbeat(context.Context, *MicHeartbeatRequest) (*MicHeartbeatResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method MicHeartbeat not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetMicMute not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetMicMute not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) ApplyRTCEvent(context.Context, *ApplyRTCEventRequest) (*ApplyRTCEventResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ApplyRTCEvent not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ApplyRTCEvent not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SetMicSeatLock(context.Context, *SetMicSeatLockRequest) (*SetMicSeatLockResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetMicSeatLock not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetMicSeatLock not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SetChatEnabled(context.Context, *SetChatEnabledRequest) (*SetChatEnabledResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetChatEnabled not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetChatEnabled not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SetRoomPassword(context.Context, *SetRoomPasswordRequest) (*SetRoomPasswordResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetRoomPassword not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetRoomPassword not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SetRoomAdmin(context.Context, *SetRoomAdminRequest) (*SetRoomAdminResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetRoomAdmin not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetRoomAdmin not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) MuteUser(context.Context, *MuteUserRequest) (*MuteUserResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method MuteUser not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method MuteUser not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) KickUser(context.Context, *KickUserRequest) (*KickUserResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method KickUser not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method KickUser not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) UnbanUser(context.Context, *UnbanUserRequest) (*UnbanUserResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UnbanUser not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UnbanUser not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SystemEvictUser(context.Context, *SystemEvictUserRequest) (*SystemEvictUserResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SystemEvictUser not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SystemEvictUser not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) SendGift(context.Context, *SendGiftRequest) (*SendGiftResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SendGift not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SendGift not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) FollowRoom(context.Context, *FollowRoomRequest) (*FollowRoomResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method FollowRoom not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method FollowRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) UnfollowRoom(context.Context, *UnfollowRoomRequest) (*UnfollowRoomResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UnfollowRoom not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UnfollowRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) mustEmbedUnimplementedRoomCommandServiceServer() {}
|
||||
func (UnimplementedRoomCommandServiceServer) testEmbeddedByValue() {}
|
||||
@ -560,7 +576,7 @@ type UnsafeRoomCommandServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterRoomCommandServiceServer(s grpc.ServiceRegistrar, srv RoomCommandServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedRoomCommandServiceServer was
|
||||
// If the following call pancis, it indicates UnimplementedRoomCommandServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -750,20 +766,20 @@ func _RoomCommandService_AdminDeleteRoom_Handler(srv interface{}, ctx context.Co
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RoomCommandService_AdminUpdateRoomTreasureConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminUpdateRoomTreasureConfigRequest)
|
||||
func _RoomCommandService_AdminUpdateRoomRocketConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminUpdateRoomRocketConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RoomCommandServiceServer).AdminUpdateRoomTreasureConfig(ctx, in)
|
||||
return srv.(RoomCommandServiceServer).AdminUpdateRoomRocketConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: RoomCommandService_AdminUpdateRoomTreasureConfig_FullMethodName,
|
||||
FullMethod: RoomCommandService_AdminUpdateRoomRocketConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RoomCommandServiceServer).AdminUpdateRoomTreasureConfig(ctx, req.(*AdminUpdateRoomTreasureConfigRequest))
|
||||
return srv.(RoomCommandServiceServer).AdminUpdateRoomRocketConfig(ctx, req.(*AdminUpdateRoomRocketConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
@ -894,6 +910,24 @@ func _RoomCommandService_ConfirmMicPublishing_Handler(srv interface{}, ctx conte
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RoomCommandService_MicHeartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(MicHeartbeatRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RoomCommandServiceServer).MicHeartbeat(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: RoomCommandService_MicHeartbeat_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RoomCommandServiceServer).MicHeartbeat(ctx, req.(*MicHeartbeatRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RoomCommandService_SetMicMute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SetMicMuteRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -1176,8 +1210,8 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
|
||||
Handler: _RoomCommandService_AdminDeleteRoom_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminUpdateRoomTreasureConfig",
|
||||
Handler: _RoomCommandService_AdminUpdateRoomTreasureConfig_Handler,
|
||||
MethodName: "AdminUpdateRoomRocketConfig",
|
||||
Handler: _RoomCommandService_AdminUpdateRoomRocketConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminUpdateRoomSeatConfig",
|
||||
@ -1207,6 +1241,10 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ConfirmMicPublishing",
|
||||
Handler: _RoomCommandService_ConfirmMicPublishing_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "MicHeartbeat",
|
||||
Handler: _RoomCommandService_MicHeartbeat_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SetMicMute",
|
||||
Handler: _RoomCommandService_SetMicMute_Handler,
|
||||
@ -1326,10 +1364,10 @@ type RoomGuardServiceServer interface {
|
||||
type UnimplementedRoomGuardServiceServer struct{}
|
||||
|
||||
func (UnimplementedRoomGuardServiceServer) CheckSpeakPermission(context.Context, *CheckSpeakPermissionRequest) (*CheckSpeakPermissionResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CheckSpeakPermission not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CheckSpeakPermission not implemented")
|
||||
}
|
||||
func (UnimplementedRoomGuardServiceServer) VerifyRoomPresence(context.Context, *VerifyRoomPresenceRequest) (*VerifyRoomPresenceResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method VerifyRoomPresence not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method VerifyRoomPresence not implemented")
|
||||
}
|
||||
func (UnimplementedRoomGuardServiceServer) mustEmbedUnimplementedRoomGuardServiceServer() {}
|
||||
func (UnimplementedRoomGuardServiceServer) testEmbeddedByValue() {}
|
||||
@ -1342,7 +1380,7 @@ type UnsafeRoomGuardServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterRoomGuardServiceServer(s grpc.ServiceRegistrar, srv RoomGuardServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedRoomGuardServiceServer was
|
||||
// If the following call pancis, it indicates UnimplementedRoomGuardServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -1409,21 +1447,21 @@ var RoomGuardService_ServiceDesc = grpc.ServiceDesc{
|
||||
}
|
||||
|
||||
const (
|
||||
RoomQueryService_AdminListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRooms"
|
||||
RoomQueryService_AdminGetRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoom"
|
||||
RoomQueryService_AdminGetRoomTreasureConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomTreasureConfig"
|
||||
RoomQueryService_AdminGetRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomSeatConfig"
|
||||
RoomQueryService_AdminListRoomPins_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRoomPins"
|
||||
RoomQueryService_ListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRooms"
|
||||
RoomQueryService_ListRoomFeeds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomFeeds"
|
||||
RoomQueryService_ListRoomGiftLeaderboard_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomGiftLeaderboard"
|
||||
RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom"
|
||||
RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom"
|
||||
RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot"
|
||||
RoomQueryService_ListRoomBackgrounds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBackgrounds"
|
||||
RoomQueryService_GetRoomTreasure_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomTreasure"
|
||||
RoomQueryService_ListRoomOnlineUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomOnlineUsers"
|
||||
RoomQueryService_ListRoomBannedUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBannedUsers"
|
||||
RoomQueryService_AdminListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRooms"
|
||||
RoomQueryService_AdminGetRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoom"
|
||||
RoomQueryService_AdminGetRoomRocketConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomRocketConfig"
|
||||
RoomQueryService_AdminGetRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomSeatConfig"
|
||||
RoomQueryService_AdminListRoomPins_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRoomPins"
|
||||
RoomQueryService_ListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRooms"
|
||||
RoomQueryService_ListRoomFeeds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomFeeds"
|
||||
RoomQueryService_ListRoomGiftLeaderboard_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomGiftLeaderboard"
|
||||
RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom"
|
||||
RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom"
|
||||
RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot"
|
||||
RoomQueryService_ListRoomBackgrounds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBackgrounds"
|
||||
RoomQueryService_GetRoomRocket_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomRocket"
|
||||
RoomQueryService_ListRoomOnlineUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomOnlineUsers"
|
||||
RoomQueryService_ListRoomBannedUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBannedUsers"
|
||||
)
|
||||
|
||||
// RoomQueryServiceClient is the client API for RoomQueryService service.
|
||||
@ -1434,7 +1472,7 @@ const (
|
||||
type RoomQueryServiceClient interface {
|
||||
AdminListRooms(ctx context.Context, in *AdminListRoomsRequest, opts ...grpc.CallOption) (*AdminListRoomsResponse, error)
|
||||
AdminGetRoom(ctx context.Context, in *AdminGetRoomRequest, opts ...grpc.CallOption) (*AdminGetRoomResponse, error)
|
||||
AdminGetRoomTreasureConfig(ctx context.Context, in *AdminGetRoomTreasureConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomTreasureConfigResponse, error)
|
||||
AdminGetRoomRocketConfig(ctx context.Context, in *AdminGetRoomRocketConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomRocketConfigResponse, error)
|
||||
AdminGetRoomSeatConfig(ctx context.Context, in *AdminGetRoomSeatConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomSeatConfigResponse, error)
|
||||
AdminListRoomPins(ctx context.Context, in *AdminListRoomPinsRequest, opts ...grpc.CallOption) (*AdminListRoomPinsResponse, error)
|
||||
ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error)
|
||||
@ -1444,7 +1482,7 @@ type RoomQueryServiceClient interface {
|
||||
GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error)
|
||||
GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, error)
|
||||
ListRoomBackgrounds(ctx context.Context, in *ListRoomBackgroundsRequest, opts ...grpc.CallOption) (*ListRoomBackgroundsResponse, error)
|
||||
GetRoomTreasure(ctx context.Context, in *GetRoomTreasureRequest, opts ...grpc.CallOption) (*GetRoomTreasureResponse, error)
|
||||
GetRoomRocket(ctx context.Context, in *GetRoomRocketRequest, opts ...grpc.CallOption) (*GetRoomRocketResponse, error)
|
||||
ListRoomOnlineUsers(ctx context.Context, in *ListRoomOnlineUsersRequest, opts ...grpc.CallOption) (*ListRoomOnlineUsersResponse, error)
|
||||
ListRoomBannedUsers(ctx context.Context, in *ListRoomBannedUsersRequest, opts ...grpc.CallOption) (*ListRoomBannedUsersResponse, error)
|
||||
}
|
||||
@ -1477,10 +1515,10 @@ func (c *roomQueryServiceClient) AdminGetRoom(ctx context.Context, in *AdminGetR
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *roomQueryServiceClient) AdminGetRoomTreasureConfig(ctx context.Context, in *AdminGetRoomTreasureConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomTreasureConfigResponse, error) {
|
||||
func (c *roomQueryServiceClient) AdminGetRoomRocketConfig(ctx context.Context, in *AdminGetRoomRocketConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomRocketConfigResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AdminGetRoomTreasureConfigResponse)
|
||||
err := c.cc.Invoke(ctx, RoomQueryService_AdminGetRoomTreasureConfig_FullMethodName, in, out, cOpts...)
|
||||
out := new(AdminGetRoomRocketConfigResponse)
|
||||
err := c.cc.Invoke(ctx, RoomQueryService_AdminGetRoomRocketConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1577,10 +1615,10 @@ func (c *roomQueryServiceClient) ListRoomBackgrounds(ctx context.Context, in *Li
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *roomQueryServiceClient) GetRoomTreasure(ctx context.Context, in *GetRoomTreasureRequest, opts ...grpc.CallOption) (*GetRoomTreasureResponse, error) {
|
||||
func (c *roomQueryServiceClient) GetRoomRocket(ctx context.Context, in *GetRoomRocketRequest, opts ...grpc.CallOption) (*GetRoomRocketResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetRoomTreasureResponse)
|
||||
err := c.cc.Invoke(ctx, RoomQueryService_GetRoomTreasure_FullMethodName, in, out, cOpts...)
|
||||
out := new(GetRoomRocketResponse)
|
||||
err := c.cc.Invoke(ctx, RoomQueryService_GetRoomRocket_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1615,7 +1653,7 @@ func (c *roomQueryServiceClient) ListRoomBannedUsers(ctx context.Context, in *Li
|
||||
type RoomQueryServiceServer interface {
|
||||
AdminListRooms(context.Context, *AdminListRoomsRequest) (*AdminListRoomsResponse, error)
|
||||
AdminGetRoom(context.Context, *AdminGetRoomRequest) (*AdminGetRoomResponse, error)
|
||||
AdminGetRoomTreasureConfig(context.Context, *AdminGetRoomTreasureConfigRequest) (*AdminGetRoomTreasureConfigResponse, error)
|
||||
AdminGetRoomRocketConfig(context.Context, *AdminGetRoomRocketConfigRequest) (*AdminGetRoomRocketConfigResponse, error)
|
||||
AdminGetRoomSeatConfig(context.Context, *AdminGetRoomSeatConfigRequest) (*AdminGetRoomSeatConfigResponse, error)
|
||||
AdminListRoomPins(context.Context, *AdminListRoomPinsRequest) (*AdminListRoomPinsResponse, error)
|
||||
ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error)
|
||||
@ -1625,7 +1663,7 @@ type RoomQueryServiceServer interface {
|
||||
GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error)
|
||||
GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error)
|
||||
ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error)
|
||||
GetRoomTreasure(context.Context, *GetRoomTreasureRequest) (*GetRoomTreasureResponse, error)
|
||||
GetRoomRocket(context.Context, *GetRoomRocketRequest) (*GetRoomRocketResponse, error)
|
||||
ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error)
|
||||
ListRoomBannedUsers(context.Context, *ListRoomBannedUsersRequest) (*ListRoomBannedUsersResponse, error)
|
||||
mustEmbedUnimplementedRoomQueryServiceServer()
|
||||
@ -1639,49 +1677,49 @@ type RoomQueryServiceServer interface {
|
||||
type UnimplementedRoomQueryServiceServer struct{}
|
||||
|
||||
func (UnimplementedRoomQueryServiceServer) AdminListRooms(context.Context, *AdminListRoomsRequest) (*AdminListRoomsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminListRooms not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminListRooms not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminGetRoom(context.Context, *AdminGetRoomRequest) (*AdminGetRoomResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminGetRoom not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminGetRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminGetRoomTreasureConfig(context.Context, *AdminGetRoomTreasureConfigRequest) (*AdminGetRoomTreasureConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminGetRoomTreasureConfig not implemented")
|
||||
func (UnimplementedRoomQueryServiceServer) AdminGetRoomRocketConfig(context.Context, *AdminGetRoomRocketConfigRequest) (*AdminGetRoomRocketConfigResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminGetRoomRocketConfig not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminGetRoomSeatConfig(context.Context, *AdminGetRoomSeatConfigRequest) (*AdminGetRoomSeatConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminGetRoomSeatConfig not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminGetRoomSeatConfig not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminListRoomPins(context.Context, *AdminListRoomPinsRequest) (*AdminListRoomPinsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminListRoomPins not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminListRoomPins not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRooms not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRooms not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomFeeds not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomFeeds not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomGiftLeaderboard(context.Context, *ListRoomGiftLeaderboardRequest) (*ListRoomGiftLeaderboardResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomGiftLeaderboard not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomGiftLeaderboard not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetMyRoom not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetMyRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetCurrentRoom not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetCurrentRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRoomSnapshot not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRoomSnapshot not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomBackgrounds not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomBackgrounds not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) GetRoomTreasure(context.Context, *GetRoomTreasureRequest) (*GetRoomTreasureResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRoomTreasure not implemented")
|
||||
func (UnimplementedRoomQueryServiceServer) GetRoomRocket(context.Context, *GetRoomRocketRequest) (*GetRoomRocketResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRoomRocket not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomOnlineUsers not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomOnlineUsers not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomBannedUsers(context.Context, *ListRoomBannedUsersRequest) (*ListRoomBannedUsersResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomBannedUsers not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRoomBannedUsers not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) mustEmbedUnimplementedRoomQueryServiceServer() {}
|
||||
func (UnimplementedRoomQueryServiceServer) testEmbeddedByValue() {}
|
||||
@ -1694,7 +1732,7 @@ type UnsafeRoomQueryServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterRoomQueryServiceServer(s grpc.ServiceRegistrar, srv RoomQueryServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedRoomQueryServiceServer was
|
||||
// If the following call pancis, it indicates UnimplementedRoomQueryServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -1740,20 +1778,20 @@ func _RoomQueryService_AdminGetRoom_Handler(srv interface{}, ctx context.Context
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RoomQueryService_AdminGetRoomTreasureConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminGetRoomTreasureConfigRequest)
|
||||
func _RoomQueryService_AdminGetRoomRocketConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminGetRoomRocketConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RoomQueryServiceServer).AdminGetRoomTreasureConfig(ctx, in)
|
||||
return srv.(RoomQueryServiceServer).AdminGetRoomRocketConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: RoomQueryService_AdminGetRoomTreasureConfig_FullMethodName,
|
||||
FullMethod: RoomQueryService_AdminGetRoomRocketConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RoomQueryServiceServer).AdminGetRoomTreasureConfig(ctx, req.(*AdminGetRoomTreasureConfigRequest))
|
||||
return srv.(RoomQueryServiceServer).AdminGetRoomRocketConfig(ctx, req.(*AdminGetRoomRocketConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
@ -1920,20 +1958,20 @@ func _RoomQueryService_ListRoomBackgrounds_Handler(srv interface{}, ctx context.
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RoomQueryService_GetRoomTreasure_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetRoomTreasureRequest)
|
||||
func _RoomQueryService_GetRoomRocket_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetRoomRocketRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RoomQueryServiceServer).GetRoomTreasure(ctx, in)
|
||||
return srv.(RoomQueryServiceServer).GetRoomRocket(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: RoomQueryService_GetRoomTreasure_FullMethodName,
|
||||
FullMethod: RoomQueryService_GetRoomRocket_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RoomQueryServiceServer).GetRoomTreasure(ctx, req.(*GetRoomTreasureRequest))
|
||||
return srv.(RoomQueryServiceServer).GetRoomRocket(ctx, req.(*GetRoomRocketRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
@ -1990,8 +2028,8 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
|
||||
Handler: _RoomQueryService_AdminGetRoom_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminGetRoomTreasureConfig",
|
||||
Handler: _RoomQueryService_AdminGetRoomTreasureConfig_Handler,
|
||||
MethodName: "AdminGetRoomRocketConfig",
|
||||
Handler: _RoomQueryService_AdminGetRoomRocketConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminGetRoomSeatConfig",
|
||||
@ -2030,8 +2068,8 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
|
||||
Handler: _RoomQueryService_ListRoomBackgrounds_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetRoomTreasure",
|
||||
Handler: _RoomQueryService_GetRoomTreasure_Handler,
|
||||
MethodName: "GetRoomRocket",
|
||||
Handler: _RoomQueryService_GetRoomRocket_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListRoomOnlineUsers",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -30,6 +30,7 @@ message Agency {
|
||||
string name = 5;
|
||||
string status = 6;
|
||||
bool join_enabled = 7;
|
||||
// Deprecated: Agency 不再限制主播数量,该字段仅为历史兼容保留。
|
||||
int32 max_hosts = 8;
|
||||
int64 created_by_user_id = 9;
|
||||
int64 created_at_ms = 10;
|
||||
@ -221,6 +222,39 @@ message InviteBDResponse {
|
||||
RoleInvitation invitation = 1;
|
||||
}
|
||||
|
||||
message ListBDLeaderBDsRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 leader_user_id = 2;
|
||||
string status = 3;
|
||||
int32 page_size = 4;
|
||||
}
|
||||
|
||||
message ListBDLeaderBDsResponse {
|
||||
repeated BDProfile bd_profiles = 1;
|
||||
}
|
||||
|
||||
message ListBDLeaderAgenciesRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 leader_user_id = 2;
|
||||
string status = 3;
|
||||
int32 page_size = 4;
|
||||
}
|
||||
|
||||
message ListBDLeaderAgenciesResponse {
|
||||
repeated Agency agencies = 1;
|
||||
}
|
||||
|
||||
message ListBDAgenciesRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 bd_user_id = 2;
|
||||
string status = 3;
|
||||
int32 page_size = 4;
|
||||
}
|
||||
|
||||
message ListBDAgenciesResponse {
|
||||
repeated Agency agencies = 1;
|
||||
}
|
||||
|
||||
message ProcessRoleInvitationRequest {
|
||||
RequestMeta meta = 1;
|
||||
string command_id = 2;
|
||||
@ -329,7 +363,6 @@ message CreateBDLeaderRequest {
|
||||
int64 admin_user_id = 3;
|
||||
int64 target_user_id = 4;
|
||||
string reason = 5;
|
||||
int64 region_id = 6;
|
||||
}
|
||||
|
||||
message CreateBDLeaderResponse {
|
||||
@ -395,6 +428,7 @@ message CreateAgencyRequest {
|
||||
int64 parent_bd_user_id = 5;
|
||||
string name = 6;
|
||||
bool join_enabled = 7;
|
||||
// Deprecated: Agency 不再限制主播数量,服务端会忽略该字段。
|
||||
int32 max_hosts = 8;
|
||||
string reason = 9;
|
||||
}
|
||||
@ -417,6 +451,18 @@ message CloseAgencyResponse {
|
||||
Agency agency = 1;
|
||||
}
|
||||
|
||||
message DeleteAgencyRequest {
|
||||
RequestMeta meta = 1;
|
||||
string command_id = 2;
|
||||
int64 admin_user_id = 3;
|
||||
int64 agency_id = 4;
|
||||
string reason = 5;
|
||||
}
|
||||
|
||||
message DeleteAgencyResponse {
|
||||
Agency agency = 1;
|
||||
}
|
||||
|
||||
message SetAgencyJoinEnabledRequest {
|
||||
RequestMeta meta = 1;
|
||||
string command_id = 2;
|
||||
@ -438,6 +484,9 @@ service UserHostService {
|
||||
rpc KickAgencyHost(KickAgencyHostRequest) returns (KickAgencyHostResponse);
|
||||
rpc InviteAgency(InviteAgencyRequest) returns (InviteAgencyResponse);
|
||||
rpc InviteBD(InviteBDRequest) returns (InviteBDResponse);
|
||||
rpc ListBDLeaderBDs(ListBDLeaderBDsRequest) returns (ListBDLeaderBDsResponse);
|
||||
rpc ListBDLeaderAgencies(ListBDLeaderAgenciesRequest) returns (ListBDLeaderAgenciesResponse);
|
||||
rpc ListBDAgencies(ListBDAgenciesRequest) returns (ListBDAgenciesResponse);
|
||||
rpc ProcessRoleInvitation(ProcessRoleInvitationRequest) returns (ProcessRoleInvitationResponse);
|
||||
rpc GetHostProfile(GetHostProfileRequest) returns (GetHostProfileResponse);
|
||||
rpc GetBDProfile(GetBDProfileRequest) returns (GetBDProfileResponse);
|
||||
@ -459,5 +508,6 @@ service UserHostAdminService {
|
||||
rpc SetCoinSellerStatus(SetCoinSellerStatusRequest) returns (SetCoinSellerStatusResponse);
|
||||
rpc CreateAgency(CreateAgencyRequest) returns (CreateAgencyResponse);
|
||||
rpc CloseAgency(CloseAgencyRequest) returns (CloseAgencyResponse);
|
||||
rpc DeleteAgency(DeleteAgencyRequest) returns (DeleteAgencyResponse);
|
||||
rpc SetAgencyJoinEnabled(SetAgencyJoinEnabledRequest) returns (SetAgencyJoinEnabledResponse);
|
||||
}
|
||||
|
||||
@ -25,6 +25,9 @@ const (
|
||||
UserHostService_KickAgencyHost_FullMethodName = "/hyapp.user.v1.UserHostService/KickAgencyHost"
|
||||
UserHostService_InviteAgency_FullMethodName = "/hyapp.user.v1.UserHostService/InviteAgency"
|
||||
UserHostService_InviteBD_FullMethodName = "/hyapp.user.v1.UserHostService/InviteBD"
|
||||
UserHostService_ListBDLeaderBDs_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDLeaderBDs"
|
||||
UserHostService_ListBDLeaderAgencies_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDLeaderAgencies"
|
||||
UserHostService_ListBDAgencies_FullMethodName = "/hyapp.user.v1.UserHostService/ListBDAgencies"
|
||||
UserHostService_ProcessRoleInvitation_FullMethodName = "/hyapp.user.v1.UserHostService/ProcessRoleInvitation"
|
||||
UserHostService_GetHostProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetHostProfile"
|
||||
UserHostService_GetBDProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetBDProfile"
|
||||
@ -49,6 +52,9 @@ type UserHostServiceClient interface {
|
||||
KickAgencyHost(ctx context.Context, in *KickAgencyHostRequest, opts ...grpc.CallOption) (*KickAgencyHostResponse, error)
|
||||
InviteAgency(ctx context.Context, in *InviteAgencyRequest, opts ...grpc.CallOption) (*InviteAgencyResponse, error)
|
||||
InviteBD(ctx context.Context, in *InviteBDRequest, opts ...grpc.CallOption) (*InviteBDResponse, error)
|
||||
ListBDLeaderBDs(ctx context.Context, in *ListBDLeaderBDsRequest, opts ...grpc.CallOption) (*ListBDLeaderBDsResponse, error)
|
||||
ListBDLeaderAgencies(ctx context.Context, in *ListBDLeaderAgenciesRequest, opts ...grpc.CallOption) (*ListBDLeaderAgenciesResponse, error)
|
||||
ListBDAgencies(ctx context.Context, in *ListBDAgenciesRequest, opts ...grpc.CallOption) (*ListBDAgenciesResponse, error)
|
||||
ProcessRoleInvitation(ctx context.Context, in *ProcessRoleInvitationRequest, opts ...grpc.CallOption) (*ProcessRoleInvitationResponse, error)
|
||||
GetHostProfile(ctx context.Context, in *GetHostProfileRequest, opts ...grpc.CallOption) (*GetHostProfileResponse, error)
|
||||
GetBDProfile(ctx context.Context, in *GetBDProfileRequest, opts ...grpc.CallOption) (*GetBDProfileResponse, error)
|
||||
@ -129,6 +135,36 @@ func (c *userHostServiceClient) InviteBD(ctx context.Context, in *InviteBDReques
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) ListBDLeaderBDs(ctx context.Context, in *ListBDLeaderBDsRequest, opts ...grpc.CallOption) (*ListBDLeaderBDsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListBDLeaderBDsResponse)
|
||||
err := c.cc.Invoke(ctx, UserHostService_ListBDLeaderBDs_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) ListBDLeaderAgencies(ctx context.Context, in *ListBDLeaderAgenciesRequest, opts ...grpc.CallOption) (*ListBDLeaderAgenciesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListBDLeaderAgenciesResponse)
|
||||
err := c.cc.Invoke(ctx, UserHostService_ListBDLeaderAgencies_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) ListBDAgencies(ctx context.Context, in *ListBDAgenciesRequest, opts ...grpc.CallOption) (*ListBDAgenciesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListBDAgenciesResponse)
|
||||
err := c.cc.Invoke(ctx, UserHostService_ListBDAgencies_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostServiceClient) ProcessRoleInvitation(ctx context.Context, in *ProcessRoleInvitationRequest, opts ...grpc.CallOption) (*ProcessRoleInvitationResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ProcessRoleInvitationResponse)
|
||||
@ -241,6 +277,9 @@ type UserHostServiceServer interface {
|
||||
KickAgencyHost(context.Context, *KickAgencyHostRequest) (*KickAgencyHostResponse, error)
|
||||
InviteAgency(context.Context, *InviteAgencyRequest) (*InviteAgencyResponse, error)
|
||||
InviteBD(context.Context, *InviteBDRequest) (*InviteBDResponse, error)
|
||||
ListBDLeaderBDs(context.Context, *ListBDLeaderBDsRequest) (*ListBDLeaderBDsResponse, error)
|
||||
ListBDLeaderAgencies(context.Context, *ListBDLeaderAgenciesRequest) (*ListBDLeaderAgenciesResponse, error)
|
||||
ListBDAgencies(context.Context, *ListBDAgenciesRequest) (*ListBDAgenciesResponse, error)
|
||||
ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error)
|
||||
GetHostProfile(context.Context, *GetHostProfileRequest) (*GetHostProfileResponse, error)
|
||||
GetBDProfile(context.Context, *GetBDProfileRequest) (*GetBDProfileResponse, error)
|
||||
@ -279,6 +318,15 @@ func (UnimplementedUserHostServiceServer) InviteAgency(context.Context, *InviteA
|
||||
func (UnimplementedUserHostServiceServer) InviteBD(context.Context, *InviteBDRequest) (*InviteBDResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method InviteBD not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListBDLeaderBDs(context.Context, *ListBDLeaderBDsRequest) (*ListBDLeaderBDsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListBDLeaderBDs not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListBDLeaderAgencies(context.Context, *ListBDLeaderAgenciesRequest) (*ListBDLeaderAgenciesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListBDLeaderAgencies not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ListBDAgencies(context.Context, *ListBDAgenciesRequest) (*ListBDAgenciesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListBDAgencies not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostServiceServer) ProcessRoleInvitation(context.Context, *ProcessRoleInvitationRequest) (*ProcessRoleInvitationResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessRoleInvitation not implemented")
|
||||
}
|
||||
@ -438,6 +486,60 @@ func _UserHostService_InviteBD_Handler(srv interface{}, ctx context.Context, dec
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_ListBDLeaderBDs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListBDLeaderBDsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UserHostServiceServer).ListBDLeaderBDs(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UserHostService_ListBDLeaderBDs_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UserHostServiceServer).ListBDLeaderBDs(ctx, req.(*ListBDLeaderBDsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_ListBDLeaderAgencies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListBDLeaderAgenciesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UserHostServiceServer).ListBDLeaderAgencies(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UserHostService_ListBDLeaderAgencies_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UserHostServiceServer).ListBDLeaderAgencies(ctx, req.(*ListBDLeaderAgenciesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_ListBDAgencies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListBDAgenciesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UserHostServiceServer).ListBDAgencies(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UserHostService_ListBDAgencies_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UserHostServiceServer).ListBDAgencies(ctx, req.(*ListBDAgenciesRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostService_ProcessRoleInvitation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ProcessRoleInvitationRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -649,6 +751,18 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "InviteBD",
|
||||
Handler: _UserHostService_InviteBD_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListBDLeaderBDs",
|
||||
Handler: _UserHostService_ListBDLeaderBDs_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListBDLeaderAgencies",
|
||||
Handler: _UserHostService_ListBDLeaderAgencies_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListBDAgencies",
|
||||
Handler: _UserHostService_ListBDAgencies_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ProcessRoleInvitation",
|
||||
Handler: _UserHostService_ProcessRoleInvitation_Handler,
|
||||
@ -702,6 +816,7 @@ const (
|
||||
UserHostAdminService_SetCoinSellerStatus_FullMethodName = "/hyapp.user.v1.UserHostAdminService/SetCoinSellerStatus"
|
||||
UserHostAdminService_CreateAgency_FullMethodName = "/hyapp.user.v1.UserHostAdminService/CreateAgency"
|
||||
UserHostAdminService_CloseAgency_FullMethodName = "/hyapp.user.v1.UserHostAdminService/CloseAgency"
|
||||
UserHostAdminService_DeleteAgency_FullMethodName = "/hyapp.user.v1.UserHostAdminService/DeleteAgency"
|
||||
UserHostAdminService_SetAgencyJoinEnabled_FullMethodName = "/hyapp.user.v1.UserHostAdminService/SetAgencyJoinEnabled"
|
||||
)
|
||||
|
||||
@ -718,6 +833,7 @@ type UserHostAdminServiceClient interface {
|
||||
SetCoinSellerStatus(ctx context.Context, in *SetCoinSellerStatusRequest, opts ...grpc.CallOption) (*SetCoinSellerStatusResponse, error)
|
||||
CreateAgency(ctx context.Context, in *CreateAgencyRequest, opts ...grpc.CallOption) (*CreateAgencyResponse, error)
|
||||
CloseAgency(ctx context.Context, in *CloseAgencyRequest, opts ...grpc.CallOption) (*CloseAgencyResponse, error)
|
||||
DeleteAgency(ctx context.Context, in *DeleteAgencyRequest, opts ...grpc.CallOption) (*DeleteAgencyResponse, error)
|
||||
SetAgencyJoinEnabled(ctx context.Context, in *SetAgencyJoinEnabledRequest, opts ...grpc.CallOption) (*SetAgencyJoinEnabledResponse, error)
|
||||
}
|
||||
|
||||
@ -799,6 +915,16 @@ func (c *userHostAdminServiceClient) CloseAgency(ctx context.Context, in *CloseA
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostAdminServiceClient) DeleteAgency(ctx context.Context, in *DeleteAgencyRequest, opts ...grpc.CallOption) (*DeleteAgencyResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(DeleteAgencyResponse)
|
||||
err := c.cc.Invoke(ctx, UserHostAdminService_DeleteAgency_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userHostAdminServiceClient) SetAgencyJoinEnabled(ctx context.Context, in *SetAgencyJoinEnabledRequest, opts ...grpc.CallOption) (*SetAgencyJoinEnabledResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SetAgencyJoinEnabledResponse)
|
||||
@ -822,6 +948,7 @@ type UserHostAdminServiceServer interface {
|
||||
SetCoinSellerStatus(context.Context, *SetCoinSellerStatusRequest) (*SetCoinSellerStatusResponse, error)
|
||||
CreateAgency(context.Context, *CreateAgencyRequest) (*CreateAgencyResponse, error)
|
||||
CloseAgency(context.Context, *CloseAgencyRequest) (*CloseAgencyResponse, error)
|
||||
DeleteAgency(context.Context, *DeleteAgencyRequest) (*DeleteAgencyResponse, error)
|
||||
SetAgencyJoinEnabled(context.Context, *SetAgencyJoinEnabledRequest) (*SetAgencyJoinEnabledResponse, error)
|
||||
mustEmbedUnimplementedUserHostAdminServiceServer()
|
||||
}
|
||||
@ -854,6 +981,9 @@ func (UnimplementedUserHostAdminServiceServer) CreateAgency(context.Context, *Cr
|
||||
func (UnimplementedUserHostAdminServiceServer) CloseAgency(context.Context, *CloseAgencyRequest) (*CloseAgencyResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CloseAgency not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) DeleteAgency(context.Context, *DeleteAgencyRequest) (*DeleteAgencyResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteAgency not implemented")
|
||||
}
|
||||
func (UnimplementedUserHostAdminServiceServer) SetAgencyJoinEnabled(context.Context, *SetAgencyJoinEnabledRequest) (*SetAgencyJoinEnabledResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetAgencyJoinEnabled not implemented")
|
||||
}
|
||||
@ -1004,6 +1134,24 @@ func _UserHostAdminService_CloseAgency_Handler(srv interface{}, ctx context.Cont
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostAdminService_DeleteAgency_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DeleteAgencyRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UserHostAdminServiceServer).DeleteAgency(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UserHostAdminService_DeleteAgency_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UserHostAdminServiceServer).DeleteAgency(ctx, req.(*DeleteAgencyRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserHostAdminService_SetAgencyJoinEnabled_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SetAgencyJoinEnabledRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -1057,6 +1205,10 @@ var UserHostAdminService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "CloseAgency",
|
||||
Handler: _UserHostAdminService_CloseAgency_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "DeleteAgency",
|
||||
Handler: _UserHostAdminService_DeleteAgency_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SetAgencyJoinEnabled",
|
||||
Handler: _UserHostAdminService_SetAgencyJoinEnabled_Handler,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -432,13 +432,22 @@ message UpdateUserProfileResponse {
|
||||
User user = 1;
|
||||
}
|
||||
|
||||
// ChangeUserCountryRequest 单独修改用户国家;该字段有冷却期和变更日志。
|
||||
// ChangeUserCountryRequest 是 App 用户自助修改国家入口;受限业务身份不允许自助修改。
|
||||
message ChangeUserCountryRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
string country = 3;
|
||||
}
|
||||
|
||||
// AdminChangeUserCountryRequest 是后台显式覆盖用户国家入口;它绕过 App 自助身份限制。
|
||||
message AdminChangeUserCountryRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 target_user_id = 2;
|
||||
string country = 3;
|
||||
int64 admin_user_id = 4;
|
||||
string reason = 5;
|
||||
}
|
||||
|
||||
// ChangeUserCountryResponse 返回更新后的用户资料和下一次可修改时间。
|
||||
message ChangeUserCountryResponse {
|
||||
User user = 1;
|
||||
@ -729,6 +738,7 @@ service UserService {
|
||||
rpc GetUserMicLifetimeStats(GetUserMicLifetimeStatsRequest) returns (GetUserMicLifetimeStatsResponse);
|
||||
rpc UpdateUserProfile(UpdateUserProfileRequest) returns (UpdateUserProfileResponse);
|
||||
rpc ChangeUserCountry(ChangeUserCountryRequest) returns (ChangeUserCountryResponse);
|
||||
rpc AdminChangeUserCountry(AdminChangeUserCountryRequest) returns (ChangeUserCountryResponse);
|
||||
rpc SetUserStatus(SetUserStatusRequest) returns (SetUserStatusResponse);
|
||||
rpc CompleteOnboarding(CompleteOnboardingRequest) returns (CompleteOnboardingResponse);
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ const (
|
||||
UserService_GetUserMicLifetimeStats_FullMethodName = "/hyapp.user.v1.UserService/GetUserMicLifetimeStats"
|
||||
UserService_UpdateUserProfile_FullMethodName = "/hyapp.user.v1.UserService/UpdateUserProfile"
|
||||
UserService_ChangeUserCountry_FullMethodName = "/hyapp.user.v1.UserService/ChangeUserCountry"
|
||||
UserService_AdminChangeUserCountry_FullMethodName = "/hyapp.user.v1.UserService/AdminChangeUserCountry"
|
||||
UserService_SetUserStatus_FullMethodName = "/hyapp.user.v1.UserService/SetUserStatus"
|
||||
UserService_CompleteOnboarding_FullMethodName = "/hyapp.user.v1.UserService/CompleteOnboarding"
|
||||
)
|
||||
@ -45,6 +46,7 @@ type UserServiceClient interface {
|
||||
GetUserMicLifetimeStats(ctx context.Context, in *GetUserMicLifetimeStatsRequest, opts ...grpc.CallOption) (*GetUserMicLifetimeStatsResponse, error)
|
||||
UpdateUserProfile(ctx context.Context, in *UpdateUserProfileRequest, opts ...grpc.CallOption) (*UpdateUserProfileResponse, error)
|
||||
ChangeUserCountry(ctx context.Context, in *ChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, error)
|
||||
AdminChangeUserCountry(ctx context.Context, in *AdminChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, error)
|
||||
SetUserStatus(ctx context.Context, in *SetUserStatusRequest, opts ...grpc.CallOption) (*SetUserStatusResponse, error)
|
||||
CompleteOnboarding(ctx context.Context, in *CompleteOnboardingRequest, opts ...grpc.CallOption) (*CompleteOnboardingResponse, error)
|
||||
}
|
||||
@ -137,6 +139,16 @@ func (c *userServiceClient) ChangeUserCountry(ctx context.Context, in *ChangeUse
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userServiceClient) AdminChangeUserCountry(ctx context.Context, in *AdminChangeUserCountryRequest, opts ...grpc.CallOption) (*ChangeUserCountryResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ChangeUserCountryResponse)
|
||||
err := c.cc.Invoke(ctx, UserService_AdminChangeUserCountry_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *userServiceClient) SetUserStatus(ctx context.Context, in *SetUserStatusRequest, opts ...grpc.CallOption) (*SetUserStatusResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SetUserStatusResponse)
|
||||
@ -171,6 +183,7 @@ type UserServiceServer interface {
|
||||
GetUserMicLifetimeStats(context.Context, *GetUserMicLifetimeStatsRequest) (*GetUserMicLifetimeStatsResponse, error)
|
||||
UpdateUserProfile(context.Context, *UpdateUserProfileRequest) (*UpdateUserProfileResponse, error)
|
||||
ChangeUserCountry(context.Context, *ChangeUserCountryRequest) (*ChangeUserCountryResponse, error)
|
||||
AdminChangeUserCountry(context.Context, *AdminChangeUserCountryRequest) (*ChangeUserCountryResponse, error)
|
||||
SetUserStatus(context.Context, *SetUserStatusRequest) (*SetUserStatusResponse, error)
|
||||
CompleteOnboarding(context.Context, *CompleteOnboardingRequest) (*CompleteOnboardingResponse, error)
|
||||
mustEmbedUnimplementedUserServiceServer()
|
||||
@ -207,6 +220,9 @@ func (UnimplementedUserServiceServer) UpdateUserProfile(context.Context, *Update
|
||||
func (UnimplementedUserServiceServer) ChangeUserCountry(context.Context, *ChangeUserCountryRequest) (*ChangeUserCountryResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ChangeUserCountry not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) AdminChangeUserCountry(context.Context, *AdminChangeUserCountryRequest) (*ChangeUserCountryResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminChangeUserCountry not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) SetUserStatus(context.Context, *SetUserStatusRequest) (*SetUserStatusResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetUserStatus not implemented")
|
||||
}
|
||||
@ -378,6 +394,24 @@ func _UserService_ChangeUserCountry_Handler(srv interface{}, ctx context.Context
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserService_AdminChangeUserCountry_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminChangeUserCountryRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(UserServiceServer).AdminChangeUserCountry(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: UserService_AdminChangeUserCountry_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(UserServiceServer).AdminChangeUserCountry(ctx, req.(*AdminChangeUserCountryRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _UserService_SetUserStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SetUserStatusRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -453,6 +487,10 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ChangeUserCountry",
|
||||
Handler: _UserService_ChangeUserCountry_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminChangeUserCountry",
|
||||
Handler: _UserService_AdminChangeUserCountry_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SetUserStatus",
|
||||
Handler: _UserService_SetUserStatus_Handler,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,7 @@ message DebitGiftRequest {
|
||||
// price_version 为空时使用当前生效价格;非空时必须命中 active 价格版本。
|
||||
string price_version = 7;
|
||||
string app_code = 8;
|
||||
// region_id 来自 room-service 房间 visible_region_id,用于校验礼物区域可用性。
|
||||
// region_id 来自 room-service 房间 visible_region_id,用于校验礼物区域可用性和匹配房间贡献比例。
|
||||
int64 region_id = 9;
|
||||
// target_is_host 由 gateway 根据 user-service active host profile 注入,客户端不能直接声明。
|
||||
bool target_is_host = 10;
|
||||
@ -23,7 +23,7 @@ message DebitGiftRequest {
|
||||
int64 target_host_region_id = 11;
|
||||
// target_agency_owner_user_id 是送礼时主播归属 Agency 的 owner,用于后续代理工资结算。
|
||||
int64 target_agency_owner_user_id = 12;
|
||||
// sender_region_id 是送礼用户所属区域,用于匹配礼物入主播周期钻石比例;不能用房间 visible_region_id 替代。
|
||||
// sender_region_id 是送礼用户所属区域快照;不能用房间 visible_region_id 替代。
|
||||
int64 sender_region_id = 13;
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ message DebitGiftResponse {
|
||||
string billing_receipt_id = 1;
|
||||
string transaction_id = 2;
|
||||
int64 coin_spent = 3;
|
||||
// gift_point_added 是历史回执字段;GIFT_POINT 已下线,新送礼固定返回 0。
|
||||
int64 gift_point_added = 4;
|
||||
int64 heat_value = 5;
|
||||
string price_version = 6;
|
||||
@ -65,6 +66,7 @@ message BatchDebitGiftRequest {
|
||||
int32 gift_count = 5;
|
||||
string price_version = 6;
|
||||
string app_code = 7;
|
||||
// region_id 来自 room-service 房间 visible_region_id,用于校验礼物区域可用性和匹配房间贡献比例。
|
||||
int64 region_id = 8;
|
||||
int64 sender_region_id = 9;
|
||||
repeated DebitGiftTarget targets = 10;
|
||||
@ -104,6 +106,66 @@ message GetBalancesResponse {
|
||||
repeated AssetBalance balances = 1;
|
||||
}
|
||||
|
||||
message HostSalaryPolicyLevel {
|
||||
int32 level_no = 1;
|
||||
int64 required_diamonds = 2;
|
||||
int64 host_salary_usd_minor = 3;
|
||||
int64 host_coin_reward = 4;
|
||||
int64 agency_salary_usd_minor = 5;
|
||||
string status = 6;
|
||||
int32 sort_order = 7;
|
||||
}
|
||||
|
||||
message HostSalaryPolicy {
|
||||
uint64 policy_id = 1;
|
||||
string name = 2;
|
||||
int64 region_id = 3;
|
||||
string status = 4;
|
||||
string settlement_mode = 5;
|
||||
string settlement_trigger_mode = 6;
|
||||
string gift_coin_to_diamond_ratio = 7;
|
||||
string residual_diamond_to_usd_rate = 8;
|
||||
int64 effective_from_ms = 9;
|
||||
int64 effective_to_ms = 10;
|
||||
repeated HostSalaryPolicyLevel levels = 11;
|
||||
}
|
||||
|
||||
message GetActiveHostSalaryPolicyRequest {
|
||||
string request_id = 1;
|
||||
string app_code = 2;
|
||||
int64 region_id = 3;
|
||||
string settlement_mode = 4;
|
||||
string settlement_trigger_mode = 5;
|
||||
int64 now_ms = 6;
|
||||
}
|
||||
|
||||
message GetActiveHostSalaryPolicyResponse {
|
||||
bool found = 1;
|
||||
HostSalaryPolicy policy = 2;
|
||||
}
|
||||
|
||||
message HostSalaryProgress {
|
||||
int64 host_user_id = 1;
|
||||
string cycle_key = 2;
|
||||
int64 region_id = 3;
|
||||
int64 agency_owner_user_id = 4;
|
||||
int64 total_diamonds = 5;
|
||||
int64 gift_diamond_total = 6;
|
||||
int64 updated_at_ms = 7;
|
||||
}
|
||||
|
||||
message GetHostSalaryProgressRequest {
|
||||
string request_id = 1;
|
||||
string app_code = 2;
|
||||
int64 host_user_id = 3;
|
||||
string cycle_key = 4;
|
||||
int64 now_ms = 5;
|
||||
}
|
||||
|
||||
message GetHostSalaryProgressResponse {
|
||||
HostSalaryProgress progress = 1;
|
||||
}
|
||||
|
||||
// AdminCreditAssetRequest 是后台手动入账/调账的最小审计命令。
|
||||
message AdminCreditAssetRequest {
|
||||
string command_id = 1;
|
||||
@ -134,6 +196,8 @@ message AdminCreditCoinSellerStockRequest {
|
||||
int64 operator_user_id = 9;
|
||||
string reason = 10;
|
||||
string app_code = 11;
|
||||
int64 seller_country_id = 12;
|
||||
int64 seller_region_id = 13;
|
||||
}
|
||||
|
||||
message AdminCreditCoinSellerStockResponse {
|
||||
@ -156,9 +220,10 @@ message TransferCoinFromSellerRequest {
|
||||
int64 amount = 4;
|
||||
string reason = 5;
|
||||
string app_code = 6;
|
||||
// seller_region_id 和 target_region_id 由 gateway 从 user-service 查询,不接受客户端提交。
|
||||
// seller_region_id、target_region_id 和 target_country_id 由 gateway 从 user-service 查询,不接受客户端提交。
|
||||
int64 seller_region_id = 7;
|
||||
int64 target_region_id = 8;
|
||||
int64 target_country_id = 9;
|
||||
}
|
||||
|
||||
// TransferCoinFromSellerResponse 同时返回金币到账结果和本次转账对应的充值金额口径。
|
||||
@ -175,6 +240,70 @@ message TransferCoinFromSellerResponse {
|
||||
int64 recharge_policy_usd_minor_amount = 10;
|
||||
}
|
||||
|
||||
// CoinSellerSalaryExchangeRateTier 是用户工资转给币商时,按区域和美元金额命中的金币兑换区间。
|
||||
message CoinSellerSalaryExchangeRateTier {
|
||||
int64 region_id = 1;
|
||||
int64 min_usd_minor = 2;
|
||||
int64 max_usd_minor = 3;
|
||||
int64 coin_per_usd = 4;
|
||||
string status = 5;
|
||||
int32 sort_order = 6;
|
||||
int64 updated_at_ms = 7;
|
||||
}
|
||||
|
||||
message ListCoinSellerSalaryExchangeRateTiersRequest {
|
||||
string request_id = 1;
|
||||
string app_code = 2;
|
||||
int64 region_id = 3;
|
||||
bool include_disabled = 4;
|
||||
}
|
||||
|
||||
message ListCoinSellerSalaryExchangeRateTiersResponse {
|
||||
repeated CoinSellerSalaryExchangeRateTier tiers = 1;
|
||||
}
|
||||
|
||||
// ExchangeSalaryToCoinRequest 扣当前用户某个身份工资美元钱包,并按固定比例给同一用户加普通 COIN。
|
||||
message ExchangeSalaryToCoinRequest {
|
||||
string command_id = 1;
|
||||
int64 user_id = 2;
|
||||
string salary_asset_type = 3;
|
||||
int64 salary_usd_minor = 4;
|
||||
string reason = 5;
|
||||
string app_code = 6;
|
||||
}
|
||||
|
||||
message ExchangeSalaryToCoinResponse {
|
||||
string transaction_id = 1;
|
||||
int64 salary_balance_after = 2;
|
||||
int64 coin_balance_after = 3;
|
||||
int64 salary_usd_minor = 4;
|
||||
int64 coin_amount = 5;
|
||||
int64 coin_per_usd = 6;
|
||||
}
|
||||
|
||||
// TransferSalaryToCoinSellerRequest 扣当前用户某个身份工资美元钱包,并按区域区间比例给同区域币商加专用金币库存。
|
||||
message TransferSalaryToCoinSellerRequest {
|
||||
string command_id = 1;
|
||||
int64 source_user_id = 2;
|
||||
int64 seller_user_id = 3;
|
||||
string salary_asset_type = 4;
|
||||
int64 salary_usd_minor = 5;
|
||||
int64 region_id = 6;
|
||||
string reason = 7;
|
||||
string app_code = 8;
|
||||
}
|
||||
|
||||
message TransferSalaryToCoinSellerResponse {
|
||||
string transaction_id = 1;
|
||||
int64 source_salary_balance_after = 2;
|
||||
int64 seller_balance_after = 3;
|
||||
int64 salary_usd_minor = 4;
|
||||
int64 coin_amount = 5;
|
||||
int64 coin_per_usd = 6;
|
||||
int64 rate_min_usd_minor = 7;
|
||||
int64 rate_max_usd_minor = 8;
|
||||
}
|
||||
|
||||
// Resource 是资源库里可配置、可赠送、可被礼物引用的最小资源。
|
||||
message Resource {
|
||||
string app_code = 1;
|
||||
@ -200,6 +329,7 @@ message Resource {
|
||||
bool manager_grant_enabled = 21;
|
||||
string price_type = 22;
|
||||
int64 coin_price = 23;
|
||||
// gift_point_amount 是历史字段;资源和礼物价格现在只维护真实 COIN 价格,新写入固定为 0。
|
||||
int64 gift_point_amount = 24;
|
||||
}
|
||||
|
||||
@ -244,6 +374,7 @@ message GiftConfig {
|
||||
string presentation_json = 8;
|
||||
string price_version = 9;
|
||||
int64 coin_price = 10;
|
||||
// gift_point_amount 是历史字段;送礼结算不再读取该值,新写入固定为 0。
|
||||
int64 gift_point_amount = 11;
|
||||
int64 heat_value = 12;
|
||||
int64 created_by_user_id = 13;
|
||||
@ -393,6 +524,7 @@ message CreateResourceRequest {
|
||||
optional bool manager_grant_enabled = 18;
|
||||
string price_type = 19;
|
||||
int64 coin_price = 20;
|
||||
// gift_point_amount 是历史字段;服务端忽略请求值并固定写入 0。
|
||||
int64 gift_point_amount = 21;
|
||||
}
|
||||
|
||||
@ -418,6 +550,7 @@ message UpdateResourceRequest {
|
||||
optional bool manager_grant_enabled = 19;
|
||||
string price_type = 20;
|
||||
int64 coin_price = 21;
|
||||
// gift_point_amount 是历史字段;服务端忽略请求值并固定写入 0。
|
||||
int64 gift_point_amount = 22;
|
||||
}
|
||||
|
||||
@ -505,6 +638,7 @@ message ListGiftConfigsRequest {
|
||||
bool active_only = 7;
|
||||
int64 region_id = 8;
|
||||
bool filter_region = 9;
|
||||
string gift_type_code = 10;
|
||||
}
|
||||
|
||||
message ListGiftConfigsResponse {
|
||||
@ -549,6 +683,7 @@ message CreateGiftConfigRequest {
|
||||
string presentation_json = 8;
|
||||
string price_version = 9;
|
||||
int64 coin_price = 10;
|
||||
// gift_point_amount 是历史字段;服务端忽略请求值并固定写入 0。
|
||||
int64 gift_point_amount = 11;
|
||||
int64 heat_value = 12;
|
||||
int64 effective_at_ms = 13;
|
||||
@ -572,6 +707,7 @@ message UpdateGiftConfigRequest {
|
||||
string presentation_json = 8;
|
||||
string price_version = 9;
|
||||
int64 coin_price = 10;
|
||||
// gift_point_amount 是历史字段;服务端忽略请求值并固定写入 0。
|
||||
int64 gift_point_amount = 11;
|
||||
int64 heat_value = 12;
|
||||
int64 effective_at_ms = 13;
|
||||
@ -800,7 +936,6 @@ message ListRechargeBillsResponse {
|
||||
message WalletFeatureFlags {
|
||||
bool recharge_enabled = 1;
|
||||
bool diamond_exchange_enabled = 2;
|
||||
bool withdraw_enabled = 3;
|
||||
}
|
||||
|
||||
message GetWalletOverviewRequest {
|
||||
@ -843,6 +978,7 @@ message GiftWallItem {
|
||||
int64 total_value = 9;
|
||||
int64 total_coin_value = 10;
|
||||
int64 total_diamond_value = 11;
|
||||
// total_gift_point 是历史礼物墙字段;新送礼不再累加。
|
||||
int64 total_gift_point = 12;
|
||||
int64 total_heat_value = 13;
|
||||
string charge_asset_type = 14;
|
||||
@ -865,6 +1001,7 @@ message GetUserGiftWallResponse {
|
||||
int64 total_value = 4;
|
||||
int64 total_coin_value = 5;
|
||||
int64 total_diamond_value = 6;
|
||||
// total_gift_point 是历史礼物墙汇总字段;新送礼不再累加。
|
||||
int64 total_gift_point = 7;
|
||||
int64 total_heat_value = 8;
|
||||
}
|
||||
@ -1042,32 +1179,6 @@ message ListWalletTransactionsResponse {
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
message WithdrawalRequest {
|
||||
string withdrawal_id = 1;
|
||||
int64 user_id = 2;
|
||||
string asset_type = 3;
|
||||
int64 amount = 4;
|
||||
string status = 5;
|
||||
string payout_account = 6;
|
||||
string reason = 7;
|
||||
int64 created_at_ms = 8;
|
||||
int64 updated_at_ms = 9;
|
||||
}
|
||||
|
||||
message ApplyWithdrawalRequest {
|
||||
string command_id = 1;
|
||||
string app_code = 2;
|
||||
int64 user_id = 3;
|
||||
int64 amount = 4;
|
||||
string payout_account = 5;
|
||||
string reason = 6;
|
||||
}
|
||||
|
||||
message ApplyWithdrawalResponse {
|
||||
WithdrawalRequest withdrawal = 1;
|
||||
AssetBalance balance = 2;
|
||||
}
|
||||
|
||||
message VipRewardItem {
|
||||
int64 resource_id = 1;
|
||||
string resource_code = 2;
|
||||
@ -1237,6 +1348,30 @@ message CreditLuckyGiftRewardResponse {
|
||||
int64 granted_at_ms = 4;
|
||||
}
|
||||
|
||||
// CreditRoomTurnoverRewardRequest 是 activity-service 每周房间流水奖励结算后的金币入账命令。
|
||||
message CreditRoomTurnoverRewardRequest {
|
||||
string command_id = 1;
|
||||
string app_code = 2;
|
||||
int64 target_user_id = 3;
|
||||
int64 amount = 4;
|
||||
string settlement_id = 5;
|
||||
string room_id = 6;
|
||||
int64 period_start_ms = 7;
|
||||
int64 period_end_ms = 8;
|
||||
int64 coin_spent = 9;
|
||||
int64 tier_id = 10;
|
||||
string tier_code = 11;
|
||||
string reason = 12;
|
||||
}
|
||||
|
||||
// CreditRoomTurnoverRewardResponse 返回房间流水奖励入账流水和用户 COIN 账后余额。
|
||||
message CreditRoomTurnoverRewardResponse {
|
||||
string transaction_id = 1;
|
||||
AssetBalance balance = 2;
|
||||
int64 amount = 3;
|
||||
int64 granted_at_ms = 4;
|
||||
}
|
||||
|
||||
// ApplyGameCoinChangeRequest 是 game-service 唯一可以调用的钱包游戏改账入口。
|
||||
message ApplyGameCoinChangeRequest {
|
||||
string request_id = 1;
|
||||
@ -1464,9 +1599,14 @@ service WalletService {
|
||||
rpc DebitGift(DebitGiftRequest) returns (DebitGiftResponse);
|
||||
rpc BatchDebitGift(BatchDebitGiftRequest) returns (BatchDebitGiftResponse);
|
||||
rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse);
|
||||
rpc GetActiveHostSalaryPolicy(GetActiveHostSalaryPolicyRequest) returns (GetActiveHostSalaryPolicyResponse);
|
||||
rpc GetHostSalaryProgress(GetHostSalaryProgressRequest) returns (GetHostSalaryProgressResponse);
|
||||
rpc AdminCreditAsset(AdminCreditAssetRequest) returns (AdminCreditAssetResponse);
|
||||
rpc AdminCreditCoinSellerStock(AdminCreditCoinSellerStockRequest) returns (AdminCreditCoinSellerStockResponse);
|
||||
rpc TransferCoinFromSeller(TransferCoinFromSellerRequest) returns (TransferCoinFromSellerResponse);
|
||||
rpc ListCoinSellerSalaryExchangeRateTiers(ListCoinSellerSalaryExchangeRateTiersRequest) returns (ListCoinSellerSalaryExchangeRateTiersResponse);
|
||||
rpc ExchangeSalaryToCoin(ExchangeSalaryToCoinRequest) returns (ExchangeSalaryToCoinResponse);
|
||||
rpc TransferSalaryToCoinSeller(TransferSalaryToCoinSellerRequest) returns (TransferSalaryToCoinSellerResponse);
|
||||
rpc ListResources(ListResourcesRequest) returns (ListResourcesResponse);
|
||||
rpc GetResource(GetResourceRequest) returns (GetResourceResponse);
|
||||
rpc CreateResource(CreateResourceRequest) returns (ResourceResponse);
|
||||
@ -1506,7 +1646,6 @@ service WalletService {
|
||||
rpc DeleteRechargeProduct(DeleteRechargeProductRequest) returns (DeleteRechargeProductResponse);
|
||||
rpc GetDiamondExchangeConfig(GetDiamondExchangeConfigRequest) returns (GetDiamondExchangeConfigResponse);
|
||||
rpc ListWalletTransactions(ListWalletTransactionsRequest) returns (ListWalletTransactionsResponse);
|
||||
rpc ApplyWithdrawal(ApplyWithdrawalRequest) returns (ApplyWithdrawalResponse);
|
||||
rpc ListVipPackages(ListVipPackagesRequest) returns (ListVipPackagesResponse);
|
||||
rpc GetMyVip(GetMyVipRequest) returns (GetMyVipResponse);
|
||||
rpc PurchaseVip(PurchaseVipRequest) returns (PurchaseVipResponse);
|
||||
@ -1515,6 +1654,7 @@ service WalletService {
|
||||
rpc UpdateAdminVipLevels(UpdateAdminVipLevelsRequest) returns (UpdateAdminVipLevelsResponse);
|
||||
rpc CreditTaskReward(CreditTaskRewardRequest) returns (CreditTaskRewardResponse);
|
||||
rpc CreditLuckyGiftReward(CreditLuckyGiftRewardRequest) returns (CreditLuckyGiftRewardResponse);
|
||||
rpc CreditRoomTurnoverReward(CreditRoomTurnoverRewardRequest) returns (CreditRoomTurnoverRewardResponse);
|
||||
rpc ApplyGameCoinChange(ApplyGameCoinChangeRequest) returns (ApplyGameCoinChangeResponse);
|
||||
rpc GetRedPacketConfig(GetRedPacketConfigRequest) returns (GetRedPacketConfigResponse);
|
||||
rpc UpdateRedPacketConfig(UpdateRedPacketConfigRequest) returns (UpdateRedPacketConfigResponse);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v7.35.0
|
||||
// source: proto/wallet/v1/wallet.proto
|
||||
|
||||
@ -93,13 +93,13 @@ type WalletCronServiceServer interface {
|
||||
type UnimplementedWalletCronServiceServer struct{}
|
||||
|
||||
func (UnimplementedWalletCronServiceServer) ProcessHostSalaryDailySettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessHostSalaryDailySettlementBatch not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ProcessHostSalaryDailySettlementBatch not implemented")
|
||||
}
|
||||
func (UnimplementedWalletCronServiceServer) ProcessHostSalaryHalfMonthSettlementBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessHostSalaryHalfMonthSettlementBatch not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ProcessHostSalaryHalfMonthSettlementBatch not implemented")
|
||||
}
|
||||
func (UnimplementedWalletCronServiceServer) ProcessHostSalaryMonthEndBatch(context.Context, *CronBatchRequest) (*CronBatchResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ProcessHostSalaryMonthEndBatch not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ProcessHostSalaryMonthEndBatch not implemented")
|
||||
}
|
||||
func (UnimplementedWalletCronServiceServer) mustEmbedUnimplementedWalletCronServiceServer() {}
|
||||
func (UnimplementedWalletCronServiceServer) testEmbeddedByValue() {}
|
||||
@ -112,7 +112,7 @@ type UnsafeWalletCronServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterWalletCronServiceServer(s grpc.ServiceRegistrar, srv WalletCronServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedWalletCronServiceServer was
|
||||
// If the following call pancis, it indicates UnimplementedWalletCronServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -201,69 +201,74 @@ var WalletCronService_ServiceDesc = grpc.ServiceDesc{
|
||||
}
|
||||
|
||||
const (
|
||||
WalletService_DebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitGift"
|
||||
WalletService_BatchDebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchDebitGift"
|
||||
WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances"
|
||||
WalletService_AdminCreditAsset_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditAsset"
|
||||
WalletService_AdminCreditCoinSellerStock_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditCoinSellerStock"
|
||||
WalletService_TransferCoinFromSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferCoinFromSeller"
|
||||
WalletService_ListResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResources"
|
||||
WalletService_GetResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResource"
|
||||
WalletService_CreateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResource"
|
||||
WalletService_UpdateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResource"
|
||||
WalletService_SetResourceStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceStatus"
|
||||
WalletService_ListResourceGroups_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGroups"
|
||||
WalletService_GetResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResourceGroup"
|
||||
WalletService_CreateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResourceGroup"
|
||||
WalletService_UpdateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResourceGroup"
|
||||
WalletService_SetResourceGroupStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceGroupStatus"
|
||||
WalletService_ListGiftConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftConfigs"
|
||||
WalletService_ListGiftTypeConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftTypeConfigs"
|
||||
WalletService_CreateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateGiftConfig"
|
||||
WalletService_UpdateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateGiftConfig"
|
||||
WalletService_SetGiftConfigStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetGiftConfigStatus"
|
||||
WalletService_UpsertGiftTypeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertGiftTypeConfig"
|
||||
WalletService_GrantResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResource"
|
||||
WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup"
|
||||
WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources"
|
||||
WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource"
|
||||
WalletService_UnequipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UnequipUserResource"
|
||||
WalletService_BatchGetUserEquippedResources_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchGetUserEquippedResources"
|
||||
WalletService_ListResourceGrants_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGrants"
|
||||
WalletService_ListResourceShopItems_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceShopItems"
|
||||
WalletService_UpsertResourceShopItems_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertResourceShopItems"
|
||||
WalletService_SetResourceShopItemStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceShopItemStatus"
|
||||
WalletService_PurchaseResourceShopItem_FullMethodName = "/hyapp.wallet.v1.WalletService/PurchaseResourceShopItem"
|
||||
WalletService_ListRechargeBills_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRechargeBills"
|
||||
WalletService_GetWalletOverview_FullMethodName = "/hyapp.wallet.v1.WalletService/GetWalletOverview"
|
||||
WalletService_GetWalletValueSummary_FullMethodName = "/hyapp.wallet.v1.WalletService/GetWalletValueSummary"
|
||||
WalletService_GetUserGiftWall_FullMethodName = "/hyapp.wallet.v1.WalletService/GetUserGiftWall"
|
||||
WalletService_ListRechargeProducts_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRechargeProducts"
|
||||
WalletService_ConfirmGooglePayment_FullMethodName = "/hyapp.wallet.v1.WalletService/ConfirmGooglePayment"
|
||||
WalletService_ListAdminRechargeProducts_FullMethodName = "/hyapp.wallet.v1.WalletService/ListAdminRechargeProducts"
|
||||
WalletService_CreateRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateRechargeProduct"
|
||||
WalletService_UpdateRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateRechargeProduct"
|
||||
WalletService_DeleteRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/DeleteRechargeProduct"
|
||||
WalletService_GetDiamondExchangeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetDiamondExchangeConfig"
|
||||
WalletService_ListWalletTransactions_FullMethodName = "/hyapp.wallet.v1.WalletService/ListWalletTransactions"
|
||||
WalletService_ApplyWithdrawal_FullMethodName = "/hyapp.wallet.v1.WalletService/ApplyWithdrawal"
|
||||
WalletService_ListVipPackages_FullMethodName = "/hyapp.wallet.v1.WalletService/ListVipPackages"
|
||||
WalletService_GetMyVip_FullMethodName = "/hyapp.wallet.v1.WalletService/GetMyVip"
|
||||
WalletService_PurchaseVip_FullMethodName = "/hyapp.wallet.v1.WalletService/PurchaseVip"
|
||||
WalletService_GrantVip_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantVip"
|
||||
WalletService_ListAdminVipLevels_FullMethodName = "/hyapp.wallet.v1.WalletService/ListAdminVipLevels"
|
||||
WalletService_UpdateAdminVipLevels_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateAdminVipLevels"
|
||||
WalletService_CreditTaskReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditTaskReward"
|
||||
WalletService_CreditLuckyGiftReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditLuckyGiftReward"
|
||||
WalletService_ApplyGameCoinChange_FullMethodName = "/hyapp.wallet.v1.WalletService/ApplyGameCoinChange"
|
||||
WalletService_GetRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRedPacketConfig"
|
||||
WalletService_UpdateRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateRedPacketConfig"
|
||||
WalletService_CreateRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateRedPacket"
|
||||
WalletService_ClaimRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/ClaimRedPacket"
|
||||
WalletService_ListRedPackets_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRedPackets"
|
||||
WalletService_GetRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRedPacket"
|
||||
WalletService_ExpireRedPackets_FullMethodName = "/hyapp.wallet.v1.WalletService/ExpireRedPackets"
|
||||
WalletService_RetryRedPacketRefund_FullMethodName = "/hyapp.wallet.v1.WalletService/RetryRedPacketRefund"
|
||||
WalletService_DebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitGift"
|
||||
WalletService_BatchDebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchDebitGift"
|
||||
WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances"
|
||||
WalletService_GetActiveHostSalaryPolicy_FullMethodName = "/hyapp.wallet.v1.WalletService/GetActiveHostSalaryPolicy"
|
||||
WalletService_GetHostSalaryProgress_FullMethodName = "/hyapp.wallet.v1.WalletService/GetHostSalaryProgress"
|
||||
WalletService_AdminCreditAsset_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditAsset"
|
||||
WalletService_AdminCreditCoinSellerStock_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditCoinSellerStock"
|
||||
WalletService_TransferCoinFromSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferCoinFromSeller"
|
||||
WalletService_ListCoinSellerSalaryExchangeRateTiers_FullMethodName = "/hyapp.wallet.v1.WalletService/ListCoinSellerSalaryExchangeRateTiers"
|
||||
WalletService_ExchangeSalaryToCoin_FullMethodName = "/hyapp.wallet.v1.WalletService/ExchangeSalaryToCoin"
|
||||
WalletService_TransferSalaryToCoinSeller_FullMethodName = "/hyapp.wallet.v1.WalletService/TransferSalaryToCoinSeller"
|
||||
WalletService_ListResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResources"
|
||||
WalletService_GetResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResource"
|
||||
WalletService_CreateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResource"
|
||||
WalletService_UpdateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResource"
|
||||
WalletService_SetResourceStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceStatus"
|
||||
WalletService_ListResourceGroups_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGroups"
|
||||
WalletService_GetResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResourceGroup"
|
||||
WalletService_CreateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResourceGroup"
|
||||
WalletService_UpdateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResourceGroup"
|
||||
WalletService_SetResourceGroupStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceGroupStatus"
|
||||
WalletService_ListGiftConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftConfigs"
|
||||
WalletService_ListGiftTypeConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftTypeConfigs"
|
||||
WalletService_CreateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateGiftConfig"
|
||||
WalletService_UpdateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateGiftConfig"
|
||||
WalletService_SetGiftConfigStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetGiftConfigStatus"
|
||||
WalletService_UpsertGiftTypeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertGiftTypeConfig"
|
||||
WalletService_GrantResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResource"
|
||||
WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup"
|
||||
WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources"
|
||||
WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource"
|
||||
WalletService_UnequipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UnequipUserResource"
|
||||
WalletService_BatchGetUserEquippedResources_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchGetUserEquippedResources"
|
||||
WalletService_ListResourceGrants_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGrants"
|
||||
WalletService_ListResourceShopItems_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceShopItems"
|
||||
WalletService_UpsertResourceShopItems_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertResourceShopItems"
|
||||
WalletService_SetResourceShopItemStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceShopItemStatus"
|
||||
WalletService_PurchaseResourceShopItem_FullMethodName = "/hyapp.wallet.v1.WalletService/PurchaseResourceShopItem"
|
||||
WalletService_ListRechargeBills_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRechargeBills"
|
||||
WalletService_GetWalletOverview_FullMethodName = "/hyapp.wallet.v1.WalletService/GetWalletOverview"
|
||||
WalletService_GetWalletValueSummary_FullMethodName = "/hyapp.wallet.v1.WalletService/GetWalletValueSummary"
|
||||
WalletService_GetUserGiftWall_FullMethodName = "/hyapp.wallet.v1.WalletService/GetUserGiftWall"
|
||||
WalletService_ListRechargeProducts_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRechargeProducts"
|
||||
WalletService_ConfirmGooglePayment_FullMethodName = "/hyapp.wallet.v1.WalletService/ConfirmGooglePayment"
|
||||
WalletService_ListAdminRechargeProducts_FullMethodName = "/hyapp.wallet.v1.WalletService/ListAdminRechargeProducts"
|
||||
WalletService_CreateRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateRechargeProduct"
|
||||
WalletService_UpdateRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateRechargeProduct"
|
||||
WalletService_DeleteRechargeProduct_FullMethodName = "/hyapp.wallet.v1.WalletService/DeleteRechargeProduct"
|
||||
WalletService_GetDiamondExchangeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetDiamondExchangeConfig"
|
||||
WalletService_ListWalletTransactions_FullMethodName = "/hyapp.wallet.v1.WalletService/ListWalletTransactions"
|
||||
WalletService_ListVipPackages_FullMethodName = "/hyapp.wallet.v1.WalletService/ListVipPackages"
|
||||
WalletService_GetMyVip_FullMethodName = "/hyapp.wallet.v1.WalletService/GetMyVip"
|
||||
WalletService_PurchaseVip_FullMethodName = "/hyapp.wallet.v1.WalletService/PurchaseVip"
|
||||
WalletService_GrantVip_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantVip"
|
||||
WalletService_ListAdminVipLevels_FullMethodName = "/hyapp.wallet.v1.WalletService/ListAdminVipLevels"
|
||||
WalletService_UpdateAdminVipLevels_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateAdminVipLevels"
|
||||
WalletService_CreditTaskReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditTaskReward"
|
||||
WalletService_CreditLuckyGiftReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditLuckyGiftReward"
|
||||
WalletService_CreditRoomTurnoverReward_FullMethodName = "/hyapp.wallet.v1.WalletService/CreditRoomTurnoverReward"
|
||||
WalletService_ApplyGameCoinChange_FullMethodName = "/hyapp.wallet.v1.WalletService/ApplyGameCoinChange"
|
||||
WalletService_GetRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRedPacketConfig"
|
||||
WalletService_UpdateRedPacketConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateRedPacketConfig"
|
||||
WalletService_CreateRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateRedPacket"
|
||||
WalletService_ClaimRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/ClaimRedPacket"
|
||||
WalletService_ListRedPackets_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRedPackets"
|
||||
WalletService_GetRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRedPacket"
|
||||
WalletService_ExpireRedPackets_FullMethodName = "/hyapp.wallet.v1.WalletService/ExpireRedPackets"
|
||||
WalletService_RetryRedPacketRefund_FullMethodName = "/hyapp.wallet.v1.WalletService/RetryRedPacketRefund"
|
||||
)
|
||||
|
||||
// WalletServiceClient is the client API for WalletService service.
|
||||
@ -275,9 +280,14 @@ type WalletServiceClient interface {
|
||||
DebitGift(ctx context.Context, in *DebitGiftRequest, opts ...grpc.CallOption) (*DebitGiftResponse, error)
|
||||
BatchDebitGift(ctx context.Context, in *BatchDebitGiftRequest, opts ...grpc.CallOption) (*BatchDebitGiftResponse, error)
|
||||
GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error)
|
||||
GetActiveHostSalaryPolicy(ctx context.Context, in *GetActiveHostSalaryPolicyRequest, opts ...grpc.CallOption) (*GetActiveHostSalaryPolicyResponse, error)
|
||||
GetHostSalaryProgress(ctx context.Context, in *GetHostSalaryProgressRequest, opts ...grpc.CallOption) (*GetHostSalaryProgressResponse, error)
|
||||
AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error)
|
||||
AdminCreditCoinSellerStock(ctx context.Context, in *AdminCreditCoinSellerStockRequest, opts ...grpc.CallOption) (*AdminCreditCoinSellerStockResponse, error)
|
||||
TransferCoinFromSeller(ctx context.Context, in *TransferCoinFromSellerRequest, opts ...grpc.CallOption) (*TransferCoinFromSellerResponse, error)
|
||||
ListCoinSellerSalaryExchangeRateTiers(ctx context.Context, in *ListCoinSellerSalaryExchangeRateTiersRequest, opts ...grpc.CallOption) (*ListCoinSellerSalaryExchangeRateTiersResponse, error)
|
||||
ExchangeSalaryToCoin(ctx context.Context, in *ExchangeSalaryToCoinRequest, opts ...grpc.CallOption) (*ExchangeSalaryToCoinResponse, error)
|
||||
TransferSalaryToCoinSeller(ctx context.Context, in *TransferSalaryToCoinSellerRequest, opts ...grpc.CallOption) (*TransferSalaryToCoinSellerResponse, error)
|
||||
ListResources(ctx context.Context, in *ListResourcesRequest, opts ...grpc.CallOption) (*ListResourcesResponse, error)
|
||||
GetResource(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*GetResourceResponse, error)
|
||||
CreateResource(ctx context.Context, in *CreateResourceRequest, opts ...grpc.CallOption) (*ResourceResponse, error)
|
||||
@ -317,7 +327,6 @@ type WalletServiceClient interface {
|
||||
DeleteRechargeProduct(ctx context.Context, in *DeleteRechargeProductRequest, opts ...grpc.CallOption) (*DeleteRechargeProductResponse, error)
|
||||
GetDiamondExchangeConfig(ctx context.Context, in *GetDiamondExchangeConfigRequest, opts ...grpc.CallOption) (*GetDiamondExchangeConfigResponse, error)
|
||||
ListWalletTransactions(ctx context.Context, in *ListWalletTransactionsRequest, opts ...grpc.CallOption) (*ListWalletTransactionsResponse, error)
|
||||
ApplyWithdrawal(ctx context.Context, in *ApplyWithdrawalRequest, opts ...grpc.CallOption) (*ApplyWithdrawalResponse, error)
|
||||
ListVipPackages(ctx context.Context, in *ListVipPackagesRequest, opts ...grpc.CallOption) (*ListVipPackagesResponse, error)
|
||||
GetMyVip(ctx context.Context, in *GetMyVipRequest, opts ...grpc.CallOption) (*GetMyVipResponse, error)
|
||||
PurchaseVip(ctx context.Context, in *PurchaseVipRequest, opts ...grpc.CallOption) (*PurchaseVipResponse, error)
|
||||
@ -326,6 +335,7 @@ type WalletServiceClient interface {
|
||||
UpdateAdminVipLevels(ctx context.Context, in *UpdateAdminVipLevelsRequest, opts ...grpc.CallOption) (*UpdateAdminVipLevelsResponse, error)
|
||||
CreditTaskReward(ctx context.Context, in *CreditTaskRewardRequest, opts ...grpc.CallOption) (*CreditTaskRewardResponse, error)
|
||||
CreditLuckyGiftReward(ctx context.Context, in *CreditLuckyGiftRewardRequest, opts ...grpc.CallOption) (*CreditLuckyGiftRewardResponse, error)
|
||||
CreditRoomTurnoverReward(ctx context.Context, in *CreditRoomTurnoverRewardRequest, opts ...grpc.CallOption) (*CreditRoomTurnoverRewardResponse, error)
|
||||
ApplyGameCoinChange(ctx context.Context, in *ApplyGameCoinChangeRequest, opts ...grpc.CallOption) (*ApplyGameCoinChangeResponse, error)
|
||||
GetRedPacketConfig(ctx context.Context, in *GetRedPacketConfigRequest, opts ...grpc.CallOption) (*GetRedPacketConfigResponse, error)
|
||||
UpdateRedPacketConfig(ctx context.Context, in *UpdateRedPacketConfigRequest, opts ...grpc.CallOption) (*UpdateRedPacketConfigResponse, error)
|
||||
@ -375,6 +385,26 @@ func (c *walletServiceClient) GetBalances(ctx context.Context, in *GetBalancesRe
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) GetActiveHostSalaryPolicy(ctx context.Context, in *GetActiveHostSalaryPolicyRequest, opts ...grpc.CallOption) (*GetActiveHostSalaryPolicyResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetActiveHostSalaryPolicyResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_GetActiveHostSalaryPolicy_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) GetHostSalaryProgress(ctx context.Context, in *GetHostSalaryProgressRequest, opts ...grpc.CallOption) (*GetHostSalaryProgressResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(GetHostSalaryProgressResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_GetHostSalaryProgress_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AdminCreditAssetResponse)
|
||||
@ -405,6 +435,36 @@ func (c *walletServiceClient) TransferCoinFromSeller(ctx context.Context, in *Tr
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) ListCoinSellerSalaryExchangeRateTiers(ctx context.Context, in *ListCoinSellerSalaryExchangeRateTiersRequest, opts ...grpc.CallOption) (*ListCoinSellerSalaryExchangeRateTiersResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListCoinSellerSalaryExchangeRateTiersResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_ListCoinSellerSalaryExchangeRateTiers_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) ExchangeSalaryToCoin(ctx context.Context, in *ExchangeSalaryToCoinRequest, opts ...grpc.CallOption) (*ExchangeSalaryToCoinResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ExchangeSalaryToCoinResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_ExchangeSalaryToCoin_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) TransferSalaryToCoinSeller(ctx context.Context, in *TransferSalaryToCoinSellerRequest, opts ...grpc.CallOption) (*TransferSalaryToCoinSellerResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(TransferSalaryToCoinSellerResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_TransferSalaryToCoinSeller_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) ListResources(ctx context.Context, in *ListResourcesRequest, opts ...grpc.CallOption) (*ListResourcesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListResourcesResponse)
|
||||
@ -795,16 +855,6 @@ func (c *walletServiceClient) ListWalletTransactions(ctx context.Context, in *Li
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) ApplyWithdrawal(ctx context.Context, in *ApplyWithdrawalRequest, opts ...grpc.CallOption) (*ApplyWithdrawalResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ApplyWithdrawalResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_ApplyWithdrawal_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) ListVipPackages(ctx context.Context, in *ListVipPackagesRequest, opts ...grpc.CallOption) (*ListVipPackagesResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListVipPackagesResponse)
|
||||
@ -885,6 +935,16 @@ func (c *walletServiceClient) CreditLuckyGiftReward(ctx context.Context, in *Cre
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) CreditRoomTurnoverReward(ctx context.Context, in *CreditRoomTurnoverRewardRequest, opts ...grpc.CallOption) (*CreditRoomTurnoverRewardResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(CreditRoomTurnoverRewardResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_CreditRoomTurnoverReward_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) ApplyGameCoinChange(ctx context.Context, in *ApplyGameCoinChangeRequest, opts ...grpc.CallOption) (*ApplyGameCoinChangeResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ApplyGameCoinChangeResponse)
|
||||
@ -984,9 +1044,14 @@ type WalletServiceServer interface {
|
||||
DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error)
|
||||
BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error)
|
||||
GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error)
|
||||
GetActiveHostSalaryPolicy(context.Context, *GetActiveHostSalaryPolicyRequest) (*GetActiveHostSalaryPolicyResponse, error)
|
||||
GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error)
|
||||
AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error)
|
||||
AdminCreditCoinSellerStock(context.Context, *AdminCreditCoinSellerStockRequest) (*AdminCreditCoinSellerStockResponse, error)
|
||||
TransferCoinFromSeller(context.Context, *TransferCoinFromSellerRequest) (*TransferCoinFromSellerResponse, error)
|
||||
ListCoinSellerSalaryExchangeRateTiers(context.Context, *ListCoinSellerSalaryExchangeRateTiersRequest) (*ListCoinSellerSalaryExchangeRateTiersResponse, error)
|
||||
ExchangeSalaryToCoin(context.Context, *ExchangeSalaryToCoinRequest) (*ExchangeSalaryToCoinResponse, error)
|
||||
TransferSalaryToCoinSeller(context.Context, *TransferSalaryToCoinSellerRequest) (*TransferSalaryToCoinSellerResponse, error)
|
||||
ListResources(context.Context, *ListResourcesRequest) (*ListResourcesResponse, error)
|
||||
GetResource(context.Context, *GetResourceRequest) (*GetResourceResponse, error)
|
||||
CreateResource(context.Context, *CreateResourceRequest) (*ResourceResponse, error)
|
||||
@ -1026,7 +1091,6 @@ type WalletServiceServer interface {
|
||||
DeleteRechargeProduct(context.Context, *DeleteRechargeProductRequest) (*DeleteRechargeProductResponse, error)
|
||||
GetDiamondExchangeConfig(context.Context, *GetDiamondExchangeConfigRequest) (*GetDiamondExchangeConfigResponse, error)
|
||||
ListWalletTransactions(context.Context, *ListWalletTransactionsRequest) (*ListWalletTransactionsResponse, error)
|
||||
ApplyWithdrawal(context.Context, *ApplyWithdrawalRequest) (*ApplyWithdrawalResponse, error)
|
||||
ListVipPackages(context.Context, *ListVipPackagesRequest) (*ListVipPackagesResponse, error)
|
||||
GetMyVip(context.Context, *GetMyVipRequest) (*GetMyVipResponse, error)
|
||||
PurchaseVip(context.Context, *PurchaseVipRequest) (*PurchaseVipResponse, error)
|
||||
@ -1035,6 +1099,7 @@ type WalletServiceServer interface {
|
||||
UpdateAdminVipLevels(context.Context, *UpdateAdminVipLevelsRequest) (*UpdateAdminVipLevelsResponse, error)
|
||||
CreditTaskReward(context.Context, *CreditTaskRewardRequest) (*CreditTaskRewardResponse, error)
|
||||
CreditLuckyGiftReward(context.Context, *CreditLuckyGiftRewardRequest) (*CreditLuckyGiftRewardResponse, error)
|
||||
CreditRoomTurnoverReward(context.Context, *CreditRoomTurnoverRewardRequest) (*CreditRoomTurnoverRewardResponse, error)
|
||||
ApplyGameCoinChange(context.Context, *ApplyGameCoinChangeRequest) (*ApplyGameCoinChangeResponse, error)
|
||||
GetRedPacketConfig(context.Context, *GetRedPacketConfigRequest) (*GetRedPacketConfigResponse, error)
|
||||
UpdateRedPacketConfig(context.Context, *UpdateRedPacketConfigRequest) (*UpdateRedPacketConfigResponse, error)
|
||||
@ -1055,193 +1120,208 @@ type WalletServiceServer interface {
|
||||
type UnimplementedWalletServiceServer struct{}
|
||||
|
||||
func (UnimplementedWalletServiceServer) DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method DebitGift not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DebitGift not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchDebitGift not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchDebitGift not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetBalances not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetBalances not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetActiveHostSalaryPolicy(context.Context, *GetActiveHostSalaryPolicyRequest) (*GetActiveHostSalaryPolicyResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetActiveHostSalaryPolicy not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetHostSalaryProgress not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminCreditAsset not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminCreditAsset not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) AdminCreditCoinSellerStock(context.Context, *AdminCreditCoinSellerStockRequest) (*AdminCreditCoinSellerStockResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminCreditCoinSellerStock not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminCreditCoinSellerStock not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) TransferCoinFromSeller(context.Context, *TransferCoinFromSellerRequest) (*TransferCoinFromSellerResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method TransferCoinFromSeller not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method TransferCoinFromSeller not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListCoinSellerSalaryExchangeRateTiers(context.Context, *ListCoinSellerSalaryExchangeRateTiersRequest) (*ListCoinSellerSalaryExchangeRateTiersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListCoinSellerSalaryExchangeRateTiers not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ExchangeSalaryToCoin(context.Context, *ExchangeSalaryToCoinRequest) (*ExchangeSalaryToCoinResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExchangeSalaryToCoin not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) TransferSalaryToCoinSeller(context.Context, *TransferSalaryToCoinSellerRequest) (*TransferSalaryToCoinSellerResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method TransferSalaryToCoinSeller not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListResources(context.Context, *ListResourcesRequest) (*ListResourcesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListResources not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListResources not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetResource(context.Context, *GetResourceRequest) (*GetResourceResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetResource not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreateResource(context.Context, *CreateResourceRequest) (*ResourceResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateResource not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateResource(context.Context, *UpdateResourceRequest) (*ResourceResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateResource not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SetResourceStatus(context.Context, *SetResourceStatusRequest) (*ResourceResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetResourceStatus not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetResourceStatus not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListResourceGroups(context.Context, *ListResourceGroupsRequest) (*ListResourceGroupsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListResourceGroups not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListResourceGroups not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetResourceGroup(context.Context, *GetResourceGroupRequest) (*GetResourceGroupResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetResourceGroup not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetResourceGroup not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreateResourceGroup(context.Context, *CreateResourceGroupRequest) (*ResourceGroupResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateResourceGroup not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateResourceGroup not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateResourceGroup(context.Context, *UpdateResourceGroupRequest) (*ResourceGroupResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateResourceGroup not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateResourceGroup not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SetResourceGroupStatus(context.Context, *SetResourceGroupStatusRequest) (*ResourceGroupResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetResourceGroupStatus not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetResourceGroupStatus not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListGiftConfigs(context.Context, *ListGiftConfigsRequest) (*ListGiftConfigsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListGiftConfigs not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListGiftConfigs not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListGiftTypeConfigs(context.Context, *ListGiftTypeConfigsRequest) (*ListGiftTypeConfigsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListGiftTypeConfigs not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListGiftTypeConfigs not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreateGiftConfig(context.Context, *CreateGiftConfigRequest) (*GiftConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateGiftConfig not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateGiftConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateGiftConfig(context.Context, *UpdateGiftConfigRequest) (*GiftConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateGiftConfig not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateGiftConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SetGiftConfigStatus(context.Context, *SetGiftConfigStatusRequest) (*GiftConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetGiftConfigStatus not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetGiftConfigStatus not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpsertGiftTypeConfig(context.Context, *UpsertGiftTypeConfigRequest) (*GiftTypeConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpsertGiftTypeConfig not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpsertGiftTypeConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GrantResource(context.Context, *GrantResourceRequest) (*ResourceGrantResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GrantResource not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GrantResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GrantResourceGroup not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GrantResourceGroup not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListUserResources not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListUserResources not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method EquipUserResource not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method EquipUserResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UnequipUserResource(context.Context, *UnequipUserResourceRequest) (*UnequipUserResourceResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UnequipUserResource not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UnequipUserResource not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) BatchGetUserEquippedResources(context.Context, *BatchGetUserEquippedResourcesRequest) (*BatchGetUserEquippedResourcesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchGetUserEquippedResources not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BatchGetUserEquippedResources not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListResourceGrants(context.Context, *ListResourceGrantsRequest) (*ListResourceGrantsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListResourceGrants not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListResourceGrants not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListResourceShopItems(context.Context, *ListResourceShopItemsRequest) (*ListResourceShopItemsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListResourceShopItems not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListResourceShopItems not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpsertResourceShopItems(context.Context, *UpsertResourceShopItemsRequest) (*UpsertResourceShopItemsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpsertResourceShopItems not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpsertResourceShopItems not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) SetResourceShopItemStatus(context.Context, *SetResourceShopItemStatusRequest) (*ResourceShopItemResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method SetResourceShopItemStatus not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetResourceShopItemStatus not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) PurchaseResourceShopItem(context.Context, *PurchaseResourceShopItemRequest) (*PurchaseResourceShopItemResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method PurchaseResourceShopItem not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PurchaseResourceShopItem not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListRechargeBills(context.Context, *ListRechargeBillsRequest) (*ListRechargeBillsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRechargeBills not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRechargeBills not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetWalletOverview(context.Context, *GetWalletOverviewRequest) (*GetWalletOverviewResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetWalletOverview not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetWalletOverview not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetWalletValueSummary(context.Context, *GetWalletValueSummaryRequest) (*GetWalletValueSummaryResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetWalletValueSummary not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetWalletValueSummary not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetUserGiftWall(context.Context, *GetUserGiftWallRequest) (*GetUserGiftWallResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUserGiftWall not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserGiftWall not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListRechargeProducts(context.Context, *ListRechargeProductsRequest) (*ListRechargeProductsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRechargeProducts not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRechargeProducts not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ConfirmGooglePayment(context.Context, *ConfirmGooglePaymentRequest) (*ConfirmGooglePaymentResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ConfirmGooglePayment not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ConfirmGooglePayment not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListAdminRechargeProducts(context.Context, *ListAdminRechargeProductsRequest) (*ListAdminRechargeProductsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListAdminRechargeProducts not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListAdminRechargeProducts not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreateRechargeProduct(context.Context, *CreateRechargeProductRequest) (*RechargeProductResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateRechargeProduct not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateRechargeProduct not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateRechargeProduct(context.Context, *UpdateRechargeProductRequest) (*RechargeProductResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateRechargeProduct not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRechargeProduct not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) DeleteRechargeProduct(context.Context, *DeleteRechargeProductRequest) (*DeleteRechargeProductResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteRechargeProduct not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteRechargeProduct not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetDiamondExchangeConfig(context.Context, *GetDiamondExchangeConfigRequest) (*GetDiamondExchangeConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetDiamondExchangeConfig not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetDiamondExchangeConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListWalletTransactions(context.Context, *ListWalletTransactionsRequest) (*ListWalletTransactionsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListWalletTransactions not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ApplyWithdrawal(context.Context, *ApplyWithdrawalRequest) (*ApplyWithdrawalResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ApplyWithdrawal not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListWalletTransactions not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListVipPackages(context.Context, *ListVipPackagesRequest) (*ListVipPackagesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListVipPackages not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListVipPackages not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetMyVip(context.Context, *GetMyVipRequest) (*GetMyVipResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetMyVip not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetMyVip not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) PurchaseVip(context.Context, *PurchaseVipRequest) (*PurchaseVipResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method PurchaseVip not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PurchaseVip not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GrantVip(context.Context, *GrantVipRequest) (*GrantVipResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GrantVip not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GrantVip not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListAdminVipLevels(context.Context, *ListAdminVipLevelsRequest) (*ListAdminVipLevelsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListAdminVipLevels not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListAdminVipLevels not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateAdminVipLevels(context.Context, *UpdateAdminVipLevelsRequest) (*UpdateAdminVipLevelsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateAdminVipLevels not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateAdminVipLevels not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreditTaskReward(context.Context, *CreditTaskRewardRequest) (*CreditTaskRewardResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CreditTaskReward not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreditTaskReward not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreditLuckyGiftReward(context.Context, *CreditLuckyGiftRewardRequest) (*CreditLuckyGiftRewardResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CreditLuckyGiftReward not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreditLuckyGiftReward not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreditRoomTurnoverReward(context.Context, *CreditRoomTurnoverRewardRequest) (*CreditRoomTurnoverRewardResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreditRoomTurnoverReward not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ApplyGameCoinChange(context.Context, *ApplyGameCoinChangeRequest) (*ApplyGameCoinChangeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ApplyGameCoinChange not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ApplyGameCoinChange not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetRedPacketConfig(context.Context, *GetRedPacketConfigRequest) (*GetRedPacketConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRedPacketConfig not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRedPacketConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) UpdateRedPacketConfig(context.Context, *UpdateRedPacketConfigRequest) (*UpdateRedPacketConfigResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateRedPacketConfig not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRedPacketConfig not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) CreateRedPacket(context.Context, *CreateRedPacketRequest) (*CreateRedPacketResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateRedPacket not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateRedPacket not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ClaimRedPacket(context.Context, *ClaimRedPacketRequest) (*ClaimRedPacketResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ClaimRedPacket not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ClaimRedPacket not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ListRedPackets(context.Context, *ListRedPacketsRequest) (*ListRedPacketsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRedPackets not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListRedPackets not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetRedPacket(context.Context, *GetRedPacketRequest) (*GetRedPacketResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetRedPacket not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetRedPacket not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) ExpireRedPackets(context.Context, *ExpireRedPacketsRequest) (*ExpireRedPacketsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ExpireRedPackets not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ExpireRedPackets not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) RetryRedPacketRefund(context.Context, *RetryRedPacketRefundRequest) (*RetryRedPacketRefundResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method RetryRedPacketRefund not implemented")
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RetryRedPacketRefund not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) mustEmbedUnimplementedWalletServiceServer() {}
|
||||
func (UnimplementedWalletServiceServer) testEmbeddedByValue() {}
|
||||
@ -1254,7 +1334,7 @@ type UnsafeWalletServiceServer interface {
|
||||
}
|
||||
|
||||
func RegisterWalletServiceServer(s grpc.ServiceRegistrar, srv WalletServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedWalletServiceServer was
|
||||
// If the following call pancis, it indicates UnimplementedWalletServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
@ -1318,6 +1398,42 @@ func _WalletService_GetBalances_Handler(srv interface{}, ctx context.Context, de
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_GetActiveHostSalaryPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetActiveHostSalaryPolicyRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).GetActiveHostSalaryPolicy(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_GetActiveHostSalaryPolicy_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).GetActiveHostSalaryPolicy(ctx, req.(*GetActiveHostSalaryPolicyRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_GetHostSalaryProgress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetHostSalaryProgressRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).GetHostSalaryProgress(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_GetHostSalaryProgress_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).GetHostSalaryProgress(ctx, req.(*GetHostSalaryProgressRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_AdminCreditAsset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminCreditAssetRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -1372,6 +1488,60 @@ func _WalletService_TransferCoinFromSeller_Handler(srv interface{}, ctx context.
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_ListCoinSellerSalaryExchangeRateTiers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListCoinSellerSalaryExchangeRateTiersRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).ListCoinSellerSalaryExchangeRateTiers(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_ListCoinSellerSalaryExchangeRateTiers_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).ListCoinSellerSalaryExchangeRateTiers(ctx, req.(*ListCoinSellerSalaryExchangeRateTiersRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_ExchangeSalaryToCoin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ExchangeSalaryToCoinRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).ExchangeSalaryToCoin(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_ExchangeSalaryToCoin_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).ExchangeSalaryToCoin(ctx, req.(*ExchangeSalaryToCoinRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_TransferSalaryToCoinSeller_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(TransferSalaryToCoinSellerRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).TransferSalaryToCoinSeller(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_TransferSalaryToCoinSeller_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).TransferSalaryToCoinSeller(ctx, req.(*TransferSalaryToCoinSellerRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_ListResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListResourcesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -2074,24 +2244,6 @@ func _WalletService_ListWalletTransactions_Handler(srv interface{}, ctx context.
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_ApplyWithdrawal_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ApplyWithdrawalRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).ApplyWithdrawal(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_ApplyWithdrawal_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).ApplyWithdrawal(ctx, req.(*ApplyWithdrawalRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_ListVipPackages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListVipPackagesRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -2236,6 +2388,24 @@ func _WalletService_CreditLuckyGiftReward_Handler(srv interface{}, ctx context.C
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_CreditRoomTurnoverReward_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CreditRoomTurnoverRewardRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).CreditRoomTurnoverReward(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_CreditRoomTurnoverReward_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).CreditRoomTurnoverReward(ctx, req.(*CreditRoomTurnoverRewardRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_ApplyGameCoinChange_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ApplyGameCoinChangeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -2417,6 +2587,14 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetBalances",
|
||||
Handler: _WalletService_GetBalances_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetActiveHostSalaryPolicy",
|
||||
Handler: _WalletService_GetActiveHostSalaryPolicy_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetHostSalaryProgress",
|
||||
Handler: _WalletService_GetHostSalaryProgress_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminCreditAsset",
|
||||
Handler: _WalletService_AdminCreditAsset_Handler,
|
||||
@ -2429,6 +2607,18 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "TransferCoinFromSeller",
|
||||
Handler: _WalletService_TransferCoinFromSeller_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListCoinSellerSalaryExchangeRateTiers",
|
||||
Handler: _WalletService_ListCoinSellerSalaryExchangeRateTiers_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ExchangeSalaryToCoin",
|
||||
Handler: _WalletService_ExchangeSalaryToCoin_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "TransferSalaryToCoinSeller",
|
||||
Handler: _WalletService_TransferSalaryToCoinSeller_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListResources",
|
||||
Handler: _WalletService_ListResources_Handler,
|
||||
@ -2585,10 +2775,6 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ListWalletTransactions",
|
||||
Handler: _WalletService_ListWalletTransactions_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ApplyWithdrawal",
|
||||
Handler: _WalletService_ApplyWithdrawal_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListVipPackages",
|
||||
Handler: _WalletService_ListVipPackages_Handler,
|
||||
@ -2621,6 +2807,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "CreditLuckyGiftReward",
|
||||
Handler: _WalletService_CreditLuckyGiftReward_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CreditRoomTurnoverReward",
|
||||
Handler: _WalletService_CreditRoomTurnoverReward_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ApplyGameCoinChange",
|
||||
Handler: _WalletService_ApplyGameCoinChange_Handler,
|
||||
|
||||
88
docs/Flutter麦上心跳接口.md
Normal file
88
docs/Flutter麦上心跳接口.md
Normal file
@ -0,0 +1,88 @@
|
||||
# Flutter 麦上心跳接口
|
||||
|
||||
## 地址
|
||||
|
||||
```http
|
||||
POST /api/v1/rooms/mic/heartbeat
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `room_id` | string | 是 | 房间 ID。 |
|
||||
| `command_id` | string | 是 | 本次心跳命令 ID。同一次请求重试复用同一个值,下一次心跳换新的值。 |
|
||||
| `target_user_id` | int64 | 否 | 传 `0` 或不传表示当前登录用户自己。 |
|
||||
| `mic_session_id` | string | 是 | 当前麦位会话 ID,必须使用上麦返回的 `mic_session_id`。 |
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"room_id": "lalu_xxx",
|
||||
"command_id": "cmd_mic_heartbeat_lalu_xxx_123_001",
|
||||
"target_user_id": 0,
|
||||
"mic_session_id": "mic_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
## 返回值
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "OK",
|
||||
"request_id": "req_xxx",
|
||||
"data": {
|
||||
"result": {
|
||||
"applied": true,
|
||||
"room_version": 18,
|
||||
"server_time_ms": 1778000005000
|
||||
},
|
||||
"seat_no": 1,
|
||||
"mic_heartbeat_at_ms": 1778000005000,
|
||||
"room": {
|
||||
"room_id": "lalu_xxx",
|
||||
"version": 18,
|
||||
"mic_seats": [
|
||||
{
|
||||
"seat_no": 1,
|
||||
"user_id": 123,
|
||||
"publish_state": "publishing",
|
||||
"mic_session_id": "mic_xxx",
|
||||
"mic_heartbeat_at_ms": 1778000005000,
|
||||
"seat_status": "publishing"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `data.seat_no` | 当前用户所在麦位。 |
|
||||
| `data.mic_heartbeat_at_ms` | 服务端接受本次麦上心跳的时间,UTC epoch milliseconds。 |
|
||||
| `data.room` | 刷新后的房间快照。 |
|
||||
| `data.room.mic_seats[].mic_heartbeat_at_ms` | 麦位最近一次服务端接受心跳的时间。 |
|
||||
| `data.room.mic_seats[].publish_state` | `publishing` 表示当前会话已确认发流,可以继续心跳。 |
|
||||
| `data.room.mic_seats[].seat_status` | 展示状态,常见值为 `publishing`、`muted`、`occupied`、`empty`、`locked`。 |
|
||||
|
||||
## 相关 IM
|
||||
|
||||
| IM | 是否新增 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 无 | 否 | 麦上心跳不广播独立 IM。 |
|
||||
| `room_mic_publish_confirmed` | 已有 | 发流确认成功后进入 `publishing`,Flutter 收到后可以开始麦上心跳。 |
|
||||
| `room_mic_down` | 已有 | 用户下麦、离房、被踢、发流超时或 RTC 停止后释放麦位,Flutter 收到后停止麦上心跳。 |
|
||||
|
||||
## Flutter 调用规则
|
||||
|
||||
1. `POST /api/v1/rooms/mic/up` 成功后保存 `mic_session_id`。
|
||||
2. RTC 发流成功后调用 `POST /api/v1/rooms/mic/publishing/confirm`。
|
||||
3. 确认成功且当前麦位 `publish_state=publishing` 后,周期调用 `POST /api/v1/rooms/mic/heartbeat`。
|
||||
4. `pending_publish` 状态不能调用麦上心跳开始计时。
|
||||
5. 返回 `mic session mismatch` 时,本地保存的是旧 `mic_session_id`,需要刷新房间详情。
|
||||
6. 收到 `room_mic_down` 或接口返回用户不在麦上时,停止本地麦上心跳。
|
||||
@ -38,7 +38,7 @@ docs/房间Outbox补偿开发.md
|
||||
| 播报发送能力 | `activity-service` | 服务端向全局/区域播报群发送自定义消息 |
|
||||
| 旧区域群成员移除 | `activity-service` | 用户区域变化后,对旧区域群执行 `delete_group_member(user_id)` |
|
||||
| 贵重礼物播报触发 | `activity-service` consuming room outbox | 基于 `RoomGiftSent` 事实判断阈值并发区域播报 |
|
||||
| 宝箱倒计时播报触发 | `activity-service` consuming room outbox | 基于 `RoomTreasureCountdownStarted` 事实按后台配置发区域或全局播报 |
|
||||
| 火箭倒计时播报触发 | `activity-service` consuming room outbox | 基于 `RoomRocketIgnited` 事实按后台配置发区域或全局播报 |
|
||||
|
||||
本阶段不做:
|
||||
|
||||
@ -107,7 +107,7 @@ func Parse(groupID string) ParsedGroup
|
||||
| 红包资金事实 | `wallet-service` 或独立 red-packet domain | IM 只携带入口和展示字段 |
|
||||
| 客户端 IM 长连接 | Tencent IM SDK | 本仓库不自建 WebSocket |
|
||||
|
||||
`room-service` 只需要继续产出 `RoomGiftSent`、`RoomTreasureCountdownStarted` 等 outbox 事实。贵重礼物和宝箱倒计时是否需要区域/全局播报,由 `activity-service` 通过 direct gRPC 或 RocketMQ `hyapp_room_outbox` consumer group 消费后判断,这样不会把区域播报策略塞进 Room Cell 主链路。
|
||||
`room-service` 只需要继续产出 `RoomGiftSent`、`RoomRocketIgnited` 等 outbox 事实。贵重礼物和火箭倒计时是否需要区域/全局播报,由 `activity-service` 通过 direct gRPC 或 RocketMQ `hyapp_room_outbox` consumer group 消费后判断,这样不会把区域播报策略塞进 Room Cell 主链路。
|
||||
|
||||
## Client Flow
|
||||
|
||||
@ -404,9 +404,9 @@ sequenceDiagram
|
||||
|
||||
如果现有事件字段不足,应优先扩展 protobuf event,而不是让 activity-service 反查多个服务拼凑事实。
|
||||
|
||||
## Room Treasure Broadcast
|
||||
## Room Rocket Broadcast
|
||||
|
||||
宝箱满能量进入倒计时时,Room Cell 写 `RoomTreasureCountdownStarted` outbox。activity-service 消费该事实后按后台配置决定发区域播报还是全局播报;开箱到点、奖励结算和宝箱状态推进仍由 room-service 负责。
|
||||
火箭燃料满进入倒计时时,Room Cell 写 `RoomRocketIgnited` outbox。activity-service 消费该事实后按后台配置决定发区域播报还是全局播报;发射到点、奖励结算和火箭状态推进仍由 room-service 负责。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@ -421,11 +421,12 @@ sequenceDiagram
|
||||
C->>G: POST /api/v1/rooms/gift/send
|
||||
G->>R: SendGift
|
||||
R->>W: DebitGift
|
||||
W-->>R: receipt + heat/gift point
|
||||
R->>R: add effective treasure energy
|
||||
R->>R: status=countdown when threshold reached
|
||||
W-->>R: receipt + heat_value
|
||||
R->>R: add effective rocket fuel
|
||||
R->>R: append pending_launch when threshold reached
|
||||
R->>R: start next level immediately
|
||||
R-->>G: SendGiftResponse
|
||||
R->>MQ: RoomTreasureCountdownStarted
|
||||
R->>MQ: RoomRocketIgnited
|
||||
MQ->>A: consume by hyapp-activity-room-outbox
|
||||
A->>IM: send_group_msg(region/global broadcast group)
|
||||
```
|
||||
@ -434,15 +435,22 @@ sequenceDiagram
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "room_treasure:lalu_room_xxx:box_1",
|
||||
"broadcast_type": "room_treasure",
|
||||
"event_id": "room_rocket:lalu_room_xxx:rocket_1",
|
||||
"broadcast_type": "room_rocket",
|
||||
"scope": "region",
|
||||
"app_code": "lalu",
|
||||
"region_id": 1001,
|
||||
"room_id": "lalu_room_xxx",
|
||||
"box_id": "box_1",
|
||||
"level": 3,
|
||||
"open_at_ms": 1770000000000,
|
||||
"rocket_id": "rocket_1",
|
||||
"rocket_level": 3,
|
||||
"rocket_cover_url": "https://cdn.example/rocket-l3.png",
|
||||
"launch_at_ms": 1770000000000,
|
||||
"igniter": {
|
||||
"user_id": "10001",
|
||||
"short_id": "80001",
|
||||
"nickname": "Alice",
|
||||
"avatar": "https://cdn.example/avatar.png"
|
||||
},
|
||||
"action": {
|
||||
"type": "enter_room",
|
||||
"room_id": "lalu_room_xxx"
|
||||
@ -450,7 +458,7 @@ sequenceDiagram
|
||||
}
|
||||
```
|
||||
|
||||
倒计时期间继续送礼不会累加到下一宝箱,溢出能量也不会跨级;这些规则由 room-service 在 `RoomTreasureProgressChanged` 和 `RoomTreasureCountdownStarted` 事实中表达,activity-service 不能自行推导宝箱进度。
|
||||
当前等级满阈值后,room-service 立刻把该火箭加入 `pending_launches`,并把下一等级作为当前进度继续累计;单次送礼溢出的燃料作废,不顺延。activity-service 只按 `RoomRocketIgnited` 做飘屏,不自行推导火箭进度。
|
||||
|
||||
## Red Packet Broadcast
|
||||
|
||||
@ -464,7 +472,7 @@ sequenceDiagram
|
||||
| 过期退款 | wallet/red-packet domain + cron |
|
||||
| IM 播报 | activity-service broadcast |
|
||||
|
||||
红包创建成功后,红包领域写 `RedPacketCreated` 事件,activity-service 转成区域/全局播报。客户端点击播报只打开红包入口,领取时必须调用后端接口,不能信任 IM payload 中的金额。
|
||||
红包创建成功后,红包领域写 `RedPacketCreated` 事件,activity-service 转成区域/全局播报。客户端点击播报只发射红包入口,领取时必须调用后端接口,不能信任 IM payload 中的金额。
|
||||
|
||||
## App Code And Region Safety
|
||||
|
||||
@ -496,7 +504,7 @@ sequenceDiagram
|
||||
5. `activity-service` 增加 broadcast group reconciler,启动时确保全局和 active 区域群。
|
||||
6. `activity-service` 增加 `PublishRegionBroadcast/PublishGlobalBroadcast` 和 broadcast outbox。
|
||||
7. 扩展 `RoomGiftSent` 事件字段,activity-service 消费后按阈值发 `super_gift` 区域播报。
|
||||
8. 消费 `RoomTreasureCountdownStarted`,按后台配置发 `room_treasure` 区域或全局播报。
|
||||
8. 消费 `RoomRocketIgnited`,按后台配置发 `room_rocket` 区域或全局播报。
|
||||
9. 红包领域落账务事实后,复用同一播报能力发送 `red_packet` 消息。
|
||||
|
||||
## Test Expectations
|
||||
@ -513,5 +521,5 @@ sequenceDiagram
|
||||
| group reconciler | 已存在群视为成功,失败可重试 |
|
||||
| broadcast outbox | 同一 `event_id` 幂等,失败后 retry |
|
||||
| super gift | 未达阈值不播报,达阈值发送到用户房间所在区域群 |
|
||||
| room treasure | 宝箱满能量后只按 `RoomTreasureCountdownStarted` 播报一次;区域/全局 scope 来自后台配置 |
|
||||
| room rocket | 火箭燃料满后只按 `RoomRocketIgnited` 播报一次;区域/全局 scope 来自后台配置 |
|
||||
| red packet | IM payload 不影响领取接口的后端金额校验 |
|
||||
|
||||
@ -138,7 +138,7 @@ GET /api/v1/wallet/me/balances
|
||||
示例:
|
||||
|
||||
```text
|
||||
GET /api/v1/wallet/me/balances?asset_type=COIN&asset_type=DIAMOND&asset_type=USD_BALANCE
|
||||
GET /api/v1/wallet/me/balances?asset_type=COIN&asset_type=DIAMOND&asset_type=HOST_SALARY_USD&asset_type=AGENCY_SALARY_USD&asset_type=BD_SALARY_USD&asset_type=ADMIN_SALARY_USD
|
||||
```
|
||||
|
||||
### 返回值 `data`
|
||||
@ -158,7 +158,7 @@ GET /api/v1/wallet/me/balances?asset_type=COIN&asset_type=DIAMOND&asset_type=USD
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------------------ | ------ | ------------------------------------------------- |
|
||||
| `asset_type` | string | `COIN` / `DIAMOND` / `USD_BALANCE` / `GIFT_POINT` |
|
||||
| `asset_type` | string | `COIN` / `DIAMOND` / `HOST_SALARY_USD` / `AGENCY_SALARY_USD` / `BD_SALARY_USD` / `ADMIN_SALARY_USD` / `GIFT_POINT` |
|
||||
| `available_amount` | int64 | 可用余额 |
|
||||
| `frozen_amount` | int64 | 冻结余额 |
|
||||
| `version` | int64 | 该资产余额版本,用于和 IM 通知去重/防乱序 |
|
||||
@ -201,7 +201,7 @@ GET /api/v1/wallet/me/overview
|
||||
"version": 4
|
||||
},
|
||||
{
|
||||
"asset_type": "USD_BALANCE",
|
||||
"asset_type": "HOST_SALARY_USD",
|
||||
"available_amount": 1500,
|
||||
"frozen_amount": 0,
|
||||
"version": 2
|
||||
@ -209,8 +209,7 @@ GET /api/v1/wallet/me/overview
|
||||
],
|
||||
"feature_flags": {
|
||||
"recharge_enabled": true,
|
||||
"diamond_exchange_enabled": true,
|
||||
"withdraw_enabled": true
|
||||
"diamond_exchange_enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -220,7 +219,6 @@ GET /api/v1/wallet/me/overview
|
||||
| `balances` | array | 钱包首页需要展示的资产余额 |
|
||||
| `feature_flags.recharge_enabled` | bool | 是否允许充值 |
|
||||
| `feature_flags.diamond_exchange_enabled` | bool | 是否允许钻石兑换 |
|
||||
| `feature_flags.withdraw_enabled` | bool | 是否允许提现 |
|
||||
|
||||
### 可能错误
|
||||
|
||||
|
||||
@ -101,7 +101,7 @@ Content-Type: application/json
|
||||
| `seat_count` | 否 | int | 不传或传 `0` 使用后台默认麦位数;正数必须命中后台启用座位数。 |
|
||||
| `mode` | 是 | string | 当前传 `voice`。 |
|
||||
| `room_name` | 是 | string | 房间标题,最长 128 个字符。 |
|
||||
| `room_avatar` | 否 | string | 房间头像 URL;为空时服务端写默认头像。 |
|
||||
| `room_avatar` | 是 | string | 房间封面 URL;不传或空字符串会返回 `INVALID_ARGUMENT`。 |
|
||||
| `room_description` | 否 | string | 房间简介,最长 512 个字符。 |
|
||||
|
||||
成功响应:
|
||||
@ -401,4 +401,3 @@ String newCommandId(String action) {
|
||||
| `/rooms/join` 返回 `PERMISSION_DENIED` | 可能被 ban 或锁房密码错误;不调用 `joinGroup`。 |
|
||||
| IM `joinGroup` 群不存在 | 当前不应出现;记录 `request_id`、`room_id`、`group_id` 上报。 |
|
||||
| IM 登录票据过期 | 重新调用 `/api/v1/im/usersig` 并 `login`。 |
|
||||
|
||||
|
||||
@ -115,7 +115,7 @@ Content-Type: application/json
|
||||
| `room_heat` | int64 | 送礼后的房间热度。 |
|
||||
| `gift_rank` | array | 当前房间礼物榜。 |
|
||||
| `room` | object | 房间快照。 |
|
||||
| `treasure` | object | 房间宝箱状态。 |
|
||||
| `rocket` | object | 房间火箭状态。 |
|
||||
| `lucky_gift` | object | 幸运礼物抽奖结果;普通礼物可为空。 |
|
||||
|
||||
`lucky_gift` 返回字段:
|
||||
@ -159,7 +159,7 @@ Content-Type: application/json
|
||||
"room_heat": 123456,
|
||||
"gift_rank": [],
|
||||
"room": {},
|
||||
"treasure": {},
|
||||
"rocket": {},
|
||||
"lucky_gift": {
|
||||
"enabled": true,
|
||||
"draw_id": "lucky_draw_test",
|
||||
@ -328,4 +328,3 @@ Content-Type: application/json
|
||||
| `reward_status=pending` / `granting` | 到账处理中;等待钱包通知、刷新余额或后端补偿。 |
|
||||
| `reward_status=failed` | 展示异常状态,记录 `request_id/draw_id/command_id` 方便排查。 |
|
||||
| HTTP 超时 | 保留原 `command_id` 重试,不生成新命令。 |
|
||||
|
||||
|
||||
@ -1,60 +1,45 @@
|
||||
# 房间锁房接口 Flutter 对接
|
||||
# 房间锁房 Flutter 对接
|
||||
|
||||
本文描述 Flutter App 对接房间锁房能力的 HTTP 接口。房间密码由 `room-service` 只保存 bcrypt 哈希;客户端只提交明文用于本次设置或进房校验,任何列表、搜索、详情、房间消息和响应体都不会返回明文或哈希。
|
||||
## 地址
|
||||
|
||||
## 接口地址
|
||||
|
||||
`POST /api/v1/rooms/password`
|
||||
|
||||
`POST /api/v1/rooms/join`
|
||||
|
||||
`GET /api/v1/rooms`
|
||||
|
||||
`GET /api/v1/rooms/feeds`
|
||||
|
||||
本地开发地址:
|
||||
本地:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:13000
|
||||
```
|
||||
|
||||
## 请求方法
|
||||
接口:
|
||||
|
||||
| 方法 | 接口 | 说明 |
|
||||
| 方法 | 地址 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `POST` | `/api/v1/rooms/password` | 房主设置或清空房间入房密码。 |
|
||||
| `POST` | `/api/v1/rooms/join` | 进入房间;带锁房间需要传 `password`。 |
|
||||
| `GET` | `/api/v1/rooms` | 公共房间列表和搜索;房间卡片返回 `locked`。 |
|
||||
| `GET` | `/api/v1/rooms/feeds` | Mine 页房间流;房间卡片返回 `locked`。 |
|
||||
| `POST` | `/api/v1/rooms/password` | 房主设置或清空房间密码 |
|
||||
| `POST` | `/api/v1/rooms/join` | 进入房间;锁房时必须传密码 |
|
||||
| `GET` | `/api/v1/rooms` | 房间列表和搜索,返回 `locked` |
|
||||
| `GET` | `/api/v1/rooms/feeds` | Mine 页房间流,返回 `locked` |
|
||||
|
||||
## 请求头
|
||||
请求头:
|
||||
|
||||
| Header | 必填 | 模拟值 | 说明 |
|
||||
| Header | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `Authorization` | 是 | `Bearer <access_token>` |
|
||||
| `X-App-Code` | 否 | App 编码,例如 `lalu` |
|
||||
|
||||
## 设置房间密码
|
||||
|
||||
`POST /api/v1/rooms/password`
|
||||
|
||||
参数:
|
||||
|
||||
| 字段 | 必填 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Authorization` | 是 | `Bearer <access_token>` | 登录 access token;服务端从 token 读取当前用户 ID。 |
|
||||
| `X-App-Code` | 否 | `lalu` | App 编码;登录态接口最终以 token 内的 `app_code` 为准。 |
|
||||
| `room_id` | 是 | string | 房间 ID |
|
||||
| `command_id` | 是 | string | 本次设置动作的幂等键,同一次重试复用 |
|
||||
| `locked` | 是 | bool | `true` 设置密码,`false` 清空密码 |
|
||||
| `password` | `locked=true` 必填 | string | 房间密码,服务端 trim 后最长 64 字符;`locked=false` 时忽略 |
|
||||
|
||||
## 设置或清空房间密码
|
||||
|
||||
只有房主可以调用。房管、主持人和普通观众调用会返回 `PERMISSION_DENIED`。
|
||||
|
||||
请求体:
|
||||
|
||||
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
||||
| `command_id` | 是 | string | `cmd_room_password_room_10001_1710000000` | 房间命令幂等键;同一次点击重试必须复用。 |
|
||||
| `locked` | 是 | bool | `true` | `true` 表示设置密码,`false` 表示清空密码。 |
|
||||
| `password` | `locked=true` 时必填 | string | `1234` | 入房密码;服务端 trim 后最长 64 个字符。`locked=false` 时忽略。 |
|
||||
|
||||
设置密码请求示例:
|
||||
|
||||
```http
|
||||
POST /api/v1/rooms/password
|
||||
Authorization: Bearer <access_token>
|
||||
X-App-Code: lalu
|
||||
Content-Type: application/json
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"room_id": "room_10001",
|
||||
"command_id": "cmd_room_password_room_10001_1710000000",
|
||||
@ -63,22 +48,7 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
清空密码请求示例:
|
||||
|
||||
```http
|
||||
POST /api/v1/rooms/password
|
||||
Authorization: Bearer <access_token>
|
||||
X-App-Code: lalu
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"room_id": "room_10001",
|
||||
"command_id": "cmd_room_password_room_10001_1710000100",
|
||||
"locked": false
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
@ -114,42 +84,56 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
返回字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `data.result.applied` | bool | 本次命令是否产生状态变更;重复提交相同命令可能为 `false`。 |
|
||||
| `data.result.room_version` | int64 | 命令完成后房间版本。 |
|
||||
| `data.room.locked` | bool | 最新锁房状态;`true` 表示后续新用户进房需要密码。 |
|
||||
| `data.room.version` | int64 | 最新房间快照版本;客户端本地房间状态以更大版本覆盖旧状态。 |
|
||||
| `data.seats` | array | 当前座位列表;锁房接口成功后可直接复用该房间快照刷新页面。 |
|
||||
|
||||
## 带密码进入房间
|
||||
|
||||
客户端进入房间前先看房间卡片或详情里的 `locked`:
|
||||
|
||||
| `locked` | 客户端行为 |
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `false` | 直接调用 JoinRoom,`password` 可不传或传空字符串。 |
|
||||
| `true` | 先弹出密码输入,再调用 JoinRoom 并传 `password`。 |
|
||||
| `data.result.applied` | 本次命令是否改变房间状态 |
|
||||
| `data.result.room_version` | 房间最新版本 |
|
||||
| `data.room.locked` | 最新锁房状态 |
|
||||
| `data.room.version` | 房间快照版本 |
|
||||
| `data.seats` | 当前麦位列表 |
|
||||
|
||||
请求体:
|
||||
## 清空房间密码
|
||||
|
||||
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
||||
| `command_id` | 是 | string | `cmd_join_room_10001_20001_1710000000` | 进房命令幂等键;同一次进房重试必须复用。 |
|
||||
| `role` | 否 | string | `audience` | 当前固定传 `audience`。 |
|
||||
| `password` | 锁房时必填 | string | `1234` | 只用于本次进房校验,服务端不保存明文。 |
|
||||
`POST /api/v1/rooms/password`
|
||||
|
||||
请求示例:
|
||||
参数:
|
||||
|
||||
```http
|
||||
POST /api/v1/rooms/join
|
||||
Authorization: Bearer <access_token>
|
||||
X-App-Code: lalu
|
||||
Content-Type: application/json
|
||||
| 字段 | 必填 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `room_id` | 是 | string | 房间 ID |
|
||||
| `command_id` | 是 | string | 本次清空动作的幂等键 |
|
||||
| `locked` | 是 | bool | 固定传 `false` |
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"room_id": "room_10001",
|
||||
"command_id": "cmd_room_unlock_room_10001_1710000100",
|
||||
"locked": false
|
||||
}
|
||||
```
|
||||
|
||||
返回同设置密码接口,`data.room.locked=false`。
|
||||
|
||||
## 进入锁房房间
|
||||
|
||||
`POST /api/v1/rooms/join`
|
||||
|
||||
参数:
|
||||
|
||||
| 字段 | 必填 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `room_id` | 是 | string | 房间 ID |
|
||||
| `command_id` | 是 | string | 本次进房动作的幂等键,同一次重试复用 |
|
||||
| `role` | 否 | string | 当前传 `audience` |
|
||||
| `password` | 锁房时必填 | string | 本次入房密码 |
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"room_id": "room_10001",
|
||||
"command_id": "cmd_join_room_10001_20001_1710000000",
|
||||
@ -158,7 +142,7 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
成功响应只展示锁房相关字段:
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
@ -196,35 +180,33 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
返回字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `data.room.locked` | 当前房间是否锁房 |
|
||||
| `data.viewer.user_id` | 当前用户 ID |
|
||||
| `data.im.group_id` | JoinRoom 成功后需要加入的腾讯 IM 房间群 |
|
||||
| `data.im.need_join_group` | `true` 时 Flutter 调腾讯 IM `joinGroup` |
|
||||
| `data.rtc.available` | 是否拿到 RTC token |
|
||||
|
||||
进房规则:
|
||||
|
||||
| 场景 | 结果 |
|
||||
| --- | --- |
|
||||
| 房间未锁 | 不校验 `password`。 |
|
||||
| 房间已锁且密码正确 | JoinRoom 成功,客户端再调用腾讯 IM `joinGroup` 和 RTC `enterRoom`。 |
|
||||
| 房间已锁且密码错误或缺失 | 返回 `403 PERMISSION_DENIED`,不会创建 presence,不会返回 RTC token。 |
|
||||
| 房主进入自己的房间 | 不需要密码。 |
|
||||
| 用户已经在房间内 | 再次 JoinRoom 按幂等恢复当前房间态,不会因为后续锁房被踢出。 |
|
||||
| 未锁房 | `password` 可不传 |
|
||||
| 已锁房且密码正确 | JoinRoom 成功,再加入 IM 房间群 |
|
||||
| 已锁房且密码错误或缺失 | 返回 `403 PERMISSION_DENIED`,不要加入 IM 房间群 |
|
||||
| 房主进自己房间 | 不需要密码 |
|
||||
| 已在房间内重复 JoinRoom | 非房主仍要按当前最新密码校验;密码错误返回 `403 PERMISSION_DENIED` |
|
||||
|
||||
## 房间列表和搜索 locked 字段
|
||||
## 房间列表 locked 字段
|
||||
|
||||
公共列表和搜索使用同一个接口:
|
||||
`GET /api/v1/rooms?tab=hot&query=100001`
|
||||
|
||||
```http
|
||||
GET /api/v1/rooms?tab=hot&query=100001
|
||||
Authorization: Bearer <access_token>
|
||||
X-App-Code: lalu
|
||||
```
|
||||
`GET /api/v1/rooms/feeds?tab=followed`
|
||||
|
||||
Mine 页房间流:
|
||||
|
||||
```http
|
||||
GET /api/v1/rooms/feeds?tab=followed
|
||||
Authorization: Bearer <access_token>
|
||||
X-App-Code: lalu
|
||||
```
|
||||
|
||||
列表响应示例:
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
@ -236,18 +218,10 @@ X-App-Code: lalu
|
||||
{
|
||||
"room_id": "room_10001",
|
||||
"im_group_id": "room_10001",
|
||||
"owner_user_id": "10001",
|
||||
"title": "Live Room",
|
||||
"cover_url": "https://cdn.example/room.png",
|
||||
"mode": "voice",
|
||||
"status": "active",
|
||||
"locked": true,
|
||||
"heat": 1000,
|
||||
"online_count": 12,
|
||||
"seat_count": 15,
|
||||
"occupied_seat_count": 2,
|
||||
"visible_region_id": 1,
|
||||
"app_code": "lalu",
|
||||
"room_short_id": "100001"
|
||||
}
|
||||
],
|
||||
@ -256,17 +230,87 @@ X-App-Code: lalu
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `rooms[].locked` | bool | 房间是否需要入房密码;搜索结果和普通列表一致。 |
|
||||
| `rooms[].im_group_id` | string | 腾讯 IM 群 ID;只用于 JoinRoom 成功后的 `joinGroup`,不能绕过 JoinRoom。 |
|
||||
| `next_cursor` | string | 下一页游标;为空表示没有下一页。 |
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `rooms[].locked` | 是否锁房;`true` 时进房前弹密码输入 |
|
||||
| `rooms[].im_group_id` | 房间 IM 群 ID,只能在 JoinRoom 成功后使用 |
|
||||
| `rooms[].visible_region_id` | 房间区域 |
|
||||
|
||||
## 错误处理
|
||||
## 相关 IM
|
||||
|
||||
失败返回仍使用统一 envelope:
|
||||
### JoinRoom 成功后加入房间群
|
||||
|
||||
接口 `POST /api/v1/rooms/join` 成功后,Flutter 使用:
|
||||
|
||||
```text
|
||||
data.im.group_id
|
||||
```
|
||||
|
||||
调用腾讯 IM SDK 加入房间群。
|
||||
|
||||
规则:
|
||||
|
||||
| 场景 | Flutter 行为 |
|
||||
| --- | --- |
|
||||
| JoinRoom 成功 | 调腾讯 IM `joinGroup(data.im.group_id)` |
|
||||
| JoinRoom 返回 `PERMISSION_DENIED` | 不调用 `joinGroup` |
|
||||
| JoinRoom 返回 `ROOM_CLOSED` 或 `NOT_FOUND` | 不调用 `joinGroup`,刷新列表 |
|
||||
|
||||
### 房间密码变更区域通知
|
||||
|
||||
房主设置或清空密码后,服务端会发区域播报 IM,不再发房间群 IM。
|
||||
|
||||
Flutter 需要监听用户所在区域播报群的自定义消息:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "room_password_changed:evt_room_xxx",
|
||||
"broadcast_type": "room_password_changed",
|
||||
"scope": "region",
|
||||
"app_code": "lalu",
|
||||
"region_id": 1,
|
||||
"room_id": "room_10001",
|
||||
"actor_user_id": 10001,
|
||||
"locked": true,
|
||||
"sent_at_ms": 1710000000456,
|
||||
"room_version": 12,
|
||||
"source_event_id": "evt_room_xxx",
|
||||
"action": {
|
||||
"type": "refresh_room",
|
||||
"room_id": "room_10001"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `broadcast_type` | 固定 `room_password_changed` |
|
||||
| `scope` | 固定 `region` |
|
||||
| `region_id` | 区域 ID |
|
||||
| `room_id` | 发生变化的房间 |
|
||||
| `locked` | 最新锁房状态 |
|
||||
| `room_version` | 房间版本;本地只用更大的版本覆盖旧状态 |
|
||||
| `action.type` | 固定 `refresh_room` |
|
||||
|
||||
处理:
|
||||
|
||||
| 当前页面 | 行为 |
|
||||
| --- | --- |
|
||||
| 房间列表页 | 更新对应房间卡片 `locked` |
|
||||
| 房间详情页 | 如果 `room_id` 相同,刷新房间快照或更新本地 `locked` |
|
||||
| 密码弹窗打开中 | 收到同房间更新后,按最新 `locked` 状态决定是否关闭弹窗 |
|
||||
|
||||
### 房间群系统消息
|
||||
|
||||
`room_password_changed` 不走房间群 IM。房间内用户刷新锁房状态依赖区域播报或接口返回的 `room.locked`。
|
||||
|
||||
## 错误返回
|
||||
|
||||
失败统一返回:
|
||||
|
||||
```json
|
||||
{
|
||||
@ -276,108 +320,13 @@ X-App-Code: lalu
|
||||
}
|
||||
```
|
||||
|
||||
| HTTP 状态码 | `code` | 典型场景 | 处理方式 |
|
||||
| --- | --- | --- | --- |
|
||||
| `400` | `INVALID_ARGUMENT` | `room_id`、`command_id` 缺失;设置锁房时 `password` 为空或超过 64 字符。 | 客户端修正参数,不自动重试。 |
|
||||
| `401` | `UNAUTHORIZED` | token 缺失、无效、过期或会话失效。 | 重新登录。 |
|
||||
| `403` | `PROFILE_REQUIRED` | 用户资料未完成。 | 跳转资料补全流程。 |
|
||||
| `403` | `PERMISSION_DENIED` | 非房主设置密码;进锁房密码错误;用户被 ban。 | 设置密码时隐藏入口或提示无权限;进房时提示密码错误。 |
|
||||
| `404` | `NOT_FOUND` | 房间不存在。 | 刷新列表或退出房间页。 |
|
||||
| `409` | `ROOM_CLOSED` | 房间已关闭。 | 刷新列表或退出房间页。 |
|
||||
| `409` | `CONFLICT` | 同一个 `command_id` 被不同请求体复用。 | 为新的用户动作生成新的 `command_id`。 |
|
||||
| `502` | `UPSTREAM_ERROR` | 内部服务暂不可用。 | 可提示稍后重试。 |
|
||||
常见错误:
|
||||
|
||||
幂等规则:
|
||||
|
||||
| 场景 | 返回 |
|
||||
| --- | --- |
|
||||
| 同一个 `command_id` 重试同一次设置密码 | 返回同一命令结果,不重复变更房间。 |
|
||||
| 同一个 `command_id` 换另一个密码 | `409 CONFLICT`。 |
|
||||
| 重复清空已解锁房间 | `200 OK`,`data.room.locked=false`。 |
|
||||
| 同一个用户重复 JoinRoom | 返回当前房间首屏数据,不重复创建 presence。 |
|
||||
|
||||
## Flutter 解析示例
|
||||
|
||||
```dart
|
||||
class RoomCommandResult {
|
||||
RoomCommandResult({
|
||||
required this.applied,
|
||||
required this.roomVersion,
|
||||
required this.serverTimeMs,
|
||||
});
|
||||
|
||||
final bool applied;
|
||||
final int roomVersion;
|
||||
final int serverTimeMs;
|
||||
|
||||
factory RoomCommandResult.fromJson(Map<String, dynamic> json) {
|
||||
return RoomCommandResult(
|
||||
applied: json['applied'] as bool? ?? false,
|
||||
roomVersion: (json['room_version'] as num?)?.toInt() ?? 0,
|
||||
serverTimeMs: (json['server_time_ms'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomCard {
|
||||
RoomCard({
|
||||
required this.roomId,
|
||||
required this.imGroupId,
|
||||
required this.title,
|
||||
required this.locked,
|
||||
required this.onlineCount,
|
||||
});
|
||||
|
||||
final String roomId;
|
||||
final String imGroupId;
|
||||
final String title;
|
||||
final bool locked;
|
||||
final int onlineCount;
|
||||
|
||||
factory RoomCard.fromJson(Map<String, dynamic> json) {
|
||||
return RoomCard(
|
||||
roomId: json['room_id'] as String? ?? '',
|
||||
imGroupId: json['im_group_id'] as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
locked: json['locked'] as bool? ?? false,
|
||||
onlineCount: (json['online_count'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomLockResult {
|
||||
RoomLockResult({
|
||||
required this.result,
|
||||
required this.room,
|
||||
required this.serverTimeMs,
|
||||
});
|
||||
|
||||
final RoomCommandResult result;
|
||||
final RoomCard room;
|
||||
final int serverTimeMs;
|
||||
|
||||
factory RoomLockResult.fromJson(Map<String, dynamic> json) {
|
||||
return RoomLockResult(
|
||||
result: RoomCommandResult.fromJson(
|
||||
json['result'] as Map<String, dynamic>? ?? const {},
|
||||
),
|
||||
room: RoomCard.fromJson(
|
||||
json['room'] as Map<String, dynamic>? ?? const {},
|
||||
),
|
||||
serverTimeMs: (json['server_time_ms'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
调用时先解析外层 envelope:只有 `code == "OK"` 时读取 `data`;其他 `code` 按登录态、参数错误、权限错误或服务错误处理,并把 `request_id` 写入客户端日志。
|
||||
|
||||
客户端生成 `command_id` 时按一次用户动作生成一次,例如:
|
||||
|
||||
```dart
|
||||
String newRoomCommandId(String action, String roomId, String userId) {
|
||||
return 'cmd_${action}_${roomId}_${userId}_${DateTime.now().millisecondsSinceEpoch}';
|
||||
}
|
||||
```
|
||||
|
||||
同一次 HTTP 重试复用原 `command_id`;用户重新输入密码或再次点击设置按钮时生成新的 `command_id`。
|
||||
| HTTP | code | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `400` | `INVALID_ARGUMENT` | 参数缺失、设置密码时密码为空或超过 64 字符 |
|
||||
| `401` | `UNAUTHORIZED` | token 无效或过期 |
|
||||
| `403` | `PERMISSION_DENIED` | 非房主设置密码、进房密码错误、用户被 ban |
|
||||
| `404` | `NOT_FOUND` | 房间不存在 |
|
||||
| `409` | `ROOM_CLOSED` | 房间已关闭 |
|
||||
| `409` | `CONFLICT` | 同一个 `command_id` 被不同请求体复用 |
|
||||
|
||||
@ -20,11 +20,11 @@
|
||||
|
||||
1. 用户进入房间并完成 `JoinRoom`。
|
||||
2. App 可在启动或进入房间前调用 `GET /api/v1/gift-tabs` 预加载 Tab 配置。
|
||||
3. 打开礼物 Tab 时调用 `GET /api/v1/rooms/{room_id}/gift-panel`。
|
||||
3. 发射礼物 Tab 时调用 `GET /api/v1/rooms/{room_id}/gift-panel`。
|
||||
4. 用 `tabs` 渲染当前房间分类,用 `gifts` 渲染礼物网格,用 `recipients` 渲染收礼人。
|
||||
5. 用户选择收礼人、礼物和数量。
|
||||
6. 点击发送时生成稳定 `command_id`,调用 `POST /api/v1/rooms/gift/send`。
|
||||
7. 发送成功后用响应里的 `room_heat`、`gift_rank`、`treasure` 更新房间 UI;如果有 `lucky_gift` 且已入账,用 `coin_balance_after` 立即刷新金币余额。
|
||||
7. 发送成功后用响应里的 `room_heat`、`gift_rank`、`rocket` 更新房间 UI;如果有 `lucky_gift` 且已入账,用 `coin_balance_after` 立即刷新金币余额。
|
||||
8. 监听房间 IM `room_gift_sent`,同步播放礼物动效。
|
||||
9. 普通礼物和未中奖余额仍监听私有钱包通知或主动刷新余额,不本地扣减余额作为最终值。
|
||||
|
||||
@ -202,7 +202,7 @@ X-App-Code: lalu
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `coin_balance` | 当前登录用户 COIN 可用余额快照。只用于打开面板时展示,最终余额仍以后续钱包事实为准。 |
|
||||
| `coin_balance` | 当前登录用户 COIN 可用余额快照。只用于发射面板时展示,最终余额仍以后续钱包事实为准。 |
|
||||
| `recipients` | 当前房间可收礼人。当前主流程只启用 `target_type=user` 的单个用户。 |
|
||||
| `tabs` | 礼物分类。`key=all` 是全量分类;其他 tab 通常对应 `gift_type_code`。 |
|
||||
| `gifts` | 当前房间国家维度可用的 active 礼物列表。 |
|
||||
@ -295,7 +295,7 @@ Content-Type: application/json
|
||||
}
|
||||
],
|
||||
"room": {},
|
||||
"treasure": {},
|
||||
"rocket": {},
|
||||
"lucky_gift": {
|
||||
"enabled": true,
|
||||
"draw_id": "lucky_draw_test",
|
||||
@ -329,7 +329,7 @@ Content-Type: application/json
|
||||
| `room_heat` | 最新房间热度。 |
|
||||
| `gift_rank` | 最新房间礼物榜。 |
|
||||
| `room` | 房间快照,字段较大;App 可按已有房间快照解析逻辑局部更新。 |
|
||||
| `treasure` | 房间宝箱状态;宝箱 UI 以该字段或宝箱 IM 事件更新。 |
|
||||
| `rocket` | 房间火箭状态;火箭 UI 以该字段或火箭 IM 事件更新。 |
|
||||
| `lucky_gift` | 幸运礼物抽奖结果;普通礼物可为空。`reward_status=granted` 且中奖时,`coin_balance_after` 是当前用户返奖入账后的余额。 |
|
||||
|
||||
发送后处理:
|
||||
|
||||
@ -1,611 +0,0 @@
|
||||
# 语音房宝箱 Flutter 对接
|
||||
|
||||
本文描述 Flutter App 对接语音房宝箱所需的 HTTP 接口、请求参数、返回值、错误处理和腾讯云 IM 自定义消息 key。宝箱进度、开箱和发奖事实由 `room-service` Room Cell 持有;客户端只展示服务端返回或 IM 推送的状态,不能本地计算能量、奖励或开箱结果。
|
||||
|
||||
## 基础约定
|
||||
|
||||
本地开发地址:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:13000
|
||||
```
|
||||
|
||||
业务接口统一响应 envelope:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "ok",
|
||||
"request_id": "req_xxx",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `code` | 成功固定 `OK`;失败为稳定错误码。 |
|
||||
| `message` | 短错误文案,不用于客户端分支判断。 |
|
||||
| `request_id` | 服务端链路追踪 ID;客户端日志要记录。 |
|
||||
| `data` | 成功业务数据;失败时通常不存在。 |
|
||||
|
||||
通用请求头:
|
||||
|
||||
| Header | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `Authorization` | 是 | `Bearer <access_token>`。 |
|
||||
| `X-App-Code` | 否 | App 编码,默认 `lalu`;登录态接口最终以 token 内 app_code 为准。 |
|
||||
| `Content-Type` | POST 必填 | `application/json`。 |
|
||||
|
||||
所有 `*_ms` 都是 Unix epoch milliseconds。业务切日按 UTC,不按用户本地时区。
|
||||
|
||||
## 客户端流程
|
||||
|
||||
1. 用户成功 `JoinRoom` 后调用 `GET /api/v1/rooms/{room_id}/treasure` 初始化 7 级物料和当前进度。
|
||||
2. 用户送礼调用 `POST /api/v1/rooms/gift/send`;成功响应里的 `data.treasure` 可立即更新当前宝箱状态。
|
||||
3. 房间内监听腾讯 IM 群 `TIMCustomElem.Ext=room_system_message`,处理宝箱进度、倒计时、开箱和发奖。
|
||||
4. App 全局或区域播报监听 `TIMCustomElem.Ext=im_broadcast` 且 `broadcast_type=room_treasure`。
|
||||
5. 任意 IM 丢失、字段解析失败、版本倒退或页面重进时,重新调用 `GET /api/v1/rooms/{room_id}/treasure` 纠正本地状态。
|
||||
|
||||
客户端必须用 `event_id` 去重,用 `room_version` 丢弃旧房间消息。
|
||||
|
||||
## 获取宝箱信息
|
||||
|
||||
```http
|
||||
GET /api/v1/rooms/{room_id}/treasure
|
||||
Authorization: Bearer <access_token>
|
||||
X-App-Code: lalu
|
||||
```
|
||||
|
||||
路径参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `room_id` | string | 是 | 当前语音房 ID,例如 `lalu_534d076a-7cbd-4b23-a297-0c10a3c2fa72`。 |
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "ok",
|
||||
"request_id": "req_xxx",
|
||||
"data": {
|
||||
"enabled": true,
|
||||
"levels": [
|
||||
{
|
||||
"level": 1,
|
||||
"energy_threshold": 1000,
|
||||
"cover_url": "https://cdn.example/chest/lv1/cover.png",
|
||||
"animation_url": "https://cdn.example/chest/lv1/idle.svga",
|
||||
"opening_animation_url": "https://cdn.example/chest/lv1/opening.svga",
|
||||
"opened_image_url": "https://cdn.example/chest/lv1/opened.png",
|
||||
"in_room_rewards": [
|
||||
{
|
||||
"reward_item_id": "lv1_in_room_1",
|
||||
"resource_group_id": 1001,
|
||||
"weight": 10000,
|
||||
"display_name": "Gold Pack",
|
||||
"icon_url": "https://cdn.example/reward/gold.png"
|
||||
}
|
||||
],
|
||||
"top1_rewards": [],
|
||||
"igniter_rewards": []
|
||||
}
|
||||
],
|
||||
"state": {
|
||||
"current_level": 1,
|
||||
"current_progress": 0,
|
||||
"energy_threshold": 1000,
|
||||
"status": "idle",
|
||||
"reset_at_ms": 1779321600000,
|
||||
"top1_user_id": "",
|
||||
"igniter_user_id": "",
|
||||
"box_id": "",
|
||||
"config_version": 3,
|
||||
"last_rewards": []
|
||||
},
|
||||
"broadcast_scope": "region",
|
||||
"open_delay_ms": 30000,
|
||||
"broadcast_delay_ms": 0,
|
||||
"reward_stack_policy": "allow_stack",
|
||||
"server_time_ms": 1779259000000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`data` 字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `enabled` | bool | 宝箱总开关。`false` 时客户端隐藏或置灰宝箱入口。 |
|
||||
| `levels` | array | 固定 7 个等级的物料、阈值和奖励池。 |
|
||||
| `state` | object | 当前房间当前 UTC 日的宝箱状态。 |
|
||||
| `broadcast_scope` | string | `none`、`region`、`global`。 |
|
||||
| `open_delay_ms` | int64 | 满能量后到开箱的倒计时配置。 |
|
||||
| `broadcast_delay_ms` | int64 | 后台配置字段;当前房间事件会立即进入播报链路。 |
|
||||
| `reward_stack_policy` | string | `allow_stack` 或 `priority_only`。 |
|
||||
| `server_time_ms` | int64 | 服务端当前时间,倒计时要以它校准本地时钟偏差。 |
|
||||
|
||||
`levels[]` 字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `level` | int32 | 等级,1 到 7。 |
|
||||
| `energy_threshold` | int64 | 当前等级满能量阈值。 |
|
||||
| `cover_url` | string | 静态封面图。 |
|
||||
| `animation_url` | string | 未开箱/蓄能态动效。 |
|
||||
| `opening_animation_url` | string | 开箱动效。 |
|
||||
| `opened_image_url` | string | 开箱后展示图。 |
|
||||
| `in_room_rewards` | array | 开箱时仍在房用户的奖励候选展示。 |
|
||||
| `top1_rewards` | array | 倒计时开始时贡献第一用户奖励候选展示。 |
|
||||
| `igniter_rewards` | array | 点火人奖励候选展示。 |
|
||||
|
||||
奖励候选字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `reward_item_id` | string | 后台配置的奖励项 ID。 |
|
||||
| `resource_group_id` | int64 | 钱包资源组 ID。 |
|
||||
| `weight` | int64 | 抽奖权重,只用于展示或调试,不参与客户端抽奖。 |
|
||||
| `display_name` | string | 奖励展示名称。 |
|
||||
| `icon_url` | string | 奖励图标。 |
|
||||
|
||||
`state` 字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `current_level` | int32 | 当前等级,1 到 7。 |
|
||||
| `current_progress` | int64 | 当前等级已累计有效能量。 |
|
||||
| `energy_threshold` | int64 | 当前等级阈值。 |
|
||||
| `status` | string | 当前返回 `idle` 或 `countdown`。 |
|
||||
| `countdown_started_at_ms` | int64 | 倒计时开始时间;非倒计时可为空或 0。 |
|
||||
| `open_at_ms` | int64 | 预计开箱时间;`status=countdown` 时有效。 |
|
||||
| `opened_at_ms` | int64 | 最近一次开箱时间。 |
|
||||
| `reset_at_ms` | int64 | 下一个 UTC 0 点;到点后进度按新 UTC 日重置。 |
|
||||
| `top1_user_id` | string | 倒计时开始时锁定的贡献第一;GET 接口中按字符串返回。 |
|
||||
| `igniter_user_id` | string | 点火人;GET 接口中按字符串返回。 |
|
||||
| `box_id` | string | 当前倒计时宝箱或最近状态的单轮幂等 ID。 |
|
||||
| `config_version` | int64 | 当前状态关联的后台配置版本。 |
|
||||
| `last_rewards` | array | 最近一次开箱发奖结果;客户端可用于展示开箱结果兜底。 |
|
||||
|
||||
`last_rewards[]` 字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `reward_role` | string | `in_room`、`top1`、`igniter`。 |
|
||||
| `user_id` | string | 获奖用户 ID 字符串。 |
|
||||
| `reward_item_id` | string | 奖励项 ID。 |
|
||||
| `resource_group_id` | int64 | 资源组 ID。 |
|
||||
| `display_name` | string | 奖励展示名称。 |
|
||||
| `icon_url` | string | 奖励图标。 |
|
||||
| `grant_id` | string | 钱包发放记录 ID。 |
|
||||
| `status` | string | 当前成功发放为 `succeeded`。 |
|
||||
|
||||
展示规则:
|
||||
|
||||
- 进度条使用 `current_progress / energy_threshold`,并且最大显示 100%。
|
||||
- 倒计时使用 `open_at_ms - server_time_ms` 初始化,再用本地 timer 驱动。
|
||||
- 不要把送礼溢出能量展示到下一等级;服务端规则是溢出作废。
|
||||
- 如果 `status=countdown`,继续送礼不会改变宝箱进度、点火人、贡献第一或 `open_at_ms`。
|
||||
|
||||
## 送礼并更新宝箱
|
||||
|
||||
```http
|
||||
POST /api/v1/rooms/gift/send
|
||||
Authorization: Bearer <access_token>
|
||||
X-App-Code: lalu
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"room_id": "lalu_xxx",
|
||||
"command_id": "cmd_send_gift_lalu_xxx_12345678_1779259000000",
|
||||
"target_type": "user",
|
||||
"target_user_ids": [12345678],
|
||||
"gift_id": "rocket",
|
||||
"gift_count": 1
|
||||
}
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `room_id` | string | 是 | 当前房间 ID。 |
|
||||
| `command_id` | string | 是 | 本次送礼动作幂等键;同一次 HTTP 重试必须复用。 |
|
||||
| `target_type` | string | 否 | 当前只支持 `user`;为空按 `user` 处理。 |
|
||||
| `target_user_ids` | int64[] | 是 | 收礼用户列表;`target_type=user` 时必须且只能 1 个。 |
|
||||
| `target_user_id` | int64 | 否 | 旧客户端兼容字段;新客户端优先用 `target_user_ids`。 |
|
||||
| `gift_id` | string | 是 | 礼物 ID。 |
|
||||
| `gift_count` | int32 | 是 | 礼物数量,必须大于 0。 |
|
||||
|
||||
成功响应核心字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "ok",
|
||||
"request_id": "req_xxx",
|
||||
"data": {
|
||||
"result": {
|
||||
"applied": true,
|
||||
"room_version": 7,
|
||||
"server_time_ms": 1779259502200
|
||||
},
|
||||
"billing_receipt_id": "bill_xxx",
|
||||
"room_heat": 500,
|
||||
"gift_rank": [
|
||||
{
|
||||
"user_id": 12345678,
|
||||
"score": 500,
|
||||
"gift_value": 500,
|
||||
"updated_at_ms": 1779259502104
|
||||
}
|
||||
],
|
||||
"room": {},
|
||||
"treasure": {
|
||||
"current_level": 2,
|
||||
"current_progress": 200,
|
||||
"energy_threshold": 200,
|
||||
"status": "countdown",
|
||||
"countdown_started_at_ms": 1779259502104,
|
||||
"open_at_ms": 1779259512104,
|
||||
"reset_at_ms": 1779321600000,
|
||||
"top1_user_id": 12345678,
|
||||
"igniter_user_id": 12345678,
|
||||
"box_id": "room_treasure_box_xxx",
|
||||
"config_version": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `data.result.applied` | bool | 命令是否产生新状态;同一 `command_id` 重试可能返回历史结果。 |
|
||||
| `data.result.room_version` | int64 | 房间版本,用于和 IM 消息去重、排序。 |
|
||||
| `data.billing_receipt_id` | string | 钱包扣费回执。 |
|
||||
| `data.room_heat` | int64 | 送礼后的房间热度。 |
|
||||
| `data.gift_rank` | array | 送礼后的本地贡献榜快照。 |
|
||||
| `data.room` | object | 房间快照。 |
|
||||
| `data.treasure` | object | 送礼后宝箱状态;送礼人可立即用它刷新 UI。 |
|
||||
|
||||
注意:`GET /treasure` 的用户 ID 字段按字符串返回,`POST /gift/send` 透出的 protobuf 状态里部分用户 ID 可能是数字。Flutter 侧建议用统一 helper 兼容 `String` 和 `num`。
|
||||
|
||||
```dart
|
||||
String readId(dynamic value) {
|
||||
if (value == null) return '';
|
||||
if (value is String) return value;
|
||||
if (value is num) return value.toInt().toString();
|
||||
return '';
|
||||
}
|
||||
```
|
||||
|
||||
幂等规则:
|
||||
|
||||
| 场景 | 处理 |
|
||||
| --- | --- |
|
||||
| HTTP 超时后重试同一次送礼 | 复用原 `command_id`。 |
|
||||
| 用户点了第二次送礼按钮 | 生成新的 `command_id`。 |
|
||||
| 同一个 `command_id` 换了礼物、数量或目标 | 服务端返回冲突错误。 |
|
||||
|
||||
## 错误处理
|
||||
|
||||
失败响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "PERMISSION_DENIED",
|
||||
"message": "permission denied",
|
||||
"request_id": "req_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
通用错误:
|
||||
|
||||
| HTTP | `code` | 场景 | Flutter 处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| 400 | `INVALID_JSON` | 请求体不是合法 JSON。 | 修正客户端请求,不重试。 |
|
||||
| 400 | `INVALID_ARGUMENT` | 缺字段、数量非法、room_id 格式非法、暂不支持的 `target_type`。 | 修正参数,不自动重试。 |
|
||||
| 401 | `UNAUTHORIZED` | token 缺失、过期或无效。 | 走登录刷新或重新登录。 |
|
||||
| 403 | `PROFILE_REQUIRED` | 用户资料未完成。 | 跳转资料补全流程。 |
|
||||
| 403 | `PERMISSION_DENIED` | 不在房间、被 ban、无权限读取或操作。 | 退出房间页或刷新房间状态。 |
|
||||
| 404 | `NOT_FOUND` | 房间不存在、送礼人不在房、收礼人不在房、礼物不可用。 | 刷新房间状态;礼物不可用时刷新礼物面板。 |
|
||||
| 409 | `ROOM_CLOSED` | 房间已关闭。 | 退出房间页并刷新列表。 |
|
||||
| 409 | `CONFLICT` | 状态或唯一约束冲突。 | 刷新后重试。 |
|
||||
| 409 | `IDEMPOTENCY_CONFLICT` | `command_id` 复用但请求内容不同。 | 新用户动作生成新 `command_id`;重试旧动作必须保持原请求体。 |
|
||||
| 409 | `INSUFFICIENT_BALANCE` | 钱包余额不足。 | 弹充值/余额不足提示。 |
|
||||
| 409 | `LEDGER_CONFLICT` | 钱包并发账本冲突。 | 可短延迟重试一次,仍失败则刷新余额。 |
|
||||
| 502 | `UPSTREAM_ERROR` | room/wallet 等依赖暂不可用。 | 提示稍后重试,可保留原 `command_id` 重试同一次送礼。 |
|
||||
| 500 | `INTERNAL_ERROR` | 服务端内部错误。 | 提示失败并记录 `request_id`。 |
|
||||
|
||||
宝箱相关额外规则:
|
||||
|
||||
- 获取宝箱信息要求当前用户仍在房间业务 presence 内;离房后调用会返回 `PERMISSION_DENIED` 或 `NOT_FOUND`。
|
||||
- 宝箱关闭时,`GET /treasure` 仍可能返回 `enabled=false` 和 7 级配置;客户端不展示进度入口即可。
|
||||
- 发奖自动完成,没有 App 领取接口;客户端不要补发或重复调用钱包接口。
|
||||
|
||||
## 房间群 IM 消息
|
||||
|
||||
房间内宝箱进度、倒计时、开箱和发奖通过腾讯云 IM 房间群 `TIMCustomElem` 投递。
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| `MsgType` | `TIMCustomElem` |
|
||||
| `MsgContent.Ext` | `room_system_message` |
|
||||
| `MsgContent.Desc` | 等于 `Data.event_type` |
|
||||
| `MsgContent.Data` | JSON 字符串 |
|
||||
| `CloudCustomData` | 与 `MsgContent.Data` 相同的 JSON 字符串 |
|
||||
|
||||
`Data` 外层结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "evt_xxx",
|
||||
"room_id": "lalu_xxx",
|
||||
"event_type": "room_treasure_progress_changed",
|
||||
"actor_user_id": 12345678,
|
||||
"target_user_id": 0,
|
||||
"room_version": 8,
|
||||
"attributes": {
|
||||
"box_id": "room_treasure_box_xxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通用字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `event_id` | string | IM 去重键。 |
|
||||
| `room_id` | string | 房间 ID。 |
|
||||
| `event_type` | string | 事件类型,见下表。 |
|
||||
| `actor_user_id` | int64 | 触发用户;倒计时/开箱通常为点火人。 |
|
||||
| `target_user_id` | int64 | 目标用户;倒计时/开箱通常为 top1。 |
|
||||
| `room_version` | int64 | 房间版本,本地版本更高时丢弃旧消息。 |
|
||||
| `attributes` | map<string,string> | 业务字段;值全部按字符串传输,Flutter 侧按需转 int/bool/JSON。 |
|
||||
|
||||
宝箱房间群事件:
|
||||
|
||||
| `event_type` | `Desc` | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `room_treasure_progress_changed` | `room_treasure_progress_changed` | 有效宝箱能量增加。 |
|
||||
| `room_treasure_countdown_started` | `room_treasure_countdown_started` | 当前等级满能量,进入倒计时。 |
|
||||
| `room_treasure_opened` | `room_treasure_opened` | 宝箱已打开,包含公开奖励摘要。 |
|
||||
| `room_treasure_reward_granted` | `room_treasure_reward_granted` | 发奖结果事件,包含奖励列表。 |
|
||||
|
||||
### room_treasure_progress_changed
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "evt_progress",
|
||||
"room_id": "lalu_xxx",
|
||||
"event_type": "room_treasure_progress_changed",
|
||||
"actor_user_id": 12345678,
|
||||
"room_version": 6,
|
||||
"attributes": {
|
||||
"box_id": "room_treasure_box_xxx",
|
||||
"level": "2",
|
||||
"added_energy": "300",
|
||||
"effective_added_energy": "200",
|
||||
"overflow_energy": "100",
|
||||
"current_progress": "200",
|
||||
"energy_threshold": "200",
|
||||
"status": "countdown",
|
||||
"reset_at_ms": "1779321600000",
|
||||
"gift_id": "rocket",
|
||||
"gift_count": "3",
|
||||
"command_id": "cmd_send_gift_xxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
处理:
|
||||
|
||||
- 更新当前等级进度为 `current_progress`。
|
||||
- `effective_added_energy` 是本次实际计入宝箱的能量。
|
||||
- `overflow_energy` 是作废能量,不能加到下一等级。
|
||||
- 如果 `status=countdown`,立即切换倒计时态,等待 `room_treasure_countdown_started` 补齐 `open_at_ms`;如该消息丢失,调用 GET 兜底。
|
||||
|
||||
### room_treasure_countdown_started
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "evt_countdown",
|
||||
"room_id": "lalu_xxx",
|
||||
"event_type": "room_treasure_countdown_started",
|
||||
"actor_user_id": 12345678,
|
||||
"target_user_id": 12345678,
|
||||
"room_version": 6,
|
||||
"attributes": {
|
||||
"box_id": "room_treasure_box_xxx",
|
||||
"level": "2",
|
||||
"current_progress": "200",
|
||||
"energy_threshold": "200",
|
||||
"countdown_start_ms": "1779259502104",
|
||||
"open_at_ms": "1779259512104",
|
||||
"broadcast_scope": "region",
|
||||
"visible_region_id": "1001",
|
||||
"top1_user_id": "12345678",
|
||||
"igniter_user_id": "12345678",
|
||||
"command_id": "cmd_send_gift_xxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
处理:
|
||||
|
||||
- 按 `open_at_ms` 展示开箱倒计时。
|
||||
- `top1_user_id` 和 `igniter_user_id` 在此刻锁定;后续离房或下线也不影响这两类奖励发放。
|
||||
- 倒计时期间再收到普通 `room_gift_sent`,只更新礼物表现和热度,不改变宝箱倒计时。
|
||||
|
||||
注意:房间群 IM 字段名是 `countdown_start_ms`,HTTP state 字段名是 `countdown_started_at_ms`。
|
||||
|
||||
### room_treasure_opened
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "evt_opened",
|
||||
"room_id": "lalu_xxx",
|
||||
"event_type": "room_treasure_opened",
|
||||
"actor_user_id": 12345678,
|
||||
"target_user_id": 12345678,
|
||||
"room_version": 8,
|
||||
"attributes": {
|
||||
"box_id": "room_treasure_box_xxx",
|
||||
"level": "2",
|
||||
"next_level": "3",
|
||||
"opened_at_ms": "1779259512668",
|
||||
"reset_at_ms": "1779321600000",
|
||||
"top1_user_id": "12345678",
|
||||
"igniter_user_id": "12345678",
|
||||
"rewards_json": "[{\"reward_role\":\"in_room\",\"user_id\":12345678,\"reward_item_id\":\"lv2_in_room_1\",\"resource_group_id\":1001,\"display_name\":\"Gold Pack\",\"icon_url\":\"https://cdn.example/reward/gold.png\",\"grant_id\":\"rgr_xxx\",\"status\":\"succeeded\"}]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
处理:
|
||||
|
||||
- 播放当前等级 `opening_animation_url`,再展示 `opened_image_url` 或奖励弹窗。
|
||||
- 解析 `rewards_json` 得到奖励列表;如果解析失败,调用 GET 获取 `state.last_rewards`。
|
||||
- 本地状态进入 `next_level`,进度归 0,状态归 `idle`。
|
||||
|
||||
### room_treasure_reward_granted
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "evt_reward",
|
||||
"room_id": "lalu_xxx",
|
||||
"event_type": "room_treasure_reward_granted",
|
||||
"room_version": 8,
|
||||
"attributes": {
|
||||
"box_id": "room_treasure_box_xxx",
|
||||
"level": "2",
|
||||
"rewards_json": "[{\"reward_role\":\"top1\",\"user_id\":12345678,\"reward_item_id\":\"lv2_top1_1\",\"resource_group_id\":1002,\"display_name\":\"Top Reward\",\"icon_url\":\"https://cdn.example/reward/top.png\",\"grant_id\":\"rgr_xxx\",\"status\":\"succeeded\"}]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
处理:
|
||||
|
||||
- 解析 `rewards_json`。
|
||||
- 当前登录用户在奖励列表中时,展示“获得奖励”提示。
|
||||
- 同一用户可能同时命中 `in_room`、`top1`、`igniter`,是否叠加由 `reward_stack_policy` 决定;客户端按服务端发下来的列表展示。
|
||||
|
||||
当前没有单独的 C2C 宝箱中奖私信,也没有 App 消息 Tab 宝箱入箱消息。后续如新增私有通知,应以新的 C2C `Ext` 文档为准,不复用房间群消息假装私信。
|
||||
|
||||
## 区域/全局播报 IM
|
||||
|
||||
满能量进入倒计时后,如果后台配置允许播报,`activity-service` 会向区域或全局播报群发送 `TIMCustomElem`。
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| `MsgType` | `TIMCustomElem` |
|
||||
| `MsgContent.Ext` | `im_broadcast` |
|
||||
| `MsgContent.Desc` | `room_treasure` |
|
||||
| `MsgContent.Data` | JSON 字符串 |
|
||||
| `CloudCustomData` | 与 `MsgContent.Data` 相同的 JSON 字符串 |
|
||||
|
||||
`Data` 示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "room_treasure_broadcast:lalu_xxx:room_treasure_box_xxx",
|
||||
"broadcast_type": "room_treasure",
|
||||
"scope": "region",
|
||||
"app_code": "lalu",
|
||||
"region_id": 1001,
|
||||
"room_id": "lalu_xxx",
|
||||
"box_id": "room_treasure_box_xxx",
|
||||
"level": 2,
|
||||
"open_at_ms": 1779259512104,
|
||||
"top1_user_id": 12345678,
|
||||
"igniter_user_id": 12345678,
|
||||
"sent_at_ms": 1779259502200,
|
||||
"room_version": 6,
|
||||
"source_event_id": "evt_countdown",
|
||||
"current_progress": 200,
|
||||
"energy_threshold": 200,
|
||||
"countdown_started_at_ms": 1779259502104,
|
||||
"action": {
|
||||
"type": "enter_room",
|
||||
"room_id": "lalu_xxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
处理:
|
||||
|
||||
- `broadcast_type=room_treasure` 时展示宝箱倒计时播报。
|
||||
- 点击播报按 `action.type=enter_room` 跳转或进房。
|
||||
- `scope=region` 表示区域播报群;`scope=global` 表示全局播报群。
|
||||
- 播报只负责引导和展示,不代替房间内宝箱状态;进房后仍调用 GET 初始化。
|
||||
|
||||
## 普通送礼房间 IM
|
||||
|
||||
送礼成功还会产生普通房间群消息:
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| `MsgContent.Ext` | `room_system_message` |
|
||||
| `MsgContent.Desc` | `room_gift_sent` |
|
||||
| `Data.event_type` | `room_gift_sent` |
|
||||
|
||||
宝箱 UI 不应该只靠 `room_gift_sent` 推算能量;能量以 `room_treasure_progress_changed`、`room_treasure_countdown_started`、`POST /gift/send.data.treasure` 或 GET 结果为准。
|
||||
|
||||
## Flutter 解析建议
|
||||
|
||||
IM `attributes` 的值都是字符串,建议统一封装解析:
|
||||
|
||||
```dart
|
||||
int readInt(dynamic value, [int fallback = 0]) {
|
||||
if (value == null) return fallback;
|
||||
if (value is num) return value.toInt();
|
||||
if (value is String) return int.tryParse(value) ?? fallback;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
String readString(dynamic value) {
|
||||
if (value == null) return '';
|
||||
if (value is String) return value;
|
||||
if (value is num) return value.toInt().toString();
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> readRewardsJson(dynamic value) {
|
||||
final raw = readString(value);
|
||||
if (raw.isEmpty) return const [];
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! List) return const [];
|
||||
return decoded.whereType<Map>().map((item) {
|
||||
return item.map((key, val) => MapEntry(key.toString(), val));
|
||||
}).toList();
|
||||
}
|
||||
```
|
||||
|
||||
推荐本地状态更新策略:
|
||||
|
||||
| 输入 | 处理 |
|
||||
| --- | --- |
|
||||
| `GET /treasure` | 覆盖本地完整宝箱状态和 7 级物料。 |
|
||||
| `POST /gift/send.data.treasure` | 送礼人立即刷新当前状态。 |
|
||||
| `room_treasure_progress_changed` | 同房间、版本不旧时更新进度。 |
|
||||
| `room_treasure_countdown_started` | 切换倒计时,并锁定 top1/点火人展示。 |
|
||||
| `room_treasure_opened` | 播放开箱动效,状态切到下一等级 idle。 |
|
||||
| `room_treasure_reward_granted` | 展示当前用户中奖提示;按 `event_id` 去重。 |
|
||||
| `im_broadcast + room_treasure` | 展示区域/全局播报入口,不直接改当前房间状态。 |
|
||||
|
||||
如果客户端发现本地状态和服务端事件无法合并,例如当前 `box_id` 不一致、`level` 倒退、`room_version` 缺失,直接调用 `GET /api/v1/rooms/{room_id}/treasure` 重新同步。
|
||||
277
docs/flutter对接/语音房火箭Flutter对接.md
Normal file
277
docs/flutter对接/语音房火箭Flutter对接.md
Normal file
@ -0,0 +1,277 @@
|
||||
# 语音房火箭接口对接
|
||||
|
||||
## 规则
|
||||
|
||||
- 火箭最高 5 级。
|
||||
- 房间送礼成功后,按礼物 `heat_value` 增加当前等级火箭燃料。
|
||||
- 当前等级满后立刻点火,并把该等级火箭放入 `pending_launches` 等待发射。
|
||||
- 点火后下一等级马上开始累计燃料。
|
||||
- 单次送礼超过当前等级阈值时,超过部分直接作废,不顺延到下一等级。
|
||||
- 发射延迟由后台 `launch_delay_ms` 配置。
|
||||
- 发射结算三类奖励:点火人、贡献第一、在房用户。在房用户按发射瞬间房间 presence 抽奖。
|
||||
|
||||
## 获取火箭
|
||||
|
||||
地址:`GET /api/v1/rooms/{room_id}/rocket`
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 位置 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `room_id` | path | 是 | 房间 ID |
|
||||
|
||||
返回值:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `enabled` | 火箭是否开启 |
|
||||
| `levels` | 1 到 5 级配置 |
|
||||
| `levels[].fuel_threshold` | 等级燃料阈值 |
|
||||
| `levels[].cover_url` | 火箭小封面图 |
|
||||
| `levels[].animation_url` | 常态动效 |
|
||||
| `levels[].launch_animation_url` | 发射动效 |
|
||||
| `levels[].launched_image_url` | 发射后图 |
|
||||
| `levels[].in_room_rewards` | 在房奖励候选 |
|
||||
| `levels[].top1_rewards` | 贡献第一奖励候选 |
|
||||
| `levels[].igniter_rewards` | 点火人奖励候选 |
|
||||
| `state.current_level` | 当前正在累计的等级 |
|
||||
| `state.current_fuel` | 当前等级已累计燃料 |
|
||||
| `state.fuel_threshold` | 当前等级阈值 |
|
||||
| `state.status` | `charging` 或 `completed` |
|
||||
| `state.rocket_id` | 当前正在累计火箭 ID |
|
||||
| `state.pending_launches` | 已点火待发射火箭列表 |
|
||||
| `state.pending_launches[].rocket_id` | 待发射火箭 ID |
|
||||
| `state.pending_launches[].level` | 待发射等级 |
|
||||
| `state.pending_launches[].ignited_at_ms` | 点火 UTC 毫秒 |
|
||||
| `state.pending_launches[].launch_at_ms` | 预计发射 UTC 毫秒 |
|
||||
| `state.pending_launches[].top1_user_id` | 点火时锁定贡献第一 |
|
||||
| `state.pending_launches[].igniter_user_id` | 点火人 |
|
||||
| `state.pending_launches[].cover_url` | 小封面图 |
|
||||
| `state.last_rewards` | 最近一次发射奖励结果 |
|
||||
| `server_time_ms` | 服务端 UTC 毫秒 |
|
||||
|
||||
## 送礼
|
||||
|
||||
地址:`POST /api/v1/rooms/gift/send`
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `room_id` | 是 | 房间 ID |
|
||||
| `command_id` | 是 | 客户端本次送礼幂等 ID |
|
||||
| `target_type` | 否 | 默认 `user` |
|
||||
| `target_user_id` | 否 | 单收礼用户 ID |
|
||||
| `target_user_ids` | 否 | 多收礼用户 ID |
|
||||
| `gift_id` | 是 | 礼物 ID |
|
||||
| `gift_count` | 是 | 礼物数量 |
|
||||
| `pool_id` | 否 | 幸运礼物奖池 ID |
|
||||
|
||||
返回值:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `result` | 命令提交结果 |
|
||||
| `billing_receipt_id` | 钱包扣费回执 |
|
||||
| `room_heat` | 最新房间热度 |
|
||||
| `gift_rank` | 最新房间礼物榜 |
|
||||
| `rocket` | 送礼后的火箭状态,字段同 `state` |
|
||||
| `lucky_gift` | 本次送礼聚合幸运礼物结果 |
|
||||
| `lucky_gifts` | 多目标送礼时每个目标的幸运礼物结果 |
|
||||
|
||||
## 后台配置
|
||||
|
||||
地址:`GET /v1/admin/activity/room-rocket/config`
|
||||
|
||||
参数:无
|
||||
|
||||
返回值:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `enabled` | 是否开启 |
|
||||
| `fuelSource` | 固定为 `heat_value` |
|
||||
| `launchDelayMs` | 点火后延迟发射毫秒 |
|
||||
| `broadcastEnabled` | 是否发区域或全局飘屏 |
|
||||
| `broadcastScope` | `region`、`global`、`none` |
|
||||
| `rewardStackPolicy` | `allow_stack` 或 `priority_only` |
|
||||
| `levels` | 5 级配置 |
|
||||
| `giftFuelRules` | 礼物燃料规则 |
|
||||
|
||||
地址:`PUT /v1/admin/activity/room-rocket/config`
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `enabled` | 是 | 是否开启 |
|
||||
| `fuelSource` | 是 | 固定传 `heat_value` |
|
||||
| `launchDelayMs` | 是 | 点火后延迟发射毫秒 |
|
||||
| `broadcastEnabled` | 是 | 是否发飘屏 |
|
||||
| `broadcastScope` | 是 | `region`、`global`、`none` |
|
||||
| `broadcastDelayMs` | 是 | 保留字段,当前点火后立即入 outbox |
|
||||
| `rewardStackPolicy` | 是 | `allow_stack` 或 `priority_only` |
|
||||
| `levels` | 是 | 必须 5 级 |
|
||||
| `levels[].fuelThreshold` | 是 | 等级阈值 |
|
||||
| `levels[].coverUrl` | 否 | 小封面图 |
|
||||
| `levels[].animationUrl` | 否 | 常态动效 |
|
||||
| `levels[].launchAnimationUrl` | 否 | 发射动效 |
|
||||
| `levels[].launchedImageUrl` | 否 | 发射后图 |
|
||||
| `levels[].inRoomRewards` | 否 | 在房奖励池 |
|
||||
| `levels[].top1Rewards` | 否 | 贡献第一奖励池 |
|
||||
| `levels[].igniterRewards` | 否 | 点火人奖励池 |
|
||||
| `giftFuelRules[].giftId` | 否 | 指定礼物 ID |
|
||||
| `giftFuelRules[].giftTypeCode` | 否 | 指定礼物类型 |
|
||||
| `giftFuelRules[].multiplierPpm` | 否 | 燃料倍率,1 倍为 `1000000` |
|
||||
| `giftFuelRules[].fixedFuel` | 否 | 固定燃料 |
|
||||
| `giftFuelRules[].excluded` | 否 | 是否排除该礼物 |
|
||||
|
||||
返回值:同获取后台配置。
|
||||
|
||||
## 房间 IM
|
||||
|
||||
来源:腾讯云 IM 房间群。
|
||||
|
||||
基础字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `TIMCustomElem.Ext` | `room_system_message` |
|
||||
| `event_type` | 事件类型 |
|
||||
| `room_id` | 房间 ID |
|
||||
| `event_id` | 消息 ID |
|
||||
| `occurred_at_ms` | 服务端 UTC 毫秒 |
|
||||
|
||||
### room_rocket_fuel_changed
|
||||
|
||||
用途:刷新当前等级进度。
|
||||
|
||||
字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `rocket_id` | 当前累计火箭 ID |
|
||||
| `level` | 当前累计等级 |
|
||||
| `added_fuel` | 本次理论燃料 |
|
||||
| `effective_added_fuel` | 本次实际计入燃料 |
|
||||
| `overflow_fuel` | 本次作废燃料 |
|
||||
| `current_fuel` | 当前等级燃料 |
|
||||
| `fuel_threshold` | 当前等级阈值 |
|
||||
| `status` | `charging` 或本次满阈值时的 `ignited` |
|
||||
| `gift_id` | 礼物 ID |
|
||||
| `gift_count` | 礼物数量 |
|
||||
|
||||
### room_rocket_ignited
|
||||
|
||||
用途:当前等级满阈值,加入待发射队列;客户端立即把下一等级作为当前进度。
|
||||
|
||||
字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `rocket_id` | 待发射火箭 ID |
|
||||
| `level` | 待发射等级 |
|
||||
| `rocket_level` | 同 `level` |
|
||||
| `room_short_id` | 房间短号 |
|
||||
| `rocket_cover_url` | 小封面图 |
|
||||
| `current_fuel` | 满阈值燃料 |
|
||||
| `fuel_threshold` | 满阈值 |
|
||||
| `ignited_at_ms` | 点火时间 |
|
||||
| `launch_at_ms` | 预计发射时间 |
|
||||
| `reset_at_ms` | UTC 日重置时间 |
|
||||
| `top1_user_id` | 点火时贡献第一 |
|
||||
| `igniter_user_id` | 点火人 |
|
||||
|
||||
### room_rocket_launched
|
||||
|
||||
用途:火箭已到点发射,客户端播放发射表现。
|
||||
|
||||
字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `rocket_id` | 已发射火箭 ID |
|
||||
| `level` | 已发射等级 |
|
||||
| `next_level` | 当前正在累计等级 |
|
||||
| `launched_at_ms` | 发射时间 |
|
||||
| `top1_user_id` | 贡献第一 |
|
||||
| `igniter_user_id` | 点火人 |
|
||||
| `rewards_json` | 奖励摘要 |
|
||||
|
||||
## 区域或全局飘屏 IM
|
||||
|
||||
来源:腾讯云 IM 区域群或全局群。
|
||||
|
||||
基础字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `TIMCustomElem.Ext` | `im_broadcast` |
|
||||
| `TIMCustomElem.Desc` | `room_rocket` |
|
||||
|
||||
消息体:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `broadcast_type` | `room_rocket` |
|
||||
| `scope` | `region` 或 `global` |
|
||||
| `room_id` | 房间 ID |
|
||||
| `room_short_id` | 房间短号 |
|
||||
| `rocket_id` | 待发射火箭 ID |
|
||||
| `rocket_level` | 火箭等级 |
|
||||
| `rocket_cover_url` | 小封面图 |
|
||||
| `launch_at_ms` | 预计发射时间 |
|
||||
| `igniter.user_id` | 点火人用户 ID |
|
||||
| `igniter.short_id` | 点火人短 ID |
|
||||
| `igniter.nickname` | 点火人昵称 |
|
||||
| `igniter.avatar` | 点火人头像 |
|
||||
|
||||
## 奖励房间 IM
|
||||
|
||||
来源:腾讯云 IM 房间群。
|
||||
|
||||
基础字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `TIMCustomElem.Ext` | `im_broadcast` |
|
||||
| `TIMCustomElem.Desc` | `room_rocket_reward_granted` |
|
||||
|
||||
消息体:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `broadcast_type` | `room_rocket_reward_granted` |
|
||||
| `scope` | `room` |
|
||||
| `room_id` | 房间 ID |
|
||||
| `rocket_id` | 已发射火箭 ID |
|
||||
| `rocket_level` | 已发射等级 |
|
||||
| `rewards` | 本次所有获得奖励的人 |
|
||||
| `rewards[].reward_role` | `in_room`、`top1`、`igniter` |
|
||||
| `rewards[].user.user_id` | 用户 ID |
|
||||
| `rewards[].user.short_id` | 用户短 ID |
|
||||
| `rewards[].user.nickname` | 用户昵称 |
|
||||
| `rewards[].user.avatar` | 用户头像 |
|
||||
| `rewards[].resource_group_id` | 资源组 ID |
|
||||
| `rewards[].display_name` | 奖励展示名 |
|
||||
| `rewards[].icon_url` | 奖励图标 |
|
||||
| `rewards[].grant_id` | 发奖回执 |
|
||||
| `rewards[].status` | 发奖状态 |
|
||||
|
||||
## 客户端处理
|
||||
|
||||
- 进房先调 `GET /api/v1/rooms/{room_id}/rocket`。
|
||||
- 送礼成功优先用返回的 `rocket` 刷新状态。
|
||||
- 收到 `room_rocket_fuel_changed` 刷新进度。
|
||||
- 收到 `room_rocket_ignited` 把该火箭加入待发射队列,并显示下一等级当前进度。
|
||||
- 收到 `room_rocket_launched` 播放发射表现。
|
||||
- 收到 `room_rocket_reward_granted` 展示本次所有中奖人和奖励。
|
||||
- 如果 IM 丢失或乱序,用 GET 接口兜底。
|
||||
- 不要用普通 `room_gift_sent` 自行推算火箭燃料。
|
||||
|
||||
## 高频和延迟处理
|
||||
|
||||
- 送礼只同步写 Room Cell 状态和 room outbox,不同步请求腾讯 IM。
|
||||
- 房间进度 IM 走 room outbox 补偿投递,失败会重试。
|
||||
- 点火后延迟发射通过 RocketMQ delayed wakeup 唤醒;到点后 room-service 重新校验 `rocket_id`、`level`、`launch_at_ms`、UTC 重置边界。
|
||||
- 奖励 IM 一次性包含所有中奖人,避免按用户逐条发 IM。
|
||||
@ -267,7 +267,7 @@ rocketmq:
|
||||
1. 钱包余额通知:已按 `walletnotice` 模块落地。
|
||||
2. 房间踢人私有通知:已按 `roomnotice` 模块落地,只消费 `RoomUserKicked` 的 C2C 通知。
|
||||
3. 幸运礼物中奖:仍由 `wallet-service` 写余额 outbox;notice 不需要新增特殊账务逻辑,只要客户端按 `balance_version` 更新。
|
||||
4. 语音房宝箱私有通知:如产品需要私聊提醒,可在 `roomnotice` 中消费 `RoomTreasureRewardGranted` 或由活动/系统消息模块转发;奖励入账事实仍由 room-service 调 wallet 完成,notice 只投通知。
|
||||
4. 语音房火箭私有通知:如产品需要私聊提醒,可在 `roomnotice` 中消费 `RoomRocketRewardGranted` 或由活动/系统消息模块转发;奖励入账事实仍由 room-service 调 wallet 完成,notice 只投通知。
|
||||
5. 活动/系统私有通知:新增 `activitynotice` 模块,读取 activity outbox,复用 `notice_delivery_events` 或新增同语义位点表。
|
||||
6. 站内 inbox:新增 inbox 模块,写 notice-service 自己的 inbox 表,再按客户端分页接口读取。
|
||||
7. Push:新增 push 模块,和 IM 投递使用不同 `channel`,独立重试和死信。
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
本文只定义后台/Admin 侧实现:政策配置、工资周期、工资单、审核、钱包入账、调整单、后台审计和运营干预。App 侧的主播申请、Agency 成员、BD 邀请和用户可见查询见 [Host Agency BD App Architecture](./主播公会BD架构.md)。
|
||||
|
||||
这里的工资统一指发放到主播侧 `USD_BALANCE` 的业务奖励。提现审核和人工打款仍由 `wallet-service` 拥有,Admin 只负责把审核通过的工资单入账到工资余额,不直接处理线下打款。
|
||||
这里的工资统一指发放到主播侧 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 的业务奖励。提现审核和人工打款仍由 `wallet-service` 拥有,Admin 只负责把审核通过的工资单入账到工资余额,不直接处理线下打款。
|
||||
|
||||
本文不新增独立 `host-service` 微服务。host、Agency、BD、政策和工资后台能力属于 user-services,代码落在当前仓库的 `services/user-service` host domain。
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
- 后台可以配置 host、agency、bd 三类工资政策,并按区域生效。
|
||||
- 后台可以选择日结、周结、半月结、月结,不把结算周期写死在代码里。
|
||||
- 后台可以生成工资周期,查看每个 host/Agency/BD/BD Leader 的明细来源。
|
||||
- 后台审批后,`user-service` host domain 以幂等方式调用 `wallet-service` 给 `USD_BALANCE` 入账。
|
||||
- 后台审批后,`user-service` host domain 以幂等方式调用 `wallet-service` 给 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 入账。
|
||||
- 后台可以对迟到事件、人工修正、争议处理创建调整单,不能直接修改已入账工资项。
|
||||
- 后台可以创建或停用 BD Leader、BD、Agency,也可以关闭 Agency;后台可以查看和新增币商 `coin_seller`;所有动作必须有审计记录。
|
||||
|
||||
@ -49,7 +49,7 @@ graph LR
|
||||
| `hyapp-admin-server` | Admin HTTP 入口、后台鉴权、request envelope、后台操作审计;使用独立 `hyapp_admin` 库 |
|
||||
| `user-service` | 用户、区域、账号状态查询;host/Agency/BD/coin_seller 政策、周期、工资单、关系管理、统计聚合、幂等结果、领域 outbox;App/客户端不能提交区域,后台关系命令的 `region_id` 只能来自 `hyapp-admin-server` 鉴权审计入口 |
|
||||
| `room-service` | 只产出上麦和礼物事件,不接收后台工资命令 |
|
||||
| `wallet-service` | `COIN`、`COIN_SELLER_COIN`、`USD_BALANCE` 入账、提现冻结、提现审核状态和钱包流水 |
|
||||
| `wallet-service` | `COIN`、`COIN_SELLER_COIN`、`HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 入账、提现冻结、提现审核状态和钱包流水 |
|
||||
|
||||
## Admin Permission Model
|
||||
|
||||
@ -188,7 +188,7 @@ flowchart LR
|
||||
E --> F
|
||||
F --> G["approve"]
|
||||
G --> H["wallet CreditSalaryBalance"]
|
||||
H --> I["USD_BALANCE"]
|
||||
H --> I["identity salary wallet"]
|
||||
```
|
||||
|
||||
```sql
|
||||
@ -353,7 +353,7 @@ Leader 直接拥有 Agency 下线时,推荐快照同时设置 `bd_user_id = le
|
||||
1. Admin 审批 cycle。
|
||||
2. `user-service` host domain 查询所有 `approved` salary items。
|
||||
3. 对每个 item 调用 `wallet-service CreditSalaryBalance`,幂等键为 `salary:{salary_item_id}`。
|
||||
4. `wallet-service` credit `USD_BALANCE` 并返回 `wallet_transaction_id`。
|
||||
4. `wallet-service` credit `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 并返回 `wallet_transaction_id`。
|
||||
5. `user-service` host domain 保存 `wallet_transaction_id`,把 item 标记为 `posted`。
|
||||
6. 如果中途失败,只重试未 posted item,幂等键保持不变。
|
||||
|
||||
@ -488,7 +488,7 @@ Outbox consumers can update App message inbox, BI, risk systems, and export jobs
|
||||
| Host changes Agency during cycle | Salary uses `host_daily_stats` relation snapshot and can split across agencies |
|
||||
| Late room event after posted cycle | Create adjustment item; do not mutate posted salary item |
|
||||
| Wallet posting partially fails | Retry unposted items with same idempotency key |
|
||||
| Admin accidentally posts twice | Wallet idempotency must prevent duplicate `USD_BALANCE` credit |
|
||||
| Admin accidentally posts twice | Wallet idempotency must prevent duplicate `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` credit |
|
||||
| Agency closed mid-cycle | Work before close remains attributable; future App joins/search stop immediately |
|
||||
| User region changed | Existing cycle uses event snapshots; new policy/cycle matching follows stats region |
|
||||
| Negative adjustment exceeds balance | Move to manual handling; do not force wallet negative unless wallet explicitly supports debt |
|
||||
@ -507,7 +507,7 @@ Outbox consumers can update App message inbox, BI, risk systems, and export jobs
|
||||
| Agency salary generated | Base uses host salary items under that Agency only |
|
||||
| BD Leader owns direct Agency | Source host salary counted once for the leader |
|
||||
| BD under Leader owns Agency | Host salary contributes to BD and leader according to snapshot |
|
||||
| Cycle posted twice | Wallet idempotency prevents duplicate `USD_BALANCE` credit |
|
||||
| Cycle posted twice | Wallet idempotency prevents duplicate `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` credit |
|
||||
| Posting fails after partial success | Retry posts only unposted items |
|
||||
| Posted item needs correction | Adjustment item created; original item remains immutable |
|
||||
| Admin closes Agency | Agency hidden from App search; existing facts remain auditable |
|
||||
@ -515,7 +515,7 @@ Outbox consumers can update App message inbox, BI, risk systems, and export jobs
|
||||
## Critical Rules
|
||||
|
||||
- Admin owns policies and salary cycles; App owns user-initiated relationship flows.
|
||||
- Wallet is the only owner of `USD_BALANCE` and withdrawal state.
|
||||
- Wallet is the only owner of `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` and withdrawal state.
|
||||
- Posted salary items are immutable.
|
||||
- BD/BD Leader base is downstream host salary only.
|
||||
- Leader direct Agency salary source must be de-duplicated.
|
||||
|
||||
@ -142,7 +142,7 @@
|
||||
- Agency salary:基于下属 host salary item 或明确配置的 agency 指标。
|
||||
- BD/BD Leader salary:基于 downstream host salary sum,且按 source host salary item 去重。
|
||||
- 后台审核工资周期。
|
||||
- 调用 `wallet-service` 给 `USD_BALANCE` 入账,幂等键为 `salary:{salary_item_id}`。
|
||||
- 调用 `wallet-service` 给 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 入账,幂等键为 `salary:{salary_item_id}`。
|
||||
- 支持调整单:迟到事件、人工修正、争议处理都走补差或冲正。
|
||||
- Salary posted 和 adjustment posted 事件接入 App message inbox,通知接收人查看收益明细;通知失败不影响钱包入账事实。
|
||||
|
||||
@ -182,7 +182,7 @@
|
||||
| M3 | Phase 3 | 用户申请、审核、踢出、重新申请、邀请链路完整跑通 |
|
||||
| M3.5 | Phase 3.5 | 申请、邀请、后台创建关系结果能进入 App 系统消息 |
|
||||
| M4 | Phase 4 | 有效上麦和礼物统计按关系快照落库 |
|
||||
| M5 | Phase 5 | 后台生成工资单,审批后入账 `USD_BALANCE` |
|
||||
| M5 | Phase 5 | 后台生成工资单,审批后入账 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` |
|
||||
| M6 | Phase 6 | 失败补偿、导出、重建和监控可用 |
|
||||
|
||||
## Stop Conditions
|
||||
@ -191,7 +191,7 @@
|
||||
- Phase 2 没有完成前,不做 App Agency 搜索上线,因为没有可信组织种子数据。
|
||||
- Phase 3 没有完成前,不做工资发放,因为 host/Agency/BD 归属还不稳定。
|
||||
- Phase 4 没有完成前,不做真实工资入账,因为缺少可审计统计输入。
|
||||
- Phase 5 没有完成审批和幂等 posting 前,不允许写 `USD_BALANCE`。
|
||||
- Phase 5 没有完成审批和幂等 posting 前,不允许写 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`。
|
||||
|
||||
## Verification Commands
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
- BD 默认不是主播;BD 可以加入别的 Agency 成为主播,也可以在未成为主播时邀请自己成为自己的 Agency。
|
||||
- `coin_seller` 是独立币商身份,可以和 Agency、BD、BD Leader 身份并存;币商拥有专用金币账户,可以给玩家转普通金币。
|
||||
- App 侧可以展示 host 统计、Agency 成员、申请、邀请和后台已经生成的收益结果。
|
||||
- App 侧不能创建政策、计算工资周期、审核工资单或直接给 `USD_BALANCE` 入账。
|
||||
- App 侧不能创建政策、计算工资周期、审核工资单或直接给 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 入账。
|
||||
|
||||
## Non-Goals
|
||||
|
||||
@ -67,7 +67,7 @@ graph LR
|
||||
| `gateway-service` | HTTP 入口、鉴权、request envelope、调用内部 gRPC |
|
||||
| `user-service` | 用户主数据、国家、区域、账号状态;host 身份、Agency、BD、coin_seller 层级、申请邀请、关系快照、App 查询读模型 |
|
||||
| `room-service` | 房间事件:上麦、确认发流、下麦、送礼、离房 |
|
||||
| `wallet-service` | `COIN`、`COIN_SELLER_COIN`、`USD_BALANCE`、收益余额、提现冻结/审核/出账 |
|
||||
| `wallet-service` | `COIN`、`COIN_SELLER_COIN`、`HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`、收益余额、提现冻结/审核/出账 |
|
||||
|
||||
`user-service` host domain 读取 `room-service` 和 `wallet-service` 事件,但不反向修改房间状态。App 查询只读取当前身份、关系、申请、邀请、统计和后台已生成的收益结果;结算写入链路见 Admin 文档。
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
| `room-service` | stale presence cleanup | `services/room-service/internal/room/service/presence.go` | 留在 `room-service` |
|
||||
| `room-service` | mic publish timeout cleanup | `services/room-service/internal/room/service/mic_publish_timeout.go` | 留在 `room-service` |
|
||||
| `room-service` | room outbox relay to direct publishers / RocketMQ | `services/room-service/internal/room/service/outbox_worker.go` | 留在 `room-service` 或后续拆 `room-worker`,不进通用 cron |
|
||||
| `room-service` | room treasure delayed open wakeup | `services/room-service/internal/room/service/room_treasure.go`、`services/room-service/internal/room/service/room_treasure_mq.go` | 留在 `room-service`;RocketMQ 只唤醒,到点后仍走 Room Cell 命令链路 |
|
||||
| `room-service` | room rocket delayed launch wakeup | `services/room-service/internal/room/service/room_rocket.go`、`services/room-service/internal/room/service/room_rocket_mq.go` | 留在 `room-service`;RocketMQ 只唤醒,到点后仍走 Room Cell 命令链路 |
|
||||
| `user-service` | region rebuild task | `UserCronService.ProcessRegionRebuildBatch` | 已迁移到 cron-service 调度,旧内置 ticker 已删除 |
|
||||
| `user-service` | login IP risk job | `UserCronService.ProcessLoginIPRiskBatch` | 已迁移到 cron-service 调度,旧内置 ticker 已删除 |
|
||||
| `user-service` | invite recharge wallet outbox consumer | `services/user-service/internal/service/invite/service.go` | 先不迁,需补 durable cursor 或事件总线 |
|
||||
@ -95,7 +95,7 @@ graph LR
|
||||
| Room Cell stale presence cleanup | 依赖本节点已装载 Room Cell 和 Redis lease,必须由 `room-service` owner 执行命令链路 |
|
||||
| Mic publish timeout cleanup | 必须对当前 Room Cell 状态做二次校验并执行 `MicDown`,不能让 cron 直接清麦 |
|
||||
| room outbox relay | 这是 room-service 的事件投递补偿,不是通用定时任务;需要和 direct publisher、RocketMQ 发布、outbox 状态语义绑定 |
|
||||
| room treasure open timer | 宝箱状态属于 Room Cell;延迟消息只负责唤醒,开箱必须由 room-service 重新校验 `box_id/open_at_ms/UTC reset` 后执行命令 |
|
||||
| room rocket launch timer | 火箭状态属于 Room Cell;延迟消息只负责唤醒,发射必须由 room-service 重新校验 `rocket_id/launch_at_ms/UTC reset` 后执行命令 |
|
||||
| wallet ledger transaction | 金币、钻石、积分、余额的加减和冻结必须在 `wallet-service` 事务内完成 |
|
||||
| wallet outbox publish | 属于 wallet-service outbox relay 或 MQ 投递,不应该由通用 cron 直接扫表改状态 |
|
||||
| invite recharge outbox consumer | 当前使用 in-memory cursor 扫 wallet outbox,迁移前必须先做 durable offset 或正式事件总线 |
|
||||
@ -269,7 +269,7 @@ cron-service 可以直接开启多个实例;调度层通过 `cron_task_leases`
|
||||
- activity room outbox consumer。
|
||||
- notice room outbox consumer。
|
||||
- room IM bridge MQ consumer。
|
||||
- room treasure delayed open consumer。
|
||||
- room rocket delayed launch consumer。
|
||||
|
||||
这些需要的是事件消费基础设施:RocketMQ、Redis Stream、或 MySQL durable offset。cron-service 不能用“每隔几秒从头扫 outbox”的方式替代事件消费者。即使某些消费者内部有低频扫描补偿,也必须留在业务 owner service 中,因为它们需要理解 outbox 状态、消费幂等和领域状态校验。
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
- `USDT进货`:平台收到币商线下 USDT 后,后台给币商发放 `COIN_SELLER_COIN` 库存;这笔记录计入币商进货/充值统计。
|
||||
- `金币补偿`:平台因为异常、活动、人工修正等原因给币商补库存;没有充值金额,不计入币商进货/充值统计。
|
||||
- 两种类型都不能给币商发放 `USD_BALANCE`,也不能直接影响玩家普通 `COIN`。
|
||||
- 两种类型都不能给币商发放 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`,也不能直接影响玩家普通 `COIN`。
|
||||
|
||||
## 服务边界
|
||||
|
||||
|
||||
@ -134,8 +134,8 @@ Authorization: Bearer <access_token>
|
||||
| --- | --- | --- |
|
||||
| 消息 tab、系统消息、活动消息 | 我的页设计稿不展示消息红点 | 消息 tab 页面接口 |
|
||||
| 邀请码入口和邀请统计 | 截图首屏没有邀请码入口 | 邀请页或活动入口 |
|
||||
| `wallet.recharge_enabled` / `withdraw_enabled` / `diamond_exchange_enabled` | 首屏卡片不需要操作开关 | `GET /api/v1/wallet/me/overview` |
|
||||
| `DIAMOND` / `USD_BALANCE` | 首屏钱包卡片只展示金币 | 钱包二级页余额接口 |
|
||||
| `wallet.recharge_enabled` / `diamond_exchange_enabled` | 首屏卡片不需要操作开关 | `GET /api/v1/wallet/me/overview` |
|
||||
| `DIAMOND` / `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` | 首屏钱包卡片只展示金币 | 钱包二级页余额接口 |
|
||||
| 邀请用户列表、钱包流水、工资明细、团队成员、背包完整列表、任务列表 | 都需要分页或聚合 | 对应二级页接口 |
|
||||
|
||||
## Performance Budget
|
||||
@ -180,7 +180,6 @@ sequenceDiagram
|
||||
| `GET /api/v1/wallet/me/overview` | 钱包二级页余额和操作开关 |
|
||||
| `GET /api/v1/wallet/recharge/products` | 当前区域充值渠道和档位 |
|
||||
| `GET /api/v1/wallet/diamond-exchange/config` | 钻石兑换金币/余额配置 |
|
||||
| `POST /api/v1/wallet/withdrawals/apply` | 创建待审核提现申请并冻结 `USD_BALANCE` |
|
||||
| `GET /api/v1/wallet/transactions` | 钱包流水分页 |
|
||||
| `GET /api/v1/vip/me` | 当前 VIP 状态 |
|
||||
| `GET /api/v1/vip/packages` | 可购买 VIP 包 |
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
| 状态 owner | Room Cell 和 MySQL snapshot/command log 是房间状态权威来源 |
|
||||
| 持久事实 | `room_outbox` 是房间外事件的持久事实源,和房间命令提交链路一起落 MySQL |
|
||||
| 发布模式 | `outbox_worker.publish_mode` 支持 `direct`、`mq`、`dual` |
|
||||
| MQ 职责 | RocketMQ 只做事件分发和宝箱延迟唤醒,不保存房间核心状态 |
|
||||
| MQ 职责 | RocketMQ 只做事件分发和火箭延迟唤醒,不保存房间核心状态 |
|
||||
| 下游语义 | 下游服务按 `event_id` 幂等消费,失败由自己的位点、重试和死信处理 |
|
||||
| 主链路 | HTTP/gRPC 请求不等待 IM REST、activity、notice、push 或 inbox 投递完成 |
|
||||
|
||||
@ -37,7 +37,7 @@ gateway -> room-service command
|
||||
|
||||
outbox worker
|
||||
-> claim pending/retryable rows
|
||||
-> if RoomTreasureCountdownStarted, schedule treasure open delayed wakeup when configured
|
||||
-> if RoomRocketIgnited, schedule rocket launch delayed wakeup when configured
|
||||
-> publish by mode:
|
||||
direct: Tencent IM publisher + activity gRPC publisher
|
||||
mq: RocketMQ room_outbox publisher
|
||||
@ -78,9 +78,9 @@ outbox worker
|
||||
| `hyapp_room_outbox` | `room-service` outbox worker | `hyapp-activity-room-outbox` | activity-service 消费房间事实,做活动、播报、系统消息等后续处理 |
|
||||
| `hyapp_room_outbox` | `room-service` outbox worker | `hyapp-notice-room-outbox` | notice-service 消费私有通知相关事实,例如 `RoomUserKicked` |
|
||||
| `hyapp_room_outbox` | `room-service` outbox worker | `hyapp-room-im-bridge` | room-service IM bridge 消费房间群系统消息和群成员控制事实 |
|
||||
| `hyapp_room_treasure_open` | `room-service` treasure scheduler | `hyapp-room-treasure-open` | 宝箱倒计时到点唤醒 Room Cell 开箱命令 |
|
||||
| `hyapp_room_rocket_launch` | `room-service` rocket scheduler | `hyapp-room-rocket-open` | 火箭倒计时到点唤醒 Room Cell 发射命令 |
|
||||
|
||||
`hyapp_room_treasure_open` 不是业务状态源。消息只携带 `app_code、room_id、box_id、level、open_at_ms、command_id` 等唤醒参数;真正能否开箱仍由 room-service 加载 Room Cell 后校验当前宝箱状态、等级、`box_id`、`open_at_ms` 和 UTC 重置边界。
|
||||
`hyapp_room_rocket_launch` 不是业务状态源。消息只携带 `app_code、room_id、rocket_id、level、launch_at_ms、command_id` 等唤醒参数;真正能否发射仍由 room-service 加载 Room Cell 后校验当前火箭状态、等级、`rocket_id`、`launch_at_ms` 和 UTC 重置边界。
|
||||
|
||||
## 配置
|
||||
|
||||
@ -103,11 +103,11 @@ rocketmq:
|
||||
tencent_im_consumer_enabled: false
|
||||
tencent_im_consumer_group: "hyapp-room-im-bridge"
|
||||
consumer_max_reconsume_times: 16
|
||||
treasure_open:
|
||||
rocket_launch:
|
||||
enabled: false
|
||||
topic: "hyapp_room_treasure_open"
|
||||
producer_group: "hyapp-room-treasure-open-producer"
|
||||
consumer_group: "hyapp-room-treasure-open"
|
||||
topic: "hyapp_room_rocket_launch"
|
||||
producer_group: "hyapp-room-rocket-open-producer"
|
||||
consumer_group: "hyapp-room-rocket-open"
|
||||
consumer_max_reconsume_times: 16
|
||||
|
||||
outbox_worker:
|
||||
@ -129,7 +129,7 @@ outbox_worker:
|
||||
| `rocketmq.enabled` | 控制 RocketMQ client 是否启用;本地默认关闭,线上示例开启 |
|
||||
| `rocketmq.room_outbox.enabled` | 控制 room outbox 是否可发布到 MQ;`publish_mode=mq/dual` 时必须开启 |
|
||||
| `rocketmq.room_outbox.tencent_im_consumer_enabled` | 控制 room-service 是否作为 IM bridge consumer 消费 `hyapp_room_outbox` |
|
||||
| `rocketmq.treasure_open.enabled` | 控制宝箱倒计时是否发布和消费延迟唤醒消息 |
|
||||
| `rocketmq.rocket_launch.enabled` | 控制火箭点火后是否发布和消费延迟发射唤醒消息 |
|
||||
| `outbox_worker.publish_mode` | 本地默认 `direct`,线上建议 `mq`,迁移期可用 `dual` |
|
||||
| `poll_interval` | worker 空轮询间隔,必须大于 0,避免 busy loop |
|
||||
| `batch_size` | 每轮最大抢占条数,必须大于 0,避免全表扫描 |
|
||||
@ -143,7 +143,7 @@ outbox_worker:
|
||||
| RocketMQ room outbox publisher | 把 protobuf outbox 包装成统一 `room_outbox_event`,发布到 `hyapp_room_outbox` |
|
||||
| Tencent IM direct publisher | `RoomCreated` 对已同步创建的房间群执行幂等 EnsureGroup;房间系统事件发送腾讯云 IM 群自定义消息 |
|
||||
| Activity direct publisher | 本地或未接 MQ 时直接调用 activity-service room event 消费入口 |
|
||||
| Treasure open scheduler | 在 `RoomTreasureCountdownStarted` 后发布到点延迟消息,唤醒 room-service 开箱 |
|
||||
| Rocket launch scheduler | 在 `RoomRocketIgnited` 后发布到点延迟消息,唤醒 room-service 发射 |
|
||||
| Activity MQ consumer | 独立 consumer group 消费 `hyapp_room_outbox`,按 `event_id` 幂等推进活动/播报逻辑 |
|
||||
| Notice MQ consumer | 独立 consumer group 消费 `hyapp_room_outbox`,写 `notice_delivery_events` 后投 C2C 私有通知 |
|
||||
| Room IM bridge MQ consumer | 独立 consumer group 消费 `hyapp_room_outbox`,投递房间群系统消息和群成员控制 |
|
||||
@ -193,6 +193,6 @@ RocketMQ consumer 失败时不修改 `room_outbox`。consumer 应依赖 RocketMQ
|
||||
| MQ 不可用,`mq/dual` | 房间写命令仍成功,outbox 进入重试;MQ 恢复后补发 |
|
||||
| activity consumer 不可用,`mq` | `room_outbox` 不回退;RocketMQ 和 activity 消费位点负责积压与重试 |
|
||||
| poison event | 达到 `max_retry_count` 后进入 `failed`,不再每秒刷日志 |
|
||||
| 宝箱倒计时 | `RoomTreasureCountdownStarted` 发布延迟唤醒;到点后 room-service 重新校验状态再开箱 |
|
||||
| 火箭倒计时 | `RoomRocketIgnited` 发布延迟唤醒;到点后 room-service 重新校验状态再发射 |
|
||||
| 正常投递 | 同一 `event_id` 可被多个 consumer group 独立消费且重复投递不产生重复副作用 |
|
||||
| 真实链路 | create/join/mic/sendGift/leave 均有 `room_command_timing` |
|
||||
|
||||
335
docs/房间区域置顶.md
335
docs/房间区域置顶.md
@ -1,46 +1,329 @@
|
||||
# 房间区域置顶
|
||||
# 房间置顶与房间列表排序产品开发文档
|
||||
|
||||
房间置顶是公共房间发现列表的运营排序能力。房间列表按 `visible_region_id` 隔离,所以置顶也按区域生效:后台选择一个房间后,后端使用该房间当前 `visible_region_id` 建立 `room_region_pins` 记录。
|
||||
## 目标
|
||||
|
||||
## 边界
|
||||
后台在「房间管理」下增加二级菜单「房间置顶」,用于运营配置公共房间发现页的置顶顺序。
|
||||
|
||||
- `room-service` 仍然是房间状态 owner;置顶不进入 Room Cell、snapshot 或 command log。
|
||||
- 置顶只影响 `GET /api/v1/rooms?tab=hot/new` 的排序,不影响进房权限。
|
||||
- 有效置顶条件为 `status=active` 且 `expires_at_ms > now_ms`。
|
||||
- 房间改区域后,旧区域置顶不会自动迁移到新区域;后台需要取消旧置顶并在新区域重新置顶。
|
||||
置顶只影响 App 房间发现列表排序,不改变房间状态、不改变房间区域、不影响进房权限。
|
||||
|
||||
## 排序
|
||||
## 后台页面
|
||||
|
||||
同一区域公共列表排序:
|
||||
菜单:房间管理 / 房间置顶
|
||||
|
||||
1. 有效置顶房间排在普通房间之前。
|
||||
2. 置顶房间按 `weight DESC, expires_at_ms DESC, room_id ASC`。
|
||||
3. 非置顶房间保留原排序:`hot` 按 `sort_score DESC`,`new` 按 `created_at_ms DESC`。
|
||||
列表展示:
|
||||
|
||||
列表 cursor 包含置顶分区、权重、过期时间和原列表排序键,跨页时不会重复返回置顶房间。
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| 房间信息 | 房间头像、房间名、房间长 ID、房间短 ID |
|
||||
| 房主信息 | 房主头像、昵称、用户长 ID、用户短 ID |
|
||||
| 区域 | 房间当前区域 |
|
||||
| 置顶时间 | `pinnedAtMs` 到 `expiresAtMs` |
|
||||
| 排序 | `weight`,数值越大越靠前 |
|
||||
| 置顶类型 | `global` 全区置顶;`region` 区域置顶 |
|
||||
| 状态 | `active` 生效中;`expired` 已过期;`cancelled` 已取消 |
|
||||
|
||||
新增置顶流程:
|
||||
|
||||
1. 输入用户短 ID 或房间 ID 搜索房间。
|
||||
2. 选择置顶类型:全区置顶或区域置顶。
|
||||
3. 选择置顶开始时间和结束时间。
|
||||
4. 填写排序值,数值越大越靠前。
|
||||
5. 点击置顶后,后台创建置顶记录。
|
||||
|
||||
区域规则:
|
||||
|
||||
- 全区置顶对所有用户国家/区域生效。
|
||||
- 区域置顶只对房间当前区域生效,不需要后台单独选择区域。
|
||||
- 房间后续改区域时,置顶记录不会自动改区域;后台可以取消旧置顶后重新置顶。
|
||||
|
||||
过期规则:
|
||||
|
||||
- 当前时间不在 `[pinnedAtMs, expiresAtMs)` 内时,App 列表不再把该房间当置顶房间。
|
||||
- 过期后房间自动恢复普通房间排序,不需要修改房间状态。
|
||||
- 后台列表读取时会把已过期的 active 记录展示为 `expired`。
|
||||
|
||||
## App 房间列表排序
|
||||
|
||||
地址:`GET /api/v1/rooms`
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `tab` | `hot` 或 `new` |
|
||||
| `limit` | 每页数量 |
|
||||
| `cursor` | 下一页游标 |
|
||||
| `q` | 搜索关键词,可不传 |
|
||||
|
||||
区域来源:
|
||||
|
||||
- gateway 使用登录用户 ID 调 user-service 获取 `users.region_id`。
|
||||
- 客户端传的区域参数不会生效。
|
||||
- room-service 按该用户区域做排序分组。
|
||||
|
||||
`hot` 排序:
|
||||
|
||||
1. 全区置顶房间,多个房间按 `weight DESC`。
|
||||
2. 当前用户区域的置顶房间,多个房间按 `weight DESC`。
|
||||
3. 当前用户区域有人普通房,按 `hot_score DESC`。
|
||||
4. 其他区域有人普通房,按 `hot_score DESC`。
|
||||
5. 当前用户区域空房,按 `hot_score DESC`。
|
||||
6. 其他区域空房,按 `hot_score DESC`。
|
||||
|
||||
`hot_score` 规则:
|
||||
|
||||
```text
|
||||
heat_score = log2(heat + 1)
|
||||
online_score = log2(min(online_count, 50) + 1)
|
||||
mic_score = min(occupied_seat_count, 10)
|
||||
|
||||
hot_score = heat_score * 650000
|
||||
+ online_score * 300000
|
||||
+ mic_score * 50000
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `heat` 是房间贡献值。
|
||||
- `online_count = 0` 的普通房进入空房分组;只要存在有人普通房,空房不会排在有人普通房前面。
|
||||
- 在线人数最多按 50 人计分,防止单个房间靠人数无限放大。
|
||||
- 上麦人数最多按 10 人计分,只作为活跃气氛补充。
|
||||
|
||||
`new` 排序:
|
||||
|
||||
1. 全区置顶房间,多个房间按 `weight DESC`。
|
||||
2. 当前用户区域的置顶房间,多个房间按 `weight DESC`。
|
||||
3. 当前用户区域的普通房间,按创建时间 `created_at_ms DESC`。
|
||||
4. 其他区域的普通房间,按创建时间 `created_at_ms DESC`。
|
||||
|
||||
同一个排序分组内,如果排序值相同,后端使用过期时间和房间 ID 保证分页稳定。
|
||||
|
||||
返回值:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"request_id": "request-id",
|
||||
"data": {
|
||||
"rooms": [
|
||||
{
|
||||
"room_id": "room-global-pin",
|
||||
"room_short_id": "163212",
|
||||
"owner_user_id": "165059",
|
||||
"title": "room name",
|
||||
"cover_url": "https://example.com/room.png",
|
||||
"visible_region_id": 2,
|
||||
"heat": 1000,
|
||||
"online_count": 0,
|
||||
"locked": false
|
||||
}
|
||||
],
|
||||
"next_cursor": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
相关 IM:
|
||||
|
||||
- 无新增 IM。
|
||||
- 置顶只影响 HTTP 房间列表读模型,不发送房间群消息或用户私信。
|
||||
|
||||
## 房间列表在线人数更新逻辑
|
||||
|
||||
房间列表返回的 `online_count` 来自 room-service 的业务 presence,不直接读取腾讯 IM 在线人数,也不直接读取 RTC 在线人数。
|
||||
|
||||
更新入口:
|
||||
|
||||
| 入口 | 说明 | 列表人数变化 |
|
||||
| --- | --- | --- |
|
||||
| `POST /api/v1/rooms/join` | 用户进房,room-service 把用户写入 Room Cell 的 `OnlineUsers` | 增加或保持不变 |
|
||||
| `POST /api/v1/rooms/heartbeat` | 用户在房间内刷新 `LastSeenAtMS` | 人数不变,只延长在线有效期 |
|
||||
| `POST /api/v1/rooms/leave` | 用户主动离房,room-service 从 `OnlineUsers` 删除用户 | 减少 |
|
||||
| 踢人、关房、系统驱逐 | 房间命令链路清理 presence | 减少或清零 |
|
||||
| stale worker | 用户断线、杀进程、没有主动离房时,由后台扫描清理超时 presence | 延迟减少 |
|
||||
|
||||
投影规则:
|
||||
|
||||
- 每次 Room Cell 命令成功提交后,room-service 用最新快照刷新 `room_list_entries`。
|
||||
- `room_list_entries.online_count = len(snapshot.online_users)`。
|
||||
- `room_list_entries.occupied_seat_count` 来自当前被占用麦位数。
|
||||
- `hot_score` 使用 `online_count` 和 `occupied_seat_count`,所以在线人数变化会影响 hot 普通房排序。
|
||||
- 列表投影是最终一致读模型;投影失败只记录日志,不回滚已经成功的房间命令。
|
||||
|
||||
延迟:
|
||||
|
||||
| 场景 | 预计延迟 |
|
||||
| --- | --- |
|
||||
| 正常进房 | 同一次 `JoinRoom` 命令内刷新,通常是毫秒级到几十毫秒 |
|
||||
| 主动离房 | 同一次 `LeaveRoom` 命令内刷新,通常是毫秒级到几十毫秒 |
|
||||
| 踢人、关房、系统驱逐 | 同一次房间命令内刷新,通常是毫秒级到几十毫秒 |
|
||||
| 心跳 | 只刷新有效期,不改变列表人数 |
|
||||
| App 断线或杀进程且没有主动离房 | 当前配置 `presence_stale_after = 2m`,`presence_stale_scan_interval = 30s`,通常约 2 分钟到 2 分 30 秒后列表人数减少 |
|
||||
|
||||
相关 IM:
|
||||
|
||||
- 无新增 IM。
|
||||
- 在线人数是 room-service 业务 presence,不等于腾讯 IM 长连接在线人数。
|
||||
- 腾讯 IM 退群、RTC 退房不会直接删除 room-service presence;业务离房只能由 `LeaveRoom`、踢人、关房、系统驱逐或 stale worker 完成。
|
||||
|
||||
## 后台接口
|
||||
|
||||
后台接口使用后台登录 token 和 `/api/v1` 前缀。
|
||||
|
||||
| 方法 | 路径 | 权限 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/v1/admin/rooms/pins` | `room-pin:view` | 查询房间置顶列表,默认只返回有效置顶 |
|
||||
| `POST` | `/api/v1/admin/rooms/pins` | `room-pin:create` | 新增或恢复房间置顶 |
|
||||
| `DELETE` | `/api/v1/admin/rooms/pins/{pin_id}` | `room-pin:cancel` | 取消房间置顶 |
|
||||
### 查询置顶列表
|
||||
|
||||
创建请求:
|
||||
地址:`GET /api/v1/admin/rooms/pins`
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `page` | 页码,默认 1 |
|
||||
| `page_size` | 每页数量,默认 20,最大 100 |
|
||||
| `keyword` | 房间 ID、房间短 ID、房间名关键词 |
|
||||
| `status` | `active`、`expired`、`cancelled`、`all`,默认 `active` |
|
||||
| `pinType` | `global` 或 `region`,不传表示全部类型 |
|
||||
| `regionId` | 区域筛选,不传表示全部区域 |
|
||||
|
||||
返回值:
|
||||
|
||||
```json
|
||||
{
|
||||
"roomId": "room_123",
|
||||
"weight": 100,
|
||||
"durationDays": 30
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"pinType": "global",
|
||||
"status": "active",
|
||||
"weight": 100,
|
||||
"pinnedAtMs": 1780665600000,
|
||||
"expiresAtMs": 1783257600000,
|
||||
"remainingMs": 2592000000,
|
||||
"room": {
|
||||
"roomId": "room-global-pin",
|
||||
"roomShortId": "163212",
|
||||
"title": "room name",
|
||||
"visibleRegionId": 2,
|
||||
"regionName": "南亚区"
|
||||
},
|
||||
"user": {
|
||||
"userId": "165059",
|
||||
"displayUserId": "165059",
|
||||
"username": "host name"
|
||||
}
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `roomId` 可以是长房间 ID 或短房间 ID,最终落库使用长房间 ID。
|
||||
- `durationDays` 不传时默认 30 天。
|
||||
- 只能置顶当前 `active` 房间。
|
||||
### 搜索房间
|
||||
|
||||
列表返回的 `room` 包含房间长短 ID、昵称、头像和区域;`user` 包含房主头像、长短 ID 和昵称;`remainingMs` 是当前 UTC 业务时间下的剩余毫秒数。
|
||||
地址:`GET /api/v1/admin/rooms`
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `keyword` | 用户短 ID、房间短 ID、房间长 ID、房间名 |
|
||||
| `status` | 建议新增置顶时传 `active` |
|
||||
| `page` | 页码 |
|
||||
| `page_size` | 每页数量 |
|
||||
|
||||
说明:
|
||||
|
||||
- 输入用户短 ID 时,后台会先查用户,再按房主用户 ID 精确查房间。
|
||||
- 输入房间 ID 时,后台直接按 room-service 房间列表查询。
|
||||
|
||||
### 新增置顶
|
||||
|
||||
地址:`POST /api/v1/admin/rooms/pins`
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `roomId` | 房间长 ID 或房间短 ID |
|
||||
| `pinType` | `global` 全区置顶;`region` 区域置顶;不传默认 `region` |
|
||||
| `weight` | 排序值,不能小于 0 |
|
||||
| `pinnedAtMs` | 置顶开始时间,Unix 毫秒 |
|
||||
| `expiresAtMs` | 置顶结束时间,Unix 毫秒 |
|
||||
| `durationDays` | 旧字段兼容;新后台优先传开始和结束时间 |
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"roomId": "room-global-pin",
|
||||
"pinType": "global",
|
||||
"weight": 100,
|
||||
"pinnedAtMs": 1780665600000,
|
||||
"expiresAtMs": 1783257600000
|
||||
}
|
||||
```
|
||||
|
||||
返回值:返回创建后的置顶记录,结构同列表单条记录。
|
||||
|
||||
### 取消置顶
|
||||
|
||||
地址:`DELETE /api/v1/admin/rooms/pins/{pin_id}`
|
||||
|
||||
参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `pin_id` | 置顶记录 ID |
|
||||
|
||||
返回值:返回取消后的置顶记录,状态为 `cancelled`。
|
||||
|
||||
相关 IM:
|
||||
|
||||
- 无新增 IM。
|
||||
- 新增、取消、过期置顶都只影响发现页排序。
|
||||
|
||||
## 数据规则
|
||||
|
||||
- `room_region_pins.pin_type = global` 表示全区置顶,落库 `visible_region_id = 0`。
|
||||
- `room_region_pins.pin_type = region` 表示区域置顶,落库 `visible_region_id = 房间当前 visible_region_id`。
|
||||
- 有效置顶条件:`status = active` 且 `pinned_at_ms <= now_ms` 且 `expires_at_ms > now_ms`。
|
||||
- 同一个房间可以同时有全区置顶和区域置顶;App 列表优先按全区置顶分组展示。
|
||||
- `hot` 普通房先按本区域/其他区域和有人/空房分组,再按 `hot_score` 排序。
|
||||
- `new` 普通房只按本区域/其他区域分组,再按创建时间排序。
|
||||
- 置顶不进入 Room Cell、snapshot 或 command log;它是发现页运营排序读模型。
|
||||
- 房间核心状态仍由 room-service 管理,后台不直接写房间库。
|
||||
|
||||
## 本地真实数据验证
|
||||
|
||||
验证环境:
|
||||
|
||||
- 本地 Docker MySQL:`hyapp-mysql`,端口 `23306`,状态 healthy。
|
||||
- 用真实 MySQL 临时库执行 room-service 用例,不使用 memory repository。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
ROOM_SERVICE_MYSQL_TEST_DSN='root:root@tcp(127.0.0.1:23306)/?parseTime=true&loc=UTC&multiStatements=true' \
|
||||
go test ./services/room-service/internal/room/service \
|
||||
-run 'TestRoomListSortScore|TestPinnedRoomListOrdersGlobalRegionLocalThenOtherFor(Hot|New)|TestRegionalPinOrdersPublicRoomList' \
|
||||
-count=1 -v
|
||||
```
|
||||
|
||||
验证结果:
|
||||
|
||||
- `TestRegionalPinOrdersPublicRoomList` 通过,验证区域置顶优先于同区域普通 hot 房间。
|
||||
- `TestPinnedRoomListOrdersGlobalRegionLocalThenOtherForHot` 通过,验证 hot 顺序为全区置顶、当前区域置顶、当前区域有人普通房、其他区域有人普通房、当前区域空房、其他区域空房。
|
||||
- `TestPinnedRoomListOrdersGlobalRegionLocalThenOtherForNew` 通过,验证 new 顺序为全区置顶、当前区域置顶、当前区域普通房、其他区域普通房。
|
||||
- `TestRoomListSortScoreUsesCompressedWeightedInputs` 通过,验证 `hot_score` 使用 65%、30%、5% 的压缩权重。
|
||||
- `TestRoomListSortScoreCapsPresenceInputs` 通过,验证在线人数和上麦人数有计分上限。
|
||||
- 测试过程创建了真实房间数据:`room-global-pin`、`room-region-pin`、`room-local-online`、`room-other-online`、`room-local-empty`、`room-other-empty`、`room-global-new`、`room-region-new` 等。
|
||||
|
||||
补充说明:
|
||||
|
||||
- 普通 `hyapp` MySQL 用户没有创建临时库权限,所以本地真实数据验证使用 root DSN 建临时库。
|
||||
- 该验证不连接线上库,不影响线上数据。
|
||||
|
||||
@ -99,7 +99,6 @@
|
||||
| GET | `/api/v1/wallet/me/balances` | wallet | `getMyBalances` | 查询当前用户钱包余额 |
|
||||
| GET | `/api/v1/wallet/recharge/products` | wallet | `listRechargeProducts` | 查询充值商品和渠道 |
|
||||
| GET | `/api/v1/wallet/diamond-exchange/config` | wallet | `getDiamondExchangeConfig` | 查询钻石兑换配置 |
|
||||
| POST | `/api/v1/wallet/withdrawals/apply` | wallet | `applyWithdrawal` | 提交美元余额提现申请 |
|
||||
| GET | `/api/v1/wallet/coin-transactions` | wallet | `listCoinTransactions` | 分页查询当前用户金币流水 |
|
||||
| GET | `/api/v1/wallet/transactions` | wallet | `listWalletTransactions` | 分页查询钱包流水 |
|
||||
| POST | `/api/v1/wallet/coin-seller/transfer` | wallet | `transferCoinFromSeller` | 币商给玩家转金币 |
|
||||
|
||||
@ -136,7 +136,7 @@
|
||||
| 礼物配置 | `PARTIAL` | `wallet_gift_prices` 提供服务端价格和 `COIN`/`DIAMOND` 收费资产,`gift_configs` 提供礼物类型、有效期和特效;SendGift 请求只传 `gift_id/gift_count`;礼物配置已绑定 `resource_type=gift` 资源 | 客户端礼物表现联调、资源版本缓存刷新未做 |
|
||||
| 背包/道具 | `PARTIAL` | `user_resource_entitlements`、`user_resource_equipment` 支持资源发放、我的资源和可佩戴资源装备 | 背包扣减、免费礼物、道具过期扫描未做 |
|
||||
| 礼物消息补偿 | `DONE` | GiftSent/Heat/Rank 事件进入 outbox,由 worker direct 投 IM/activity,或发布到 RocketMQ 后由各 consumer group 消费 | 客户端去重和展示策略未验收 |
|
||||
| 语音房宝箱 | `DONE` | `room_treasure_configs`、`GetRoomTreasure`、`RoomTreasureProgressChanged/CountdownStarted/Opened/RewardGranted`,开箱支持本地扫描和 RocketMQ 延迟唤醒 | App UI、后台配置运营验收和真实 MQ 联调 |
|
||||
| 语音房火箭 | `DONE` | `room_rocket_configs`、`GetRoomRocket`、`RoomRocketFuelChanged/Ignited/Launched/RewardGranted`,发射支持本地扫描和 RocketMQ 延迟唤醒 | App UI、后台配置运营验收和真实 MQ 联调 |
|
||||
|
||||
## Activity And Lucky Gift
|
||||
|
||||
|
||||
@ -130,7 +130,7 @@ sequenceDiagram
|
||||
participant U as user-service
|
||||
participant R as room-service
|
||||
|
||||
C->>G: POST /api/v1/rooms/create(room_name, room_avatar?, room_description?)
|
||||
C->>G: POST /api/v1/rooms/create(room_name, room_avatar, room_description?)
|
||||
G->>U: GetUser(actor_user_id)
|
||||
U-->>G: region_id, display_user_id
|
||||
G->>G: generate room_id = app_code + uuid
|
||||
@ -146,7 +146,7 @@ sequenceDiagram
|
||||
- 客户端不能提交 `room_id`;gateway 按当前 `app_code` 生成 `<app_code>_<uuid>` 作为房间长 ID。
|
||||
- `room_short_id` 首版直接等于创建者当前 `display_user_id`,用于搜索、分享和展示。
|
||||
- 同一个登录用户只能作为 owner 创建一个房间;room-service 按 `RequestMeta.actor_user_id` 判定 owner,不能通过更换 `room_id` 创建第二个房间。
|
||||
- 客户端必须提交 `room_name`;`room_avatar` 为空时由 room-service 写默认系统头像,`room_description` 只进入 `RoomExt`。
|
||||
- 客户端必须提交 `room_name` 和 `room_avatar`;`room_description` 只进入 `RoomExt`。
|
||||
- `visible_region_id` 来自创建者当前 `users.region_id`。
|
||||
- 创建者没有区域时写 `0`,进入 `GLOBAL` 列表桶。
|
||||
- 房间创建成功后,创建者改国家不自动修改房间区域。
|
||||
|
||||
@ -83,7 +83,7 @@ sequenceDiagram
|
||||
U-->>G: access token with user_id
|
||||
G-->>C: token envelope
|
||||
|
||||
C->>G: POST /api/v1/rooms/create(room_name, room_avatar?, room_description?)
|
||||
C->>G: POST /api/v1/rooms/create(room_name, room_avatar, room_description?)
|
||||
G->>R: CreateRoom(actor_user_id, room profile)
|
||||
R->>T: create_group(room_id)
|
||||
T-->>R: group ready
|
||||
|
||||
@ -1,378 +0,0 @@
|
||||
# 语音房宝箱功能架构
|
||||
|
||||
本文定义当前语音房宝箱的产品规则、服务边界、App 接口、后台配置、MQ 唤醒、room outbox 事件和验收边界。宝箱是房间内送礼驱动的互动玩法:用户在语音房送礼积攒能量,当前等级满能量后进入倒计时,倒计时结束时由 `room-service` 按 Room Cell 快照结算并调用 `wallet-service` 发放资源组奖励。所有周期、重置和统计口径统一使用 UTC。
|
||||
|
||||
## 当前实现结论
|
||||
|
||||
| 能力 | 当前事实 |
|
||||
| --- | --- |
|
||||
| App 初始化 | `GET /api/v1/rooms/{room_id}/treasure` 返回 7 级物料、当前等级、当前进度、重置时间、奖励展示配置 |
|
||||
| 送礼累积 | `SendGift` 同步调用 `wallet-service.DebitGift` 成功后,Room Cell 内更新热度、礼物榜和宝箱进度 |
|
||||
| 满能量 | 当前等级进度封顶为阈值,锁定点火人、贡献第一、`box_id`、`open_at_ms` 和 UTC `reset_at_ms` |
|
||||
| 溢出和倒计时送礼 | 当前等级溢出能量作废;倒计时期间送礼不累加到下一级,也不改变点火人或贡献第一 |
|
||||
| 到期开箱 | `room-service` 通过 RocketMQ 延迟消息到 `open_at_ms` 唤醒;本地已加载 Cell 扫描 worker 仍作为兜底 |
|
||||
| 奖励结算 | `room-service` 在开箱命令内按配置加权抽取奖励,并调用 `wallet-service.GrantResourceGroup` 幂等发放 |
|
||||
| 离房/下线 | `top1` 和 `igniter` 在倒计时开始时锁定;开箱前离房或下线仍结算对应角色奖励 |
|
||||
| 在房奖励 | 只发给开箱命令执行时 Room Cell `OnlineUsers` 中的用户 |
|
||||
| 播报 | `activity-service` 消费 `RoomTreasureCountdownStarted`,按 `broadcast_scope` 发区域或全局播报 |
|
||||
| 事件分发 | `room_outbox` 先落 MySQL,再由 outbox worker 发布 RocketMQ;activity、notice、IM bridge 各自 consumer group 消费 |
|
||||
|
||||
## Non-Goals
|
||||
|
||||
| 不做 | 原因 |
|
||||
| --- | --- |
|
||||
| 客户端提交能量、奖励或中奖结果 | 能量和奖励必须来自服务端扣费、配置和确定性抽奖 |
|
||||
| 只用 Redis 保存宝箱进度 | MySQL snapshot、command log 和 `room_outbox` 才是恢复来源 |
|
||||
| activity-service 判断谁在房 | 房间 presence 和开箱快照必须由 Room Cell 产出 |
|
||||
| activity-service 发放宝箱奖励 | 当前发奖在 `room-service` 开箱命令内调用 `wallet-service`,避免跨服务二次改写房间结算事实 |
|
||||
| cron-service 到点开箱 | 到点唤醒属于房间事件消费和 Room Cell 命令触发,不是通用 cron 任务 |
|
||||
| 倒计时期间继续排队能量 | 产品规则明确无效,不累加到下一个宝箱 |
|
||||
|
||||
## 服务边界
|
||||
|
||||
| 模块 | 拥有 | 不拥有 |
|
||||
| --- | --- | --- |
|
||||
| `room-service` | 宝箱进度、等级、状态机、点火人、贡献第一、开箱在线用户快照、抽奖结果、房间宝箱 IM outbox、RocketMQ 延迟开箱唤醒消费 | 用户完整资料、全局/区域播报群成员、钱包余额账本 |
|
||||
| `wallet-service` | 送礼扣费、资源组发放、钱包账务、发放幂等 | 宝箱进度、中奖资格、房间 presence |
|
||||
| `activity-service` | 满能量后区域/全局播报、成长值等房间事件派生消费 | Room Cell 状态、宝箱奖励结算 |
|
||||
| `notice-service` | 通过 room outbox/MQ 消费需要私信的房间事实,写自己的投递位点和死信 | 房间群系统消息、群成员控制、房间状态 |
|
||||
| `gateway-service` | App HTTP envelope、鉴权、协议转换、调用 room gRPC | 能量、抽奖和发奖事实 |
|
||||
| `server/admin` | 后台菜单、配置表单、审计、调用 owner service 管理接口 | 直接写 room/wallet/activity 业务表 |
|
||||
|
||||
宝箱进度和开箱必须经过 Room Cell 命令链路。MQ 只承担“到点唤醒”和“已提交事实分发”,不能成为宝箱状态 owner。
|
||||
|
||||
## 状态机
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> idle
|
||||
idle --> idle: SendGift progress<threshold
|
||||
idle --> countdown: SendGift progress>=threshold
|
||||
countdown --> opened: open_at_ms reached
|
||||
opened --> idle: next level
|
||||
opened --> exhausted: level 7 opened
|
||||
idle --> idle: UTC day reset
|
||||
countdown --> idle: UTC day reset before open
|
||||
exhausted --> idle: next UTC day
|
||||
```
|
||||
|
||||
| 状态 | 语义 |
|
||||
| --- | --- |
|
||||
| `idle` | 当前等级可累积能量 |
|
||||
| `countdown` | 当前等级已满,`open_at_ms` 已确定,本轮进度、点火人、贡献第一和配置版本锁定 |
|
||||
| `opened` | 本轮已开箱,奖励已在命令内完成抽取和资源组发放请求 |
|
||||
| `exhausted` | 当天 7 级都已开启,等下一个 UTC 自然日 |
|
||||
|
||||
当前实现没有单独持久化 `round_id` 表。`box_id` 是单轮宝箱幂等和奖励选择的核心 ID,开箱命令 ID 固定为:
|
||||
|
||||
```text
|
||||
cmd_room_treasure_open_<box_id>
|
||||
```
|
||||
|
||||
奖励发放命令 ID 固定由 `box_id + role + user_id + reward_item_id` 派生,重复开箱或 MQ 重投不会重复发放同一条奖励。
|
||||
|
||||
## 能量规则
|
||||
|
||||
| 规则 | 决策 |
|
||||
| --- | --- |
|
||||
| 能量来源 | 默认使用 `DebitGiftResponse.gift_point_added`;后台可配置 `gift_id` 或 `gift_type_code` 的倍率、覆盖值和排除规则 |
|
||||
| 生效条件 | wallet 扣费成功、Room Cell 提交 `SendGift`、礼物能量值大于 0 |
|
||||
| 幂等 | 同一 `SendGift.command_id` 只能增加一次宝箱能量 |
|
||||
| 点火人 | 第一笔让当前等级 `progress >= threshold` 的送礼用户 |
|
||||
| 贡献第一 | 当前等级倒计时前累计有效能量最高用户 |
|
||||
| 溢出能量 | 单笔礼物超过当前等级剩余阈值时,只计入补满当前等级所需能量,多余能量作废 |
|
||||
| 倒计时送礼 | 当前等级已经满能量,继续送礼不再增加宝箱能量,也不会排队到下一等级 |
|
||||
| 7 级后送礼 | 当天不再累计宝箱能量,礼物仍正常扣费、热度和榜单照常更新 |
|
||||
|
||||
无效能量只进入 `RoomTreasureProgressChanged` 的审计字段,不计入当前等级贡献、不计入下一等级进度、不改变点火人或贡献第一。
|
||||
|
||||
## 开箱与 MQ 唤醒
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as App
|
||||
participant R as room-service
|
||||
participant W as wallet-service
|
||||
participant DB as hyapp_room
|
||||
participant MQ as RocketMQ
|
||||
participant A as activity-service
|
||||
participant IM as Tencent IM
|
||||
|
||||
C->>R: SendGift(command_id)
|
||||
R->>W: DebitGift(command_id)
|
||||
W-->>R: gift_point_added, heat_value
|
||||
R->>R: Room Cell updates treasure
|
||||
R->>DB: command_log + snapshot + room_outbox
|
||||
R-->>C: SendGiftResponse.treasure
|
||||
R->>DB: outbox worker claims RoomTreasureCountdownStarted
|
||||
R->>MQ: delayed RoomTreasureOpenDue(open_at_ms)
|
||||
R->>MQ: room_outbox event
|
||||
MQ-->>A: RoomTreasureCountdownStarted
|
||||
A-->>IM: region/global broadcast
|
||||
MQ-->>R: RoomTreasureOpenDue at open_at_ms
|
||||
R->>R: OpenRoomTreasure command
|
||||
R->>W: GrantResourceGroup per reward
|
||||
R->>DB: command_log + snapshot + opened/reward outbox
|
||||
```
|
||||
|
||||
关键规则:
|
||||
|
||||
- 延迟消息只唤醒,不直接修改状态。
|
||||
- MQ 提前投递时,`room-service` 先检查 `open_at_ms`,未到点返回可重试错误,不写 no-op command log,避免污染开箱 `command_id`。
|
||||
- MQ 重复投递、outbox 重试或扫描 worker 同时触发时,`command_id` 和 wallet grant command 保证幂等。
|
||||
- 如果 `reset_at_ms` 已过,UTC 日界优先,昨天倒计时宝箱不再结算。
|
||||
- 本地 `room_treasure_open_scan_interval` 只扫描本节点已加载且仍持有 lease 的 Cell,是兜底,不解决未加载房间;未加载房间依赖 MQ delayed wakeup。
|
||||
|
||||
## 奖励规则
|
||||
|
||||
| 奖励角色 | 资格 |
|
||||
| --- | --- |
|
||||
| `in_room` | 开箱命令执行时 Room Cell `OnlineUsers` 内的每个用户 |
|
||||
| `top1` | 倒计时开始时锁定的贡献第一用户 |
|
||||
| `igniter` | 倒计时开始时锁定的点火人 |
|
||||
|
||||
奖励配置按等级拆分,每个角色是一个加权奖励池。奖励项引用 `resource_group_id`,由 `wallet-service` 按资源组发放金币、钻石或资源权益。
|
||||
|
||||
```json
|
||||
{
|
||||
"level": 3,
|
||||
"reward_role": "top1",
|
||||
"items": [
|
||||
{
|
||||
"reward_item_id": "lv3_top1_a",
|
||||
"resource_group_id": 12003,
|
||||
"weight": 9000,
|
||||
"display_name": "Top1 Reward",
|
||||
"icon_url": "https://cdn.example/reward.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`reward_stack_policy`:
|
||||
|
||||
| 策略 | 语义 |
|
||||
| --- | --- |
|
||||
| `allow_stack` | 同一用户可同时获得在房、top1、igniter 多个角色奖励 |
|
||||
| `priority_only` | 同一用户只获得优先级最高的一份,优先级为 `igniter -> top1 -> in_room` |
|
||||
|
||||
## App 接口
|
||||
|
||||
### 获取宝箱信息
|
||||
|
||||
```http
|
||||
GET /api/v1/rooms/{room_id}/treasure
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
Response `data` 核心字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"server_time_ms": 1779120000000,
|
||||
"reset_at_ms": 1779148800000,
|
||||
"broadcast_scope": "region",
|
||||
"open_delay_ms": 30000,
|
||||
"broadcast_delay_ms": 0,
|
||||
"reward_stack_policy": "allow_stack",
|
||||
"state": {
|
||||
"current_level": 3,
|
||||
"current_progress": 3600,
|
||||
"energy_threshold": 10000,
|
||||
"status": "idle",
|
||||
"open_at_ms": 0,
|
||||
"top1_user_id": "10001",
|
||||
"igniter_user_id": "0",
|
||||
"box_id": "box_xxx",
|
||||
"last_rewards": []
|
||||
},
|
||||
"levels": [
|
||||
{
|
||||
"level": 1,
|
||||
"energy_threshold": 3000,
|
||||
"cover_url": "https://cdn.example/chest/lv1/cover.png",
|
||||
"animation_url": "https://cdn.example/chest/lv1/idle.svga",
|
||||
"opening_animation_url": "https://cdn.example/chest/lv1/opening.svga",
|
||||
"opened_image_url": "https://cdn.example/chest/lv1/opened.png",
|
||||
"in_room_rewards": [],
|
||||
"top1_rewards": [],
|
||||
"igniter_rewards": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `levels` 固定返回 7 个等级。
|
||||
- `reset_at_ms` 是下一个 UTC 零点,不是用户本地零点。
|
||||
- 客户端只展示服务端返回的当前等级进度;不要自行把溢出能量展示到下一等级。
|
||||
- 当前没有单独的 App 领奖接口,奖励自动发放,最近一次开箱奖励通过 `state.last_rewards` 展示。
|
||||
|
||||
### SendGift 响应
|
||||
|
||||
`SendGiftResponse` 已携带 `treasure` 状态,送礼人成功送礼后可立即更新宝箱 UI,不必等待 IM。
|
||||
|
||||
## 房间 IM 事件
|
||||
|
||||
房间内宝箱 IM 走腾讯云房间群 `TIMCustomElem`,由 room outbox 事件转换。当前事件类型:
|
||||
|
||||
| Event | 客户端 `event_type` | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `RoomTreasureProgressChanged` | `room_treasure_progress_changed` | 宝箱进度增加 |
|
||||
| `RoomTreasureCountdownStarted` | `room_treasure_countdown_started` | 满能量进入倒计时 |
|
||||
| `RoomTreasureOpened` | `room_treasure_opened` | 宝箱打开并给出公开奖励摘要 |
|
||||
| `RoomTreasureRewardGranted` | `room_treasure_reward_granted` | 发奖结果事件,包含奖励列表 |
|
||||
|
||||
### Progress Changed
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "room_treasure_progress_changed",
|
||||
"box_id": "box_xxx",
|
||||
"level": 3,
|
||||
"added_energy": 500,
|
||||
"effective_added_energy": 500,
|
||||
"overflow_energy": 0,
|
||||
"current_progress": 3600,
|
||||
"energy_threshold": 10000,
|
||||
"status": "idle",
|
||||
"reset_at_ms": 1779148800000,
|
||||
"gift_id": "rose",
|
||||
"gift_count": 10
|
||||
}
|
||||
```
|
||||
|
||||
### Countdown Started
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "room_treasure_countdown_started",
|
||||
"box_id": "box_xxx",
|
||||
"level": 3,
|
||||
"current_progress": 10000,
|
||||
"energy_threshold": 10000,
|
||||
"open_at_ms": 1779120030000,
|
||||
"broadcast_scope": "region",
|
||||
"visible_region_id": 1001,
|
||||
"top1_user_id": "10001",
|
||||
"igniter_user_id": "10002"
|
||||
}
|
||||
```
|
||||
|
||||
### Opened / Reward Granted
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "room_treasure_opened",
|
||||
"box_id": "box_xxx",
|
||||
"level": 3,
|
||||
"next_level": 4,
|
||||
"opened_at_ms": 1779120030000,
|
||||
"top1_user_id": "10001",
|
||||
"igniter_user_id": "10002",
|
||||
"rewards_json": "[...]"
|
||||
}
|
||||
```
|
||||
|
||||
群 IM 承载公开摘要。后续如要做个人中奖私信或消息 Tab,应由 `notice-service` 或 inbox consumer 消费 `RoomTreasureRewardGranted`,不要让 `room-service` 同步投递私信。
|
||||
|
||||
## 后台配置
|
||||
|
||||
后台配置带 `config_version`。倒计时开始后,当前宝箱的 `box_id`、等级、进度、`open_at_ms`、点火人和贡献第一不再因配置修改而变化;当前实现开箱时读取最新启用配置的奖励池,因此运营在倒计时窗口内修改奖励配置要按发布窗口处理。
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `enabled` | 总开关 |
|
||||
| `energy_source` | 默认 `gift_point_added` |
|
||||
| `open_delay_ms` | 满能量到开箱的倒计时 |
|
||||
| `broadcast_enabled` | 是否满能量后播报 |
|
||||
| `broadcast_scope` | `none/region/global` |
|
||||
| `broadcast_delay_ms` | 满能量后多久发播报,当前代码写入配置和读模型,实际倒计时事件立即进入 room outbox |
|
||||
| `reward_stack_policy` | `allow_stack` 或 `priority_only` |
|
||||
| `levels` | 7 个等级配置,包含阈值、物料 URL 和三类奖励池 |
|
||||
| `gift_energy_rules` | gift_id 或 gift_type_code 的能量倍率、覆盖值和排除规则 |
|
||||
| `updated_by_admin_id` | 审计字段 |
|
||||
|
||||
奖励池保存时应校验:
|
||||
|
||||
- `resource_group_id` 必须存在且 active。
|
||||
- 同一奖励池权重总和必须大于 0。
|
||||
- 展示字段可以保存快照,但发放仍以 wallet-service 资源组事实为准。
|
||||
- 配置不能直接填写金币数量绕过资源组,除非新增明确的钱包奖励配置类型。
|
||||
|
||||
## room_outbox 与 RocketMQ
|
||||
|
||||
`room_outbox` 是 MySQL 内的可靠事实源,RocketMQ 是发布后的事件总线。不能用 MQ 替代 outbox,因为房间状态提交成功但 MQ 发布失败时必须能从 MySQL 补偿。
|
||||
|
||||
当前 topic:
|
||||
|
||||
| Topic | Producer | Consumer |
|
||||
| --- | --- | --- |
|
||||
| `hyapp_room_outbox` | `room-service` outbox worker | `activity-service`、`notice-service`、`room-service` IM bridge、后续 push/inbox |
|
||||
| `hyapp_room_treasure_open` | `room-service` treasure open scheduler | `room-service` treasure open consumer |
|
||||
|
||||
推荐线上配置:
|
||||
|
||||
```yaml
|
||||
outbox_worker:
|
||||
enabled: true
|
||||
publish_mode: "mq"
|
||||
|
||||
rocketmq:
|
||||
enabled: true
|
||||
room_outbox:
|
||||
enabled: true
|
||||
topic: "hyapp_room_outbox"
|
||||
treasure_open:
|
||||
enabled: true
|
||||
topic: "hyapp_room_treasure_open"
|
||||
```
|
||||
|
||||
`publish_mode`:
|
||||
|
||||
| 模式 | 行为 |
|
||||
| --- | --- |
|
||||
| `direct` | room outbox worker 直接投 activity gRPC 和 Tencent IM,适合本地默认 |
|
||||
| `mq` | room outbox worker 只投 RocketMQ,activity/notice/IM bridge 各自消费,推荐线上 |
|
||||
| `dual` | direct + MQ 双写,只用于迁移验证,所有消费者必须幂等 |
|
||||
|
||||
## Daily Reset
|
||||
|
||||
| 规则 | 决策 |
|
||||
| --- | --- |
|
||||
| 日期来源 | `time.Now().UTC()` |
|
||||
| 重置时间 | 每天 UTC `00:00:00.000` |
|
||||
| 时间区间 | `[day_start_ms, next_day_start_ms)` |
|
||||
| idle | 到达新 UTC 日时回到 level 1,清空进度和贡献 |
|
||||
| countdown | 如果 `open_at_ms` 跨过 `reset_at_ms`,UTC 日界优先,旧宝箱不再结算 |
|
||||
| exhausted | 下一个 UTC 日恢复 level 1 |
|
||||
|
||||
必须覆盖 UTC 边界测试:`day_start_ms`、`next_day_start_ms - 1`、`next_day_start_ms`。不要重新引入 `time.Local`、`task_timezone` 或客户端时区。
|
||||
|
||||
## 验收清单
|
||||
|
||||
| 场景 | 验收 |
|
||||
| --- | --- |
|
||||
| 送礼成功 | wallet 扣费成功,Room Cell 进度增加,`SendGiftResponse.treasure` 返回最新状态 |
|
||||
| 单笔溢出 | 进度封顶当前阈值,溢出能量作废,不进入下一等级 |
|
||||
| 倒计时送礼 | 礼物正常扣费和加热度,但宝箱进度、点火人、贡献第一不变化 |
|
||||
| 满能量 | 写 `RoomTreasureCountdownStarted`,outbox worker 安排 RocketMQ delayed open |
|
||||
| 提前延迟消息 | 不写开箱 no-op command log,MQ 后续可重试 |
|
||||
| 到点开箱 | 即使房间未加载,MQ 唤醒后 room-service 恢复 Room Cell 并开箱 |
|
||||
| top1/igniter 离房 | 仍发角色奖励 |
|
||||
| 在房奖励 | 只发开箱瞬间在线用户 |
|
||||
| UTC 重置 | 到达 UTC 零点后进度回到 level 1,跨日倒计时不结算 |
|
||||
| MQ/outbox 重投 | `event_id`、`box_id`、wallet grant command 保证幂等 |
|
||||
|
||||
## 代码位置
|
||||
|
||||
| 模块 | 路径 |
|
||||
| --- | --- |
|
||||
| 宝箱状态机和结算 | `services/room-service/internal/room/service/room_treasure.go` |
|
||||
| MQ delayed open | `services/room-service/internal/room/service/room_treasure_mq.go` |
|
||||
| outbox worker | `services/room-service/internal/room/service/outbox_worker.go` |
|
||||
| RocketMQ 适配 | `pkg/rocketmqx`、`pkg/roommq`、`services/room-service/internal/integration/rocketmq_outbox.go` |
|
||||
| App HTTP | `services/gateway-service/internal/transport/http/roomapi` |
|
||||
| 后台配置 | `server/admin/internal/modules/roomtreasure` |
|
||||
@ -687,6 +687,46 @@ Rules:
|
||||
- `room_version` 和 `event_time_ms` 用于丢弃旧事件,避免旧 audience/leave 事件清理新会话。
|
||||
- 成功后麦位 `publish_state=publishing`。
|
||||
|
||||
### Mic Heartbeat
|
||||
|
||||
地址:
|
||||
|
||||
```http
|
||||
POST /api/v1/rooms/mic/heartbeat
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
```json
|
||||
{
|
||||
"room_id": "lalu_xxx",
|
||||
"command_id": "cmd_mic_heartbeat_<room_id>_<user_id>_<nonce>",
|
||||
"target_user_id": 0,
|
||||
"mic_session_id": "mic_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
返回值:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {},
|
||||
"seat_no": 1,
|
||||
"mic_heartbeat_at_ms": 1778000005000,
|
||||
"room": {}
|
||||
}
|
||||
```
|
||||
|
||||
相关 IM:
|
||||
|
||||
- 无独立 IM。该接口只刷新当前 `publishing` 麦位会话的服务端心跳时间和房间 presence。
|
||||
- `target_user_id=0` 表示当前登录用户自己。
|
||||
- `mic_session_id` 必须等于当前麦位会话。
|
||||
- `pending_publish` 不能调用该接口开始计时,必须先调用 `POST /api/v1/rooms/mic/publishing/confirm`。
|
||||
- 重试同一次心跳复用同一个 `command_id`;下一次心跳必须换新的 `command_id`。
|
||||
|
||||
### Mic Down
|
||||
|
||||
```http
|
||||
|
||||
125
docs/语音房火箭功能架构.md
Normal file
125
docs/语音房火箭功能架构.md
Normal file
@ -0,0 +1,125 @@
|
||||
# 语音房火箭功能架构
|
||||
|
||||
## 服务边界
|
||||
|
||||
- `room-service` 是火箭进度、点火、待发射队列和奖励结算 owner。
|
||||
- `gateway-service` 只提供 App HTTP 入口和鉴权后的协议转换。
|
||||
- `server/admin` 只读写 `room-service` 内部配置接口,不直连 room 数据库。
|
||||
- `wallet-service` 负责资源组发放。
|
||||
- `activity-service` 负责区域/全局飘屏和带用户资料的奖励房间 IM。
|
||||
- 腾讯云 IM 负责房间群、区域群和全局群投递。
|
||||
|
||||
## 状态模型
|
||||
|
||||
`RoomRocketState` 保存当前正在累计的火箭和已点火待发射队列。
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `current_level` | 当前正在累计的等级,最高 5 |
|
||||
| `current_fuel` | 当前等级燃料 |
|
||||
| `fuel_threshold` | 当前等级阈值 |
|
||||
| `status` | `charging` 或 `completed` |
|
||||
| `rocket_id` | 当前正在累计的火箭 ID |
|
||||
| `pending_launches` | 已点火、等待延迟发射的火箭 |
|
||||
| `last_rewards` | 最近一次发射奖励结果 |
|
||||
|
||||
`pending_launches` 中每一项锁定:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `rocket_id` | 本枚火箭幂等 ID |
|
||||
| `level` | 点火等级 |
|
||||
| `fuel_threshold` | 点火阈值 |
|
||||
| `ignited_at_ms` | 点火时间 |
|
||||
| `launch_at_ms` | 预计发射时间 |
|
||||
| `reset_at_ms` | UTC 日重置时间 |
|
||||
| `top1_user_id` | 点火时贡献第一 |
|
||||
| `igniter_user_id` | 点火人 |
|
||||
| `config_version` | 点火配置版本 |
|
||||
| `cover_url` | 小封面图 |
|
||||
|
||||
## 送礼加燃料
|
||||
|
||||
1. `SendGift` 先调用 wallet 扣费。
|
||||
2. 扣费成功后,Room Cell 串行更新热度、礼物榜和火箭。
|
||||
3. 燃料来自 wallet 返回的 `heat_value`。
|
||||
4. 当前等级未满时,增加 `min(本次燃料, 剩余阈值)`。
|
||||
5. 超过阈值的燃料直接写入 `overflow_fuel`,不进入下一等级。
|
||||
6. 当前等级满后:
|
||||
- 写入 `RoomRocketFuelChanged`。
|
||||
- 写入 `RoomRocketIgnited`。
|
||||
- 将当前火箭追加到 `pending_launches`。
|
||||
- 如果未到 5 级,立刻把当前状态切到下一等级,`current_fuel=0`。
|
||||
- 如果 5 级已满,状态切到 `completed`,UTC 重置前不再累计新火箭。
|
||||
|
||||
## 发射结算
|
||||
|
||||
1. `RoomRocketIgnited` 被 outbox worker 处理后,安排 RocketMQ delayed wakeup。
|
||||
2. 本地扫描 worker 也会扫描已加载且仍持有 lease 的 Room Cell,作为兜底。
|
||||
3. 到点后 `LaunchRoomRocket` 命令重新加载 Room Cell。
|
||||
4. 命令必须校验:
|
||||
- `rocket_id` 存在于 `pending_launches`。
|
||||
- `level` 匹配。
|
||||
- `launch_at_ms <= now_ms`。
|
||||
- `now_ms < reset_at_ms`。
|
||||
5. 结算时按发射瞬间 `OnlineUsers` 抽在房奖励。
|
||||
6. `top1` 和 `igniter` 使用点火时锁定的用户,离房后仍发对应奖励。
|
||||
7. 发奖完成后移除该 `pending_launches` 项,当前正在累计的火箭不被发射命令推进。
|
||||
|
||||
## 奖励
|
||||
|
||||
| 奖励 | 用户来源 |
|
||||
| --- | --- |
|
||||
| 点火人奖励 | `pending_launches[].igniter_user_id` |
|
||||
| 贡献第一奖励 | `pending_launches[].top1_user_id` |
|
||||
| 在房奖励 | 发射命令执行时 Room Cell `OnlineUsers` |
|
||||
|
||||
`reward_stack_policy`:
|
||||
|
||||
| 值 | 说明 |
|
||||
| --- | --- |
|
||||
| `allow_stack` | 同一用户可同时拿在房、top1、点火人奖励 |
|
||||
| `priority_only` | 先点火人,再 top1,再在房;同一用户只拿一份 |
|
||||
|
||||
## IM
|
||||
|
||||
| 事实 | IM | 投递方 |
|
||||
| --- | --- | --- |
|
||||
| `RoomRocketFuelChanged` | `room_rocket_fuel_changed` | room-service IM bridge |
|
||||
| `RoomRocketIgnited` | `room_rocket_ignited` | room-service IM bridge |
|
||||
| `RoomRocketIgnited` | `room_rocket` 区域/全局飘屏 | activity-service |
|
||||
| `RoomRocketLaunched` | `room_rocket_launched` | room-service IM bridge |
|
||||
| `RoomRocketRewardGranted` | `room_rocket_reward_granted` 房间奖励 IM | activity-service |
|
||||
|
||||
奖励 IM 由 activity-service 查询用户公开资料后组装,包含头像、昵称、短 ID 和奖励信息。room-service 不反查 user-service。
|
||||
|
||||
## 高频处理
|
||||
|
||||
- Room Cell 串行处理送礼命令,所以同房高频送礼不会并发写乱进度。
|
||||
- 满阈值时只把火箭追加到 `pending_launches`,不会阻塞下一等级继续累计。
|
||||
- 房间状态 IM 通过 room outbox 补偿投递,不在送礼主链路同步调用腾讯 IM。
|
||||
- 奖励 IM 合并为一条消息,`rewards` 内包含所有中奖人,避免按用户逐条堆积。
|
||||
- RocketMQ delayed wakeup 只负责唤醒,最终发射仍由 Room Cell 命令校验。
|
||||
|
||||
## UTC 重置
|
||||
|
||||
- 火箭周期按 UTC 自然日。
|
||||
- 到达 `reset_at_ms` 后,查询或下一次送礼会投影为 1 级新进度。
|
||||
- 跨过 UTC 日界仍未发射的待发射火箭不结算。
|
||||
|
||||
## 后台配置
|
||||
|
||||
配置表:`room_rocket_configs`
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `enabled` | 是否开启 |
|
||||
| `fuel_source` | 固定 `heat_value` |
|
||||
| `launch_delay_ms` | 点火到发射延迟 |
|
||||
| `broadcast_enabled` | 是否发飘屏 |
|
||||
| `broadcast_scope` | `region`、`global`、`none` |
|
||||
| `reward_stack_policy` | 奖励叠加策略 |
|
||||
| `levels_json` | 5 级阈值、物料和奖励池 |
|
||||
| `gift_fuel_rules_json` | 礼物燃料规则 |
|
||||
|
||||
`levels_json` 字段使用 `fuelThreshold`、`coverUrl`、`animationUrl`、`launchAnimationUrl`、`launchedImageUrl`、`inRoomRewards`、`top1Rewards`、`igniterRewards`。
|
||||
@ -75,12 +75,12 @@ Authorization: Bearer <access_token>
|
||||
"command_id": "cmd_create_room_550e8400",
|
||||
"mode": "voice",
|
||||
"room_name": "周末语音房",
|
||||
"room_avatar": "",
|
||||
"room_avatar": "https://cdn.example/room.png",
|
||||
"room_description": "本房间用于轻量聊天和上麦互动"
|
||||
}
|
||||
```
|
||||
|
||||
正式客户端不应该传 `room_id`、`owner_user_id` 和 `host_user_id`。`command_id` 由客户端按创建房间动作生成,同一动作重试必须复用。gateway 必须从 access token 取当前 `user_id` 写入 `RequestMeta.actor_user_id`,并生成内部 `room_id`;room-service 使用该 actor 确定 owner 和默认 host。`seat_count` 可省略,省略或传 `0` 时使用后台默认座位数 `15`;传正数时必须命中后台启用座位数。`room_name` 必填,`room_avatar` 为空时写入默认系统头像,房间展示资料统一落在 `RoomExt`。
|
||||
正式客户端不应该传 `room_id`、`owner_user_id` 和 `host_user_id`。`command_id` 由客户端按创建房间动作生成,同一动作重试必须复用。gateway 必须从 access token 取当前 `user_id` 写入 `RequestMeta.actor_user_id`,并生成内部 `room_id`;room-service 使用该 actor 确定 owner 和默认 host。`seat_count` 可省略,省略或传 `0` 时使用后台默认座位数 `15`;传正数时必须命中后台启用座位数。`room_name` 和 `room_avatar` 必填,房间展示资料统一落在 `RoomExt`。
|
||||
|
||||
响应必须包含:
|
||||
|
||||
|
||||
@ -21,9 +21,9 @@
|
||||
| `COIN_SELLER_COIN` | 币商专用金币库存,只能转给玩家形成普通 `COIN` | yes, seller transfer only | no | wallet-service |
|
||||
| `DIAMOND` | 钻石,不可直接消费,只能按政策兑换金币或美元余额 | no | no | wallet-service |
|
||||
| `GIFT_POINT` | 主播礼物积分,达到政策后结算美元奖励 | no | no | wallet-service |
|
||||
| `USD_BALANCE` | 主播可提现美元余额,建议单位为 cent 或 micro cent | no | yes | wallet-service |
|
||||
| `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` | 主播可提现美元余额,建议单位为 cent 或 micro cent | no | yes | wallet-service |
|
||||
|
||||
钻石和积分都不是直接消费资产。钻石兑换金币或美元余额时必须落兑换订单,记录政策版本、汇率和结果。主播积分达到政策后,由结算任务发起美元余额奖励入账,用户提现只能提现 `USD_BALANCE`。
|
||||
钻石和积分都不是直接消费资产。钻石兑换金币或美元余额时必须落兑换订单,记录政策版本、汇率和结果。主播积分达到政策后,由结算任务发起美元余额奖励入账,用户提现只能提现 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`。
|
||||
|
||||
资源组可以通过资源域的 `wallet_asset` 组成员直接给用户发放 `COIN` 或 `DIAMOND`,但这仍然是钱包入账流水,不写用户资源权益;钻石后续兑换仍必须走独立兑换订单。
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
| 充值 | `PARTIAL` | 币商转账已写 `wallet_recharge_records`,provider order 未实现 | Apple、Google、线下订单和 provider 校验仍需补齐 |
|
||||
| 币商转金币 | `DONE` | `TransferCoinFromSeller` 扣 `COIN_SELLER_COIN`、加玩家 `COIN`,并按区域充值政策记录美元充值金额 | 限额、线下订单和风控审计仍需补齐 |
|
||||
| 钻石兑换 | `TODO` | 无 diamond exchange order | 需要政策快照、兑换状态机和原子出入账 |
|
||||
| 主播积分/奖励 | `PARTIAL` | 送礼实时给收礼人加 `GIFT_POINT` | 周期结算任务和 `USD_BALANCE` 奖励入账未落地 |
|
||||
| 主播积分/奖励 | `PARTIAL` | 送礼实时给收礼人加 `GIFT_POINT` | 周期结算任务和 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 奖励入账未落地 |
|
||||
| 提现 | `TODO` | 无 withdraw request | 需要冻结、审核、人工打款、失败回滚 |
|
||||
| 钱包 outbox | `PARTIAL` | `wallet_outbox` 已和交易同事务写入 `WalletBalanceChanged/WalletGiftDebited/WalletCoinSellerTransferred/WalletRechargeRecorded` | outbox worker 和 MQ 投递适配未落地 |
|
||||
|
||||
@ -83,8 +83,8 @@
|
||||
### P4: Anchor Reward And Withdraw
|
||||
|
||||
- 送礼实时加主播 `GIFT_POINT`,但美元奖励由结算任务按政策转换,不能写死在送礼链路。
|
||||
- 奖励结算生成 `anchor_reward_settlements`,确认后加主播 `USD_BALANCE`。
|
||||
- 提现申请必须先冻结 `USD_BALANCE`,审核拒绝解冻,审核通过后进入人工打款流程。
|
||||
- 奖励结算生成 `anchor_reward_settlements`,确认后加主播 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`。
|
||||
- 提现申请必须先冻结 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`,审核拒绝解冻,审核通过后进入人工打款流程。
|
||||
- 人工打款完成后从冻结余额出账;打款失败回到可重试状态,不能自动解冻后丢失审核上下文。
|
||||
|
||||
## Component Diagram
|
||||
@ -229,7 +229,7 @@ wallet_recharge_records(
|
||||
target_region_id BIGINT NOT NULL,
|
||||
policy_id BIGINT NOT NULL,
|
||||
policy_version VARCHAR(64) NOT NULL,
|
||||
currency_code VARCHAR(3) NOT NULL,
|
||||
currency_code VARCHAR(8) NOT NULL,
|
||||
coin_amount BIGINT NOT NULL,
|
||||
usd_minor_amount BIGINT NOT NULL,
|
||||
exchange_coin_amount BIGINT NOT NULL,
|
||||
@ -239,7 +239,7 @@ wallet_recharge_records(
|
||||
);
|
||||
```
|
||||
|
||||
`wallet_recharge_records.usd_minor_amount` 是用户充值额的权威统计口径;用户不会因此获得 `USD_BALANCE`。政策字段必须保存快照,后续修改区域汇率不能重算历史充值。
|
||||
`wallet_recharge_records.usd_minor_amount` 是用户充值额的权威统计口径;用户不会因此获得 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`。政策字段必须保存快照,后续修改区域汇率不能重算历史充值。
|
||||
|
||||
## Send Gift Flow
|
||||
|
||||
@ -321,7 +321,7 @@ stateDiagram-v2
|
||||
flowchart LR
|
||||
A["User DIAMOND account"] -->|"debit diamonds"| X["diamond_exchange_order"]
|
||||
X -->|"policy: diamond to coin"| C["User COIN account"]
|
||||
X -->|"policy: diamond to USD"| U["User USD_BALANCE account"]
|
||||
X -->|"policy: diamond to USD"| U["User identity salary wallet account"]
|
||||
```
|
||||
|
||||
钻石兑换必须冻结政策版本。订单里至少记录 `from_diamond_amount`、`to_asset_type`、`to_amount`、`policy_id`、`rate_snapshot` 和 `status`。兑换成功后写分录和 outbox;兑换失败不能出现钻石扣了但目标资产没到账。
|
||||
@ -332,8 +332,8 @@ flowchart LR
|
||||
|
||||
1. 送礼时钱包给主播增加 `GIFT_POINT`。
|
||||
2. 结算任务读取积分和政策,生成 `anchor_reward_settlements`。
|
||||
3. 结算单确认后调用钱包 `CreditRewardBalance`,增加主播 `USD_BALANCE`。
|
||||
4. 主播提现时先冻结 `USD_BALANCE`,审核拒绝解冻,审核通过后等待人工转账。
|
||||
3. 结算单确认后调用钱包 `CreditRewardBalance`,增加主播 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`。
|
||||
4. 主播提现时先冻结 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`,审核拒绝解冻,审核通过后等待人工转账。
|
||||
5. 人工转账完成后把冻结余额出账,状态改为 `paid`。
|
||||
|
||||
提现状态机:
|
||||
@ -499,7 +499,7 @@ stateDiagram-v2
|
||||
|
||||
提现:
|
||||
|
||||
- 提现申请成功后 `USD_BALANCE.available_amount` 减少,`frozen_amount` 增加。
|
||||
- 提现申请成功后 `identity salary wallet.available_amount` 减少,`frozen_amount` 增加。
|
||||
- 审核拒绝后冻结金额回到 available。
|
||||
- 打款完成后 frozen 减少并写出账分录。
|
||||
- 打款失败后状态可重试,不丢失冻结关系和审核上下文。
|
||||
@ -533,7 +533,7 @@ docker compose config
|
||||
- 幂等键必须覆盖业务语义,同一个 command_id 带不同 payload 必须返回冲突。
|
||||
- Apple/Google 充值以 provider 真实交易号为最终幂等源,不能只信客户端 request id。
|
||||
- 币商充值分两段:平台给币商发 `COIN_SELLER_COIN` 库存,币商再把库存转成用户 `COIN`;必须有币商金币余额、充值政策快照、限额和审计。
|
||||
- `USD_BALANCE` 是可提现负债,调账和奖励发放需要更高权限和审计。
|
||||
- `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 是可提现负债,调账和奖励发放需要更高权限和审计。
|
||||
- 兑换汇率和奖励政策必须记录快照,不能只存当前 policy id。
|
||||
- 提现人工打款前必须冻结余额,打款失败要回到可重新处理状态。
|
||||
- 钱包事件投递使用 outbox,MQ 投递失败不回滚账务事实。
|
||||
|
||||
@ -20,8 +20,8 @@ import (
|
||||
const (
|
||||
// Tencent Cloud RocketMQ accepts this property as an absolute Unix epoch
|
||||
// millisecond delivery timestamp. Open-source RocketMQ delay levels remain
|
||||
// available through the native client, but the treasure open timer needs an
|
||||
// exact backend-configured open_at_ms.
|
||||
// available through the native client, but the rocket launch timer needs an
|
||||
// exact backend-configured launch_at_ms.
|
||||
PropertyStartDeliverTime = "__STARTDELIVERTIME"
|
||||
)
|
||||
|
||||
|
||||
@ -11,10 +11,10 @@ import (
|
||||
|
||||
const (
|
||||
MessageTypeRoomOutboxEvent = "room_outbox_event"
|
||||
MessageTypeRoomTreasureOpenDue = "room_treasure_open_due"
|
||||
MessageTypeRoomRocketLaunchDue = "room_rocket_launch_due"
|
||||
|
||||
TagRoomOutboxEvent = "room_outbox_event"
|
||||
TagRoomTreasureOpenDue = "room_treasure_open_due"
|
||||
TagRoomRocketLaunchDue = "room_rocket_launch_due"
|
||||
)
|
||||
|
||||
// RoomOutboxMessage is the MQ representation of one durable room_outbox fact.
|
||||
@ -29,14 +29,14 @@ type RoomOutboxMessage struct {
|
||||
Envelope []byte `json:"envelope"`
|
||||
}
|
||||
|
||||
// RoomTreasureOpenDueMessage wakes room-service at the configured open_at_ms.
|
||||
type RoomTreasureOpenDueMessage struct {
|
||||
// RoomRocketLaunchDueMessage wakes room-service at the configured launch_at_ms.
|
||||
type RoomRocketLaunchDueMessage struct {
|
||||
MessageType string `json:"message_type"`
|
||||
AppCode string `json:"app_code"`
|
||||
RoomID string `json:"room_id"`
|
||||
BoxID string `json:"box_id"`
|
||||
RocketID string `json:"rocket_id"`
|
||||
Level int32 `json:"level"`
|
||||
OpenAtMS int64 `json:"open_at_ms"`
|
||||
LaunchAtMS int64 `json:"launch_at_ms"`
|
||||
ResetAtMS int64 `json:"reset_at_ms"`
|
||||
CommandID string `json:"command_id"`
|
||||
}
|
||||
@ -81,26 +81,26 @@ func DecodeRoomOutboxMessage(body []byte) (*roomeventsv1.EventEnvelope, RoomOutb
|
||||
return &envelope, message, nil
|
||||
}
|
||||
|
||||
// EncodeRoomTreasureOpenDueMessage serializes an open wakeup command.
|
||||
func EncodeRoomTreasureOpenDueMessage(message RoomTreasureOpenDueMessage) ([]byte, error) {
|
||||
message.MessageType = MessageTypeRoomTreasureOpenDue
|
||||
if message.AppCode == "" || message.RoomID == "" || message.BoxID == "" || message.Level <= 0 || message.OpenAtMS <= 0 {
|
||||
return nil, errors.New("room treasure open due message is incomplete")
|
||||
// EncodeRoomRocketLaunchDueMessage serializes a launch wakeup command.
|
||||
func EncodeRoomRocketLaunchDueMessage(message RoomRocketLaunchDueMessage) ([]byte, error) {
|
||||
message.MessageType = MessageTypeRoomRocketLaunchDue
|
||||
if message.AppCode == "" || message.RoomID == "" || message.RocketID == "" || message.Level <= 0 || message.LaunchAtMS <= 0 {
|
||||
return nil, errors.New("room rocket launch due message is incomplete")
|
||||
}
|
||||
return json.Marshal(message)
|
||||
}
|
||||
|
||||
// DecodeRoomTreasureOpenDueMessage validates an open wakeup body.
|
||||
func DecodeRoomTreasureOpenDueMessage(body []byte) (RoomTreasureOpenDueMessage, error) {
|
||||
var message RoomTreasureOpenDueMessage
|
||||
// DecodeRoomRocketLaunchDueMessage validates a launch wakeup body.
|
||||
func DecodeRoomRocketLaunchDueMessage(body []byte) (RoomRocketLaunchDueMessage, error) {
|
||||
var message RoomRocketLaunchDueMessage
|
||||
if err := json.Unmarshal(body, &message); err != nil {
|
||||
return RoomTreasureOpenDueMessage{}, err
|
||||
return RoomRocketLaunchDueMessage{}, err
|
||||
}
|
||||
if message.MessageType != MessageTypeRoomTreasureOpenDue {
|
||||
return RoomTreasureOpenDueMessage{}, errors.New("unexpected room treasure message_type")
|
||||
if message.MessageType != MessageTypeRoomRocketLaunchDue {
|
||||
return RoomRocketLaunchDueMessage{}, errors.New("unexpected room rocket message_type")
|
||||
}
|
||||
if message.AppCode == "" || message.RoomID == "" || message.BoxID == "" || message.Level <= 0 || message.OpenAtMS <= 0 {
|
||||
return RoomTreasureOpenDueMessage{}, errors.New("room treasure open due message is incomplete")
|
||||
if message.AppCode == "" || message.RoomID == "" || message.RocketID == "" || message.Level <= 0 || message.LaunchAtMS <= 0 {
|
||||
return RoomRocketLaunchDueMessage{}, errors.New("room rocket launch due message is incomplete")
|
||||
}
|
||||
return message, nil
|
||||
}
|
||||
|
||||
@ -8,14 +8,14 @@ import (
|
||||
)
|
||||
|
||||
func TestRoomOutboxMessageRoundTrip(t *testing.T) {
|
||||
body, err := proto.Marshal(&roomeventsv1.RoomTreasureCountdownStarted{BoxId: "box-1", Level: 2, OpenAtMs: 12345})
|
||||
body, err := proto.Marshal(&roomeventsv1.RoomRocketIgnited{RocketId: "rocket-1", Level: 2, LaunchAtMs: 12345})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal body: %v", err)
|
||||
}
|
||||
envelope := &roomeventsv1.EventEnvelope{
|
||||
AppCode: "default",
|
||||
EventId: "evt-1",
|
||||
EventType: "RoomTreasureCountdownStarted",
|
||||
EventType: "RoomRocketIgnited",
|
||||
RoomId: "room-1",
|
||||
RoomVersion: 9,
|
||||
OccurredAtMs: 12300,
|
||||
@ -34,24 +34,24 @@ func TestRoomOutboxMessageRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomTreasureOpenDueRoundTrip(t *testing.T) {
|
||||
encoded, err := EncodeRoomTreasureOpenDueMessage(RoomTreasureOpenDueMessage{
|
||||
AppCode: "default",
|
||||
RoomID: "room-1",
|
||||
BoxID: "box-1",
|
||||
Level: 1,
|
||||
OpenAtMS: 12345,
|
||||
ResetAtMS: 99999,
|
||||
CommandID: "cmd_room_treasure_open_box-1",
|
||||
func TestRoomRocketLaunchDueRoundTrip(t *testing.T) {
|
||||
encoded, err := EncodeRoomRocketLaunchDueMessage(RoomRocketLaunchDueMessage{
|
||||
AppCode: "default",
|
||||
RoomID: "room-1",
|
||||
RocketID: "rocket-1",
|
||||
Level: 1,
|
||||
LaunchAtMS: 12345,
|
||||
ResetAtMS: 99999,
|
||||
CommandID: "cmd_room_rocket_launch_rocket-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
decoded, err := DecodeRoomTreasureOpenDueMessage(encoded)
|
||||
decoded, err := DecodeRoomRocketLaunchDueMessage(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if decoded.MessageType != MessageTypeRoomTreasureOpenDue || decoded.BoxID != "box-1" || decoded.OpenAtMS != 12345 {
|
||||
if decoded.MessageType != MessageTypeRoomRocketLaunchDue || decoded.RocketID != "rocket-1" || decoded.LaunchAtMS != 12345 {
|
||||
t.Fatalf("unexpected decoded message: %#v", decoded)
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +63,12 @@ var catalog = map[Code]Spec{
|
||||
RegionCountryConflict: spec(codes.FailedPrecondition, httpStatusConflict, RegionCountryConflict, "conflict"),
|
||||
RegionDisabled: spec(codes.FailedPrecondition, httpStatusConflict, RegionDisabled, "conflict"),
|
||||
InvalidInviteCode: spec(codes.InvalidArgument, httpStatusBadRequest, InvalidInviteCode, "invalid argument"),
|
||||
DeviceAlreadyRegistered: spec(
|
||||
codes.FailedPrecondition,
|
||||
httpStatusConflict,
|
||||
DeviceAlreadyRegistered,
|
||||
"conflict",
|
||||
),
|
||||
|
||||
InsufficientBalance: spec(codes.FailedPrecondition, httpStatusConflict, InsufficientBalance, "insufficient balance"),
|
||||
DuplicateBillingCommand: spec(codes.AlreadyExists, httpStatusConflict, DuplicateBillingCommand, "conflict"),
|
||||
|
||||
@ -71,6 +71,8 @@ const (
|
||||
RegionDisabled Code = "REGION_DISABLED"
|
||||
// InvalidInviteCode 表示邀请码不存在、停用、跨 App、邀请人不可用或用户尝试自邀。
|
||||
InvalidInviteCode Code = "INVALID_INVITE_CODE"
|
||||
// DeviceAlreadyRegistered 表示同一注册设备已经创建过账号,不能再次作为三方注册入口。
|
||||
DeviceAlreadyRegistered Code = "DEVICE_ALREADY_REGISTERED"
|
||||
|
||||
// InsufficientBalance 表示钱包余额不足。
|
||||
InsufficientBalance Code = "INSUFFICIENT_BALANCE"
|
||||
|
||||
55
scripts/mysql/034_remove_usd_balance_withdrawal_requests.sql
Normal file
55
scripts/mysql/034_remove_usd_balance_withdrawal_requests.sql
Normal file
@ -0,0 +1,55 @@
|
||||
-- Remove the obsolete generic USD withdrawal request table from the wallet database.
|
||||
-- Salary settlement now writes role-specific wallets only:
|
||||
-- HOST_SALARY_USD, AGENCY_SALARY_USD, BD_SALARY_USD, ADMIN_SALARY_USD.
|
||||
-- Keep an archive copy before dropping the source table so production history is not lost.
|
||||
|
||||
SET @source_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'wallet_withdrawal_requests'
|
||||
);
|
||||
|
||||
SET @archive_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'wallet_withdrawal_requests_removed_20260602'
|
||||
);
|
||||
|
||||
SET @sql := IF(
|
||||
@source_exists = 1 AND @archive_exists = 0,
|
||||
'CREATE TABLE wallet_withdrawal_requests_removed_20260602 LIKE wallet_withdrawal_requests',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
@source_exists = 1,
|
||||
'INSERT IGNORE INTO wallet_withdrawal_requests_removed_20260602 SELECT * FROM wallet_withdrawal_requests',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql := IF(
|
||||
@source_exists = 1,
|
||||
'DROP TABLE wallet_withdrawal_requests',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT
|
||||
SUM(TABLE_NAME = 'wallet_withdrawal_requests') AS source_table_exists,
|
||||
SUM(TABLE_NAME = 'wallet_withdrawal_requests_removed_20260602') AS archive_table_exists
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME IN (
|
||||
'wallet_withdrawal_requests',
|
||||
'wallet_withdrawal_requests_removed_20260602'
|
||||
);
|
||||
27
scripts/mysql/035_wallet_recharge_record_currency_code.sql
Normal file
27
scripts/mysql/035_wallet_recharge_record_currency_code.sql
Normal file
@ -0,0 +1,27 @@
|
||||
-- Expand wallet recharge record currency codes so Google Play products such as USDT
|
||||
-- can be snapshotted before payment order auditing and Google consume.
|
||||
|
||||
SET @wallet_recharge_record_currency_ddl := (
|
||||
SELECT CASE
|
||||
WHEN COUNT(*) = 0 THEN 'SELECT 1'
|
||||
WHEN MAX(CHARACTER_MAXIMUM_LENGTH) < 8 THEN
|
||||
'ALTER TABLE wallet_recharge_records MODIFY COLUMN currency_code VARCHAR(8) NOT NULL COMMENT ''币种编码'''
|
||||
ELSE 'SELECT 1'
|
||||
END
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'wallet_recharge_records'
|
||||
AND COLUMN_NAME = 'currency_code'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @wallet_recharge_record_currency_ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT
|
||||
COLUMN_TYPE AS wallet_recharge_records_currency_code_type,
|
||||
IS_NULLABLE AS wallet_recharge_records_currency_code_nullable
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'wallet_recharge_records'
|
||||
AND COLUMN_NAME = 'currency_code';
|
||||
5
scripts/mysql/036_user_register_device_unique.sql
Normal file
5
scripts/mysql/036_user_register_device_unique.sql
Normal file
@ -0,0 +1,5 @@
|
||||
USE hyapp_user;
|
||||
|
||||
-- 同一个 App 内,一个非空注册设备号只能创建一个用户;NULL 仍用于没有注册设备快照的历史或测试行。
|
||||
ALTER TABLE users
|
||||
ADD UNIQUE KEY uk_users_register_device_id (app_code, register_device_id);
|
||||
18
scripts/mysql/037_gift_diamond_global_defaults.sql
Normal file
18
scripts/mysql/037_gift_diamond_global_defaults.sql
Normal file
@ -0,0 +1,18 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE hyapp_wallet;
|
||||
|
||||
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
||||
|
||||
INSERT INTO gift_diamond_ratio_configs (
|
||||
app_code, region_id, gift_type_code, status, ratio_percent,
|
||||
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES
|
||||
('lalu', 0, 'normal', 'active', 100.00, 0, 0, @now_ms, @now_ms),
|
||||
('lalu', 0, 'lucky', 'active', 10.00, 0, 0, @now_ms, @now_ms),
|
||||
('lalu', 0, 'super_lucky', 'active', 1.00, 0, 0, @now_ms, @now_ms)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
status = VALUES(status),
|
||||
ratio_percent = VALUES(ratio_percent),
|
||||
updated_by_admin_id = VALUES(updated_by_admin_id),
|
||||
updated_at_ms = VALUES(updated_at_ms);
|
||||
@ -58,7 +58,6 @@ LEFT JOIN (
|
||||
SET p.wealth_badge_resource_id = COALESCE(picked.resource_id, 0),
|
||||
p.wealth_badge_source_level = COALESCE(picked.level, 0),
|
||||
p.updated_at_ms = UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000;
|
||||
|
||||
UPDATE hyapp_activity.user_level_display_profiles AS p
|
||||
LEFT JOIN (
|
||||
SELECT a.app_code, a.user_id, refs.level, refs.resource_id
|
||||
@ -94,4 +93,3 @@ LEFT JOIN (
|
||||
SET p.charm_badge_resource_id = COALESCE(picked.resource_id, 0),
|
||||
p.charm_badge_source_level = COALESCE(picked.level, 0),
|
||||
p.updated_at_ms = UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000;
|
||||
|
||||
|
||||
@ -32,6 +32,7 @@ import (
|
||||
authmodule "hyapp-admin-server/internal/modules/auth"
|
||||
coinledgermodule "hyapp-admin-server/internal/modules/coinledger"
|
||||
countryregionmodule "hyapp-admin-server/internal/modules/countryregion"
|
||||
cumulativerechargerewardmodule "hyapp-admin-server/internal/modules/cumulativerechargereward"
|
||||
dailytaskmodule "hyapp-admin-server/internal/modules/dailytask"
|
||||
dashboardmodule "hyapp-admin-server/internal/modules/dashboard"
|
||||
firstrechargerewardmodule "hyapp-admin-server/internal/modules/firstrechargereward"
|
||||
@ -53,7 +54,8 @@ import (
|
||||
reportmodule "hyapp-admin-server/internal/modules/report"
|
||||
resourcemodule "hyapp-admin-server/internal/modules/resource"
|
||||
roomadminmodule "hyapp-admin-server/internal/modules/roomadmin"
|
||||
roomtreasuremodule "hyapp-admin-server/internal/modules/roomtreasure"
|
||||
roomrocketmodule "hyapp-admin-server/internal/modules/roomrocket"
|
||||
roomturnoverrewardmodule "hyapp-admin-server/internal/modules/roomturnoverreward"
|
||||
searchmodule "hyapp-admin-server/internal/modules/search"
|
||||
sevendaycheckinmodule "hyapp-admin-server/internal/modules/sevendaycheckin"
|
||||
teamsalarypolicymodule "hyapp-admin-server/internal/modules/teamsalarypolicy"
|
||||
@ -61,6 +63,7 @@ import (
|
||||
uploadmodule "hyapp-admin-server/internal/modules/upload"
|
||||
userleaderboardmodule "hyapp-admin-server/internal/modules/userleaderboard"
|
||||
vipconfigmodule "hyapp-admin-server/internal/modules/vipconfig"
|
||||
weeklystarmodule "hyapp-admin-server/internal/modules/weeklystar"
|
||||
"hyapp-admin-server/internal/platform/logging"
|
||||
"hyapp-admin-server/internal/platform/tencentcos"
|
||||
"hyapp-admin-server/internal/repository"
|
||||
@ -70,6 +73,7 @@ import (
|
||||
mysqlDriver "github.com/go-sql-driver/mysql"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
@ -154,32 +158,32 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
userConn, err := grpc.Dial(cfg.UserService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
userConn, err := dialBackendGRPC(cfg.UserService.Addr)
|
||||
if err != nil {
|
||||
fatalRuntime("connect_user_service_failed", err)
|
||||
}
|
||||
defer userConn.Close()
|
||||
|
||||
walletConn, err := grpc.Dial(cfg.WalletService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
walletConn, err := dialBackendGRPC(cfg.WalletService.Addr)
|
||||
if err != nil {
|
||||
fatalRuntime("connect_wallet_service_failed", err)
|
||||
}
|
||||
defer walletConn.Close()
|
||||
|
||||
roomConn, err := grpc.Dial(cfg.RoomService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
roomConn, err := dialBackendGRPC(cfg.RoomService.Addr)
|
||||
if err != nil {
|
||||
fatalRuntime("connect_room_service_failed", err)
|
||||
}
|
||||
defer roomConn.Close()
|
||||
roomClient := roomclient.NewGRPC(roomConn)
|
||||
|
||||
activityConn, err := grpc.Dial(cfg.ActivityService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
activityConn, err := dialBackendGRPC(cfg.ActivityService.Addr)
|
||||
if err != nil {
|
||||
fatalRuntime("connect_activity_service_failed", err)
|
||||
}
|
||||
defer activityConn.Close()
|
||||
|
||||
gameConn, err := grpc.Dial(cfg.GameService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
gameConn, err := dialBackendGRPC(cfg.GameService.Addr)
|
||||
if err != nil {
|
||||
fatalRuntime("connect_game_service_failed", err)
|
||||
}
|
||||
@ -226,6 +230,7 @@ func main() {
|
||||
AppUser: appusermodule.New(userclient.NewGRPC(userConn), activityclient.NewGRPC(activityConn), sqlDB, userDB, walletDB, cfg, auditHandler),
|
||||
CoinLedger: coinledgermodule.New(userDB, walletDB, sqlDB, walletclient.NewGRPC(walletConn), auditHandler),
|
||||
CountryRegion: countryregionmodule.New(userclient.NewGRPC(userConn), userDB, cfg, auditHandler),
|
||||
CumulativeRecharge: cumulativerechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||
DailyTask: dailytaskmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||
Dashboard: dashboardmodule.New(store, cfg),
|
||||
FirstRechargeReward: firstrechargerewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||
@ -239,15 +244,16 @@ func main() {
|
||||
LevelConfig: levelconfigmodule.New(activityclient.NewGRPC(activityConn), walletclient.NewGRPC(walletConn), auditHandler),
|
||||
LuckyGift: luckygiftmodule.New(activityclient.NewGRPC(activityConn), cfg.ActivityService.RequestTimeout, auditHandler),
|
||||
Menu: menumodule.New(store, auditHandler),
|
||||
Payment: paymentmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
|
||||
Payment: paymentmodule.New(walletclient.NewGRPC(walletConn), userDB, auditHandler),
|
||||
RBAC: rbacmodule.New(store, auditHandler),
|
||||
RedPacket: redpacketmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
|
||||
Report: reportmodule.New(userDB, roomClient),
|
||||
RegistrationReward: registrationrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||
RegionBlock: regionblockmodule.New(userDB, auditHandler),
|
||||
Resource: resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, auditHandler),
|
||||
Resource: resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, cfg.WalletService.RequestTimeout, auditHandler),
|
||||
RoomAdmin: roomadminmodule.New(userDB, roomClient, auditHandler),
|
||||
RoomTreasure: roomtreasuremodule.New(roomClient, auditHandler),
|
||||
RoomRocket: roomrocketmodule.New(roomClient, auditHandler),
|
||||
RoomTurnoverReward: roomturnoverrewardmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||
Search: searchmodule.New(store),
|
||||
SevenDayCheckIn: sevendaycheckinmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||
TeamSalaryPolicy: teamsalarypolicymodule.New(store, auditHandler),
|
||||
@ -255,6 +261,7 @@ func main() {
|
||||
Upload: uploadmodule.New(objectUploader, cfg.TencentCOS.ObjectPrefix, auditHandler),
|
||||
UserLeaderboard: userleaderboardmodule.New(walletDB, userDB, roomClient),
|
||||
VIPConfig: vipconfigmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
|
||||
WeeklyStar: weeklystarmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||
}
|
||||
engine := router.New(cfg, auth, handlers)
|
||||
|
||||
@ -287,6 +294,19 @@ func fatalRuntime(msg string, err error) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func dialBackendGRPC(addr string) (*grpc.ClientConn, error) {
|
||||
// admin-server 在广州跨区访问 Saudi 内网 CLB;gRPC 长连接如果长时间空闲,
|
||||
// TCP 层可能留下半开连接。客户端 keepalive 主动探活并触发重连,避免请求卡到 HTTP 超时。
|
||||
return grpc.Dial(addr,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: 30 * time.Second,
|
||||
Timeout: 10 * time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func ensureDatabase(dsn string) error {
|
||||
parsed, err := mysqlDriver.ParseDSN(dsn)
|
||||
if err != nil {
|
||||
|
||||
@ -36,10 +36,10 @@ bootstrap:
|
||||
password: "REPLACE_WITH_TEMP_BOOTSTRAP_PASSWORD"
|
||||
user_service:
|
||||
addr: "10.2.1.16:13005"
|
||||
request_timeout: "3s"
|
||||
request_timeout: "10s"
|
||||
wallet_service:
|
||||
addr: "10.2.1.15:13004"
|
||||
request_timeout: "3s"
|
||||
request_timeout: "10s"
|
||||
room_service:
|
||||
addr: "10.2.1.16:13001"
|
||||
request_timeout: "3s"
|
||||
|
||||
@ -245,8 +245,8 @@
|
||||
| `room:update` | `button` | 后台修改房间展示资料、模式、状态、区域 |
|
||||
| `room:delete` | `button` | 后台删除房间 |
|
||||
| `room-pin:view` | `menu` | 房间置顶菜单、置顶列表 |
|
||||
| `room-pin:create` | `button` | 新增或恢复区域房间置顶 |
|
||||
| `room-pin:cancel` | `button` | 取消区域房间置顶 |
|
||||
| `room-pin:create` | `button` | 新增或恢复房间置顶 |
|
||||
| `room-pin:cancel` | `button` | 取消房间置顶 |
|
||||
| `room-config:view` | `menu` | 房间配置页面、读取座位数配置 |
|
||||
| `room-config:update` | `button` | 保存启用座位数和默认座位数 |
|
||||
| `room:create` | `button` | 后台创建房间 |
|
||||
|
||||
@ -21,9 +21,23 @@ type Client interface {
|
||||
GetFirstRechargeRewardConfig(ctx context.Context, req *activityv1.GetFirstRechargeRewardConfigRequest) (*activityv1.GetFirstRechargeRewardConfigResponse, error)
|
||||
UpdateFirstRechargeRewardConfig(ctx context.Context, req *activityv1.UpdateFirstRechargeRewardConfigRequest) (*activityv1.UpdateFirstRechargeRewardConfigResponse, error)
|
||||
ListFirstRechargeRewardClaims(ctx context.Context, req *activityv1.ListFirstRechargeRewardClaimsRequest) (*activityv1.ListFirstRechargeRewardClaimsResponse, error)
|
||||
GetCumulativeRechargeRewardConfig(ctx context.Context, req *activityv1.GetCumulativeRechargeRewardConfigRequest) (*activityv1.GetCumulativeRechargeRewardConfigResponse, error)
|
||||
UpdateCumulativeRechargeRewardConfig(ctx context.Context, req *activityv1.UpdateCumulativeRechargeRewardConfigRequest) (*activityv1.UpdateCumulativeRechargeRewardConfigResponse, error)
|
||||
ListCumulativeRechargeRewardGrants(ctx context.Context, req *activityv1.ListCumulativeRechargeRewardGrantsRequest) (*activityv1.ListCumulativeRechargeRewardGrantsResponse, error)
|
||||
GetSevenDayCheckInConfig(ctx context.Context, req *activityv1.GetSevenDayCheckInConfigRequest) (*activityv1.GetSevenDayCheckInConfigResponse, error)
|
||||
UpdateSevenDayCheckInConfig(ctx context.Context, req *activityv1.UpdateSevenDayCheckInConfigRequest) (*activityv1.UpdateSevenDayCheckInConfigResponse, error)
|
||||
ListSevenDayCheckInClaims(ctx context.Context, req *activityv1.ListSevenDayCheckInClaimsRequest) (*activityv1.ListSevenDayCheckInClaimsResponse, error)
|
||||
GetRoomTurnoverRewardConfig(ctx context.Context, req *activityv1.GetRoomTurnoverRewardConfigRequest) (*activityv1.GetRoomTurnoverRewardConfigResponse, error)
|
||||
UpdateRoomTurnoverRewardConfig(ctx context.Context, req *activityv1.UpdateRoomTurnoverRewardConfigRequest) (*activityv1.UpdateRoomTurnoverRewardConfigResponse, error)
|
||||
ListRoomTurnoverRewardSettlements(ctx context.Context, req *activityv1.ListRoomTurnoverRewardSettlementsRequest) (*activityv1.ListRoomTurnoverRewardSettlementsResponse, error)
|
||||
RetryRoomTurnoverRewardSettlement(ctx context.Context, req *activityv1.RetryRoomTurnoverRewardSettlementRequest) (*activityv1.RetryRoomTurnoverRewardSettlementResponse, error)
|
||||
ListWeeklyStarCycles(ctx context.Context, req *activityv1.ListWeeklyStarCyclesRequest) (*activityv1.ListWeeklyStarCyclesResponse, error)
|
||||
CreateWeeklyStarCycle(ctx context.Context, req *activityv1.UpsertWeeklyStarCycleRequest) (*activityv1.UpsertWeeklyStarCycleResponse, error)
|
||||
GetWeeklyStarCycle(ctx context.Context, req *activityv1.GetWeeklyStarCycleRequest) (*activityv1.GetWeeklyStarCycleResponse, error)
|
||||
UpdateWeeklyStarCycle(ctx context.Context, req *activityv1.UpsertWeeklyStarCycleRequest) (*activityv1.UpsertWeeklyStarCycleResponse, error)
|
||||
SetWeeklyStarCycleStatus(ctx context.Context, req *activityv1.SetWeeklyStarCycleStatusRequest) (*activityv1.SetWeeklyStarCycleStatusResponse, error)
|
||||
ListWeeklyStarLeaderboard(ctx context.Context, req *activityv1.ListWeeklyStarLeaderboardRequest) (*activityv1.ListWeeklyStarLeaderboardResponse, error)
|
||||
ListWeeklyStarSettlements(ctx context.Context, req *activityv1.ListWeeklyStarSettlementsRequest) (*activityv1.ListWeeklyStarSettlementsResponse, error)
|
||||
GetLuckyGiftConfig(ctx context.Context, req *activityv1.GetLuckyGiftConfigRequest) (*activityv1.GetLuckyGiftConfigResponse, error)
|
||||
UpsertLuckyGiftConfig(ctx context.Context, req *activityv1.UpsertLuckyGiftConfigRequest) (*activityv1.UpsertLuckyGiftConfigResponse, error)
|
||||
ListLuckyGiftConfigs(ctx context.Context, req *activityv1.ListLuckyGiftConfigsRequest) (*activityv1.ListLuckyGiftConfigsResponse, error)
|
||||
@ -41,7 +55,10 @@ type GRPCClient struct {
|
||||
achievementClient activityv1.AdminAchievementServiceClient
|
||||
registrationRewardClient activityv1.AdminRegistrationRewardServiceClient
|
||||
firstRechargeRewardClient activityv1.AdminFirstRechargeRewardServiceClient
|
||||
cumulativeRechargeClient activityv1.AdminCumulativeRechargeRewardServiceClient
|
||||
sevenDayCheckInClient activityv1.AdminSevenDayCheckInServiceClient
|
||||
roomTurnoverRewardClient activityv1.AdminRoomTurnoverRewardServiceClient
|
||||
weeklyStarClient activityv1.AdminWeeklyStarServiceClient
|
||||
luckyGiftClient activityv1.AdminLuckyGiftServiceClient
|
||||
broadcastClient activityv1.BroadcastServiceClient
|
||||
growthClient activityv1.AdminGrowthLevelServiceClient
|
||||
@ -53,7 +70,10 @@ func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
|
||||
achievementClient: activityv1.NewAdminAchievementServiceClient(conn),
|
||||
registrationRewardClient: activityv1.NewAdminRegistrationRewardServiceClient(conn),
|
||||
firstRechargeRewardClient: activityv1.NewAdminFirstRechargeRewardServiceClient(conn),
|
||||
cumulativeRechargeClient: activityv1.NewAdminCumulativeRechargeRewardServiceClient(conn),
|
||||
sevenDayCheckInClient: activityv1.NewAdminSevenDayCheckInServiceClient(conn),
|
||||
roomTurnoverRewardClient: activityv1.NewAdminRoomTurnoverRewardServiceClient(conn),
|
||||
weeklyStarClient: activityv1.NewAdminWeeklyStarServiceClient(conn),
|
||||
luckyGiftClient: activityv1.NewAdminLuckyGiftServiceClient(conn),
|
||||
broadcastClient: activityv1.NewBroadcastServiceClient(conn),
|
||||
growthClient: activityv1.NewAdminGrowthLevelServiceClient(conn),
|
||||
@ -104,6 +124,18 @@ func (c *GRPCClient) ListFirstRechargeRewardClaims(ctx context.Context, req *act
|
||||
return c.firstRechargeRewardClient.ListFirstRechargeRewardClaims(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetCumulativeRechargeRewardConfig(ctx context.Context, req *activityv1.GetCumulativeRechargeRewardConfigRequest) (*activityv1.GetCumulativeRechargeRewardConfigResponse, error) {
|
||||
return c.cumulativeRechargeClient.GetCumulativeRechargeRewardConfig(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UpdateCumulativeRechargeRewardConfig(ctx context.Context, req *activityv1.UpdateCumulativeRechargeRewardConfigRequest) (*activityv1.UpdateCumulativeRechargeRewardConfigResponse, error) {
|
||||
return c.cumulativeRechargeClient.UpdateCumulativeRechargeRewardConfig(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListCumulativeRechargeRewardGrants(ctx context.Context, req *activityv1.ListCumulativeRechargeRewardGrantsRequest) (*activityv1.ListCumulativeRechargeRewardGrantsResponse, error) {
|
||||
return c.cumulativeRechargeClient.ListCumulativeRechargeRewardGrants(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetSevenDayCheckInConfig(ctx context.Context, req *activityv1.GetSevenDayCheckInConfigRequest) (*activityv1.GetSevenDayCheckInConfigResponse, error) {
|
||||
return c.sevenDayCheckInClient.GetSevenDayCheckInConfig(ctx, req)
|
||||
}
|
||||
@ -116,6 +148,50 @@ func (c *GRPCClient) ListSevenDayCheckInClaims(ctx context.Context, req *activit
|
||||
return c.sevenDayCheckInClient.ListSevenDayCheckInClaims(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetRoomTurnoverRewardConfig(ctx context.Context, req *activityv1.GetRoomTurnoverRewardConfigRequest) (*activityv1.GetRoomTurnoverRewardConfigResponse, error) {
|
||||
return c.roomTurnoverRewardClient.GetRoomTurnoverRewardConfig(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UpdateRoomTurnoverRewardConfig(ctx context.Context, req *activityv1.UpdateRoomTurnoverRewardConfigRequest) (*activityv1.UpdateRoomTurnoverRewardConfigResponse, error) {
|
||||
return c.roomTurnoverRewardClient.UpdateRoomTurnoverRewardConfig(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListRoomTurnoverRewardSettlements(ctx context.Context, req *activityv1.ListRoomTurnoverRewardSettlementsRequest) (*activityv1.ListRoomTurnoverRewardSettlementsResponse, error) {
|
||||
return c.roomTurnoverRewardClient.ListRoomTurnoverRewardSettlements(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) RetryRoomTurnoverRewardSettlement(ctx context.Context, req *activityv1.RetryRoomTurnoverRewardSettlementRequest) (*activityv1.RetryRoomTurnoverRewardSettlementResponse, error) {
|
||||
return c.roomTurnoverRewardClient.RetryRoomTurnoverRewardSettlement(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListWeeklyStarCycles(ctx context.Context, req *activityv1.ListWeeklyStarCyclesRequest) (*activityv1.ListWeeklyStarCyclesResponse, error) {
|
||||
return c.weeklyStarClient.ListWeeklyStarCycles(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) CreateWeeklyStarCycle(ctx context.Context, req *activityv1.UpsertWeeklyStarCycleRequest) (*activityv1.UpsertWeeklyStarCycleResponse, error) {
|
||||
return c.weeklyStarClient.CreateWeeklyStarCycle(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetWeeklyStarCycle(ctx context.Context, req *activityv1.GetWeeklyStarCycleRequest) (*activityv1.GetWeeklyStarCycleResponse, error) {
|
||||
return c.weeklyStarClient.GetWeeklyStarCycle(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UpdateWeeklyStarCycle(ctx context.Context, req *activityv1.UpsertWeeklyStarCycleRequest) (*activityv1.UpsertWeeklyStarCycleResponse, error) {
|
||||
return c.weeklyStarClient.UpdateWeeklyStarCycle(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) SetWeeklyStarCycleStatus(ctx context.Context, req *activityv1.SetWeeklyStarCycleStatusRequest) (*activityv1.SetWeeklyStarCycleStatusResponse, error) {
|
||||
return c.weeklyStarClient.SetWeeklyStarCycleStatus(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListWeeklyStarLeaderboard(ctx context.Context, req *activityv1.ListWeeklyStarLeaderboardRequest) (*activityv1.ListWeeklyStarLeaderboardResponse, error) {
|
||||
return c.weeklyStarClient.ListWeeklyStarLeaderboard(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListWeeklyStarSettlements(ctx context.Context, req *activityv1.ListWeeklyStarSettlementsRequest) (*activityv1.ListWeeklyStarSettlementsResponse, error) {
|
||||
return c.weeklyStarClient.ListWeeklyStarSettlements(ctx, req)
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetLuckyGiftConfig(ctx context.Context, req *activityv1.GetLuckyGiftConfigRequest) (*activityv1.GetLuckyGiftConfigResponse, error) {
|
||||
return c.luckyGiftClient.GetLuckyGiftConfig(ctx, req)
|
||||
}
|
||||
|
||||
@ -17,8 +17,8 @@ type Client interface {
|
||||
GetRoom(ctx context.Context, req GetRoomRequest) (*Room, error)
|
||||
UpdateRoom(ctx context.Context, req UpdateRoomRequest) (*CloseRoomResult, error)
|
||||
DeleteRoom(ctx context.Context, req DeleteRoomRequest) (*CloseRoomResult, error)
|
||||
GetRoomTreasureConfig(ctx context.Context) (RoomTreasureConfig, error)
|
||||
UpdateRoomTreasureConfig(ctx context.Context, req UpdateRoomTreasureConfigRequest) (RoomTreasureConfig, error)
|
||||
GetRoomRocketConfig(ctx context.Context) (RoomRocketConfig, error)
|
||||
UpdateRoomRocketConfig(ctx context.Context, req UpdateRoomRocketConfigRequest) (RoomRocketConfig, error)
|
||||
GetRoomSeatConfig(ctx context.Context) (RoomSeatConfig, error)
|
||||
UpdateRoomSeatConfig(ctx context.Context, req UpdateRoomSeatConfigRequest) (RoomSeatConfig, error)
|
||||
ListRoomPins(ctx context.Context, req ListRoomPinsRequest) (ListRoomPinsResult, error)
|
||||
@ -34,6 +34,7 @@ type ListRoomsRequest struct {
|
||||
Keyword string
|
||||
Status string
|
||||
RegionID int64
|
||||
OwnerUserID int64
|
||||
SortBy string
|
||||
SortDirection string
|
||||
}
|
||||
@ -93,36 +94,36 @@ type Room struct {
|
||||
UpdatedAtMS int64
|
||||
}
|
||||
|
||||
type RoomTreasureConfig struct {
|
||||
type RoomRocketConfig struct {
|
||||
AppCode string
|
||||
Enabled bool
|
||||
ConfigVersion int64
|
||||
EnergySource string
|
||||
OpenDelayMS int64
|
||||
FuelSource string
|
||||
LaunchDelayMS int64
|
||||
BroadcastEnabled bool
|
||||
BroadcastScope string
|
||||
BroadcastDelayMS int64
|
||||
RewardStackPolicy string
|
||||
Levels []RoomTreasureLevelConfig
|
||||
GiftEnergyRules []GiftEnergyRuleConfig
|
||||
Levels []RoomRocketLevelConfig
|
||||
GiftFuelRules []GiftFuelRuleConfig
|
||||
UpdatedByAdminID int64
|
||||
CreatedAtMS int64
|
||||
UpdatedAtMS int64
|
||||
}
|
||||
|
||||
type RoomTreasureLevelConfig struct {
|
||||
Level int32
|
||||
EnergyThreshold int64
|
||||
CoverURL string
|
||||
AnimationURL string
|
||||
OpeningAnimationURL string
|
||||
OpenedImageURL string
|
||||
InRoomRewards []RoomTreasureRewardItem
|
||||
Top1Rewards []RoomTreasureRewardItem
|
||||
IgniterRewards []RoomTreasureRewardItem
|
||||
type RoomRocketLevelConfig struct {
|
||||
Level int32
|
||||
FuelThreshold int64
|
||||
CoverURL string
|
||||
AnimationURL string
|
||||
LaunchAnimationURL string
|
||||
LaunchedImageURL string
|
||||
InRoomRewards []RoomRocketRewardItem
|
||||
Top1Rewards []RoomRocketRewardItem
|
||||
IgniterRewards []RoomRocketRewardItem
|
||||
}
|
||||
|
||||
type RoomTreasureRewardItem struct {
|
||||
type RoomRocketRewardItem struct {
|
||||
RewardItemID string
|
||||
ResourceGroupID int64
|
||||
Weight int64
|
||||
@ -130,17 +131,17 @@ type RoomTreasureRewardItem struct {
|
||||
IconURL string
|
||||
}
|
||||
|
||||
type GiftEnergyRuleConfig struct {
|
||||
type GiftFuelRuleConfig struct {
|
||||
RuleID string
|
||||
GiftID string
|
||||
GiftTypeCode string
|
||||
MultiplierPPM int64
|
||||
FixedEnergy int64
|
||||
FixedFuel int64
|
||||
Excluded bool
|
||||
}
|
||||
|
||||
type UpdateRoomTreasureConfigRequest struct {
|
||||
Config RoomTreasureConfig
|
||||
type UpdateRoomRocketConfigRequest struct {
|
||||
Config RoomRocketConfig
|
||||
AdminID int64
|
||||
}
|
||||
|
||||
@ -169,6 +170,7 @@ type RoomPinRoom struct {
|
||||
type RoomPin struct {
|
||||
ID int64
|
||||
VisibleRegionID int64
|
||||
PinType string
|
||||
RoomID string
|
||||
Weight int64
|
||||
Status string
|
||||
@ -188,6 +190,7 @@ type ListRoomPinsRequest struct {
|
||||
Keyword string
|
||||
Status string
|
||||
RegionID int64
|
||||
PinType string
|
||||
}
|
||||
|
||||
type ListRoomPinsResult struct {
|
||||
@ -197,8 +200,11 @@ type ListRoomPinsResult struct {
|
||||
|
||||
type CreateRoomPinRequest struct {
|
||||
RoomID string
|
||||
PinType string
|
||||
Weight int64
|
||||
DurationDays int64
|
||||
PinnedAtMS int64
|
||||
ExpiresAtMS int64
|
||||
AdminID uint64
|
||||
}
|
||||
|
||||
@ -245,6 +251,7 @@ func (c *GRPCClient) ListRooms(ctx context.Context, req ListRoomsRequest) (*List
|
||||
Keyword: strings.TrimSpace(req.Keyword),
|
||||
Status: strings.TrimSpace(req.Status),
|
||||
VisibleRegionId: req.RegionID,
|
||||
OwnerUserId: req.OwnerUserID,
|
||||
SortBy: strings.TrimSpace(req.SortBy),
|
||||
SortDirection: strings.TrimSpace(req.SortDirection),
|
||||
})
|
||||
@ -301,26 +308,26 @@ func (c *GRPCClient) DeleteRoom(ctx context.Context, req DeleteRoomRequest) (*Cl
|
||||
return closeRoomResultFromCommand(resp.GetResult(), resp.GetRoom()), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetRoomTreasureConfig(ctx context.Context) (RoomTreasureConfig, error) {
|
||||
resp, err := c.queryClient.AdminGetRoomTreasureConfig(ctx, &roomv1.AdminGetRoomTreasureConfigRequest{
|
||||
Meta: requestMeta(ctx, "", 0, "admin-get-room-treasure-config"),
|
||||
func (c *GRPCClient) GetRoomRocketConfig(ctx context.Context) (RoomRocketConfig, error) {
|
||||
resp, err := c.queryClient.AdminGetRoomRocketConfig(ctx, &roomv1.AdminGetRoomRocketConfigRequest{
|
||||
Meta: requestMeta(ctx, "", 0, "admin-get-room-rocket-config"),
|
||||
})
|
||||
if err != nil {
|
||||
return RoomTreasureConfig{}, err
|
||||
return RoomRocketConfig{}, err
|
||||
}
|
||||
return roomTreasureConfigFromProto(resp.GetConfig()), nil
|
||||
return roomRocketConfigFromProto(resp.GetConfig()), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UpdateRoomTreasureConfig(ctx context.Context, req UpdateRoomTreasureConfigRequest) (RoomTreasureConfig, error) {
|
||||
resp, err := c.client.AdminUpdateRoomTreasureConfig(ctx, &roomv1.AdminUpdateRoomTreasureConfigRequest{
|
||||
Meta: requestMeta(ctx, "", req.AdminID, "admin-update-room-treasure-config"),
|
||||
Config: roomTreasureConfigToProto(req.Config),
|
||||
func (c *GRPCClient) UpdateRoomRocketConfig(ctx context.Context, req UpdateRoomRocketConfigRequest) (RoomRocketConfig, error) {
|
||||
resp, err := c.client.AdminUpdateRoomRocketConfig(ctx, &roomv1.AdminUpdateRoomRocketConfigRequest{
|
||||
Meta: requestMeta(ctx, "", req.AdminID, "admin-update-room-rocket-config"),
|
||||
Config: roomRocketConfigToProto(req.Config),
|
||||
AdminId: req.AdminID,
|
||||
})
|
||||
if err != nil {
|
||||
return RoomTreasureConfig{}, err
|
||||
return RoomRocketConfig{}, err
|
||||
}
|
||||
return roomTreasureConfigFromProto(resp.GetConfig()), nil
|
||||
return roomRocketConfigFromProto(resp.GetConfig()), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetRoomSeatConfig(ctx context.Context) (RoomSeatConfig, error) {
|
||||
@ -351,6 +358,7 @@ func (c *GRPCClient) ListRoomPins(ctx context.Context, req ListRoomPinsRequest)
|
||||
Keyword: strings.TrimSpace(req.Keyword),
|
||||
Status: strings.TrimSpace(req.Status),
|
||||
VisibleRegionId: req.RegionID,
|
||||
PinType: strings.TrimSpace(req.PinType),
|
||||
})
|
||||
if err != nil {
|
||||
return ListRoomPinsResult{}, err
|
||||
@ -366,8 +374,11 @@ func (c *GRPCClient) CreateRoomPin(ctx context.Context, req CreateRoomPinRequest
|
||||
resp, err := c.client.AdminCreateRoomPin(ctx, &roomv1.AdminCreateRoomPinRequest{
|
||||
Meta: requestMeta(ctx, strings.TrimSpace(req.RoomID), int64(req.AdminID), "admin-create-room-pin"),
|
||||
RoomId: strings.TrimSpace(req.RoomID),
|
||||
PinType: strings.TrimSpace(req.PinType),
|
||||
Weight: req.Weight,
|
||||
DurationDays: req.DurationDays,
|
||||
PinnedAtMs: req.PinnedAtMS,
|
||||
ExpiresAtMs: req.ExpiresAtMS,
|
||||
AdminId: req.AdminID,
|
||||
})
|
||||
if err != nil {
|
||||
@ -483,22 +494,22 @@ func roomFromProto(item *roomv1.AdminRoomListItem) Room {
|
||||
}
|
||||
}
|
||||
|
||||
func roomTreasureConfigFromProto(input *roomv1.AdminRoomTreasureConfig) RoomTreasureConfig {
|
||||
func roomRocketConfigFromProto(input *roomv1.AdminRoomRocketConfig) RoomRocketConfig {
|
||||
if input == nil {
|
||||
return RoomTreasureConfig{}
|
||||
return RoomRocketConfig{}
|
||||
}
|
||||
return RoomTreasureConfig{
|
||||
return RoomRocketConfig{
|
||||
AppCode: input.GetAppCode(),
|
||||
Enabled: input.GetEnabled(),
|
||||
ConfigVersion: input.GetConfigVersion(),
|
||||
EnergySource: input.GetEnergySource(),
|
||||
OpenDelayMS: input.GetOpenDelayMs(),
|
||||
FuelSource: input.GetFuelSource(),
|
||||
LaunchDelayMS: input.GetLaunchDelayMs(),
|
||||
BroadcastEnabled: input.GetBroadcastEnabled(),
|
||||
BroadcastScope: input.GetBroadcastScope(),
|
||||
BroadcastDelayMS: input.GetBroadcastDelayMs(),
|
||||
RewardStackPolicy: input.GetRewardStackPolicy(),
|
||||
Levels: roomTreasureLevelsFromProto(input.GetLevels()),
|
||||
GiftEnergyRules: roomTreasureEnergyRulesFromProto(input.GetGiftEnergyRules()),
|
||||
Levels: roomRocketLevelsFromProto(input.GetLevels()),
|
||||
GiftFuelRules: roomRocketFuelRulesFromProto(input.GetGiftFuelRules()),
|
||||
UpdatedByAdminID: input.GetUpdatedByAdminId(),
|
||||
CreatedAtMS: input.GetCreatedAtMs(),
|
||||
UpdatedAtMS: input.GetUpdatedAtMs(),
|
||||
@ -525,6 +536,7 @@ func roomPinFromProto(input *roomv1.AdminRoomPin) RoomPin {
|
||||
return RoomPin{
|
||||
ID: input.GetId(),
|
||||
VisibleRegionID: input.GetVisibleRegionId(),
|
||||
PinType: input.GetPinType(),
|
||||
RoomID: input.GetRoomId(),
|
||||
Weight: input.GetWeight(),
|
||||
Status: input.GetStatus(),
|
||||
@ -547,66 +559,66 @@ func roomPinFromProto(input *roomv1.AdminRoomPin) RoomPin {
|
||||
}
|
||||
}
|
||||
|
||||
func roomTreasureConfigToProto(input RoomTreasureConfig) *roomv1.AdminRoomTreasureConfig {
|
||||
return &roomv1.AdminRoomTreasureConfig{
|
||||
func roomRocketConfigToProto(input RoomRocketConfig) *roomv1.AdminRoomRocketConfig {
|
||||
return &roomv1.AdminRoomRocketConfig{
|
||||
Enabled: input.Enabled,
|
||||
EnergySource: input.EnergySource,
|
||||
OpenDelayMs: input.OpenDelayMS,
|
||||
FuelSource: input.FuelSource,
|
||||
LaunchDelayMs: input.LaunchDelayMS,
|
||||
BroadcastEnabled: input.BroadcastEnabled,
|
||||
BroadcastScope: input.BroadcastScope,
|
||||
BroadcastDelayMs: input.BroadcastDelayMS,
|
||||
RewardStackPolicy: input.RewardStackPolicy,
|
||||
Levels: roomTreasureLevelsToProto(input.Levels),
|
||||
GiftEnergyRules: roomTreasureEnergyRulesToProto(input.GiftEnergyRules),
|
||||
Levels: roomRocketLevelsToProto(input.Levels),
|
||||
GiftFuelRules: roomRocketFuelRulesToProto(input.GiftFuelRules),
|
||||
}
|
||||
}
|
||||
|
||||
func roomTreasureLevelsFromProto(input []*roomv1.RoomTreasureLevel) []RoomTreasureLevelConfig {
|
||||
out := make([]RoomTreasureLevelConfig, 0, len(input))
|
||||
func roomRocketLevelsFromProto(input []*roomv1.RoomRocketLevel) []RoomRocketLevelConfig {
|
||||
out := make([]RoomRocketLevelConfig, 0, len(input))
|
||||
for _, level := range input {
|
||||
if level == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, RoomTreasureLevelConfig{
|
||||
Level: level.GetLevel(),
|
||||
EnergyThreshold: level.GetEnergyThreshold(),
|
||||
CoverURL: level.GetCoverUrl(),
|
||||
AnimationURL: level.GetAnimationUrl(),
|
||||
OpeningAnimationURL: level.GetOpeningAnimationUrl(),
|
||||
OpenedImageURL: level.GetOpenedImageUrl(),
|
||||
InRoomRewards: roomTreasureRewardsFromProto(level.GetInRoomRewards()),
|
||||
Top1Rewards: roomTreasureRewardsFromProto(level.GetTop1Rewards()),
|
||||
IgniterRewards: roomTreasureRewardsFromProto(level.GetIgniterRewards()),
|
||||
out = append(out, RoomRocketLevelConfig{
|
||||
Level: level.GetLevel(),
|
||||
FuelThreshold: level.GetFuelThreshold(),
|
||||
CoverURL: level.GetCoverUrl(),
|
||||
AnimationURL: level.GetAnimationUrl(),
|
||||
LaunchAnimationURL: level.GetLaunchAnimationUrl(),
|
||||
LaunchedImageURL: level.GetLaunchedImageUrl(),
|
||||
InRoomRewards: roomRocketRewardsFromProto(level.GetInRoomRewards()),
|
||||
Top1Rewards: roomRocketRewardsFromProto(level.GetTop1Rewards()),
|
||||
IgniterRewards: roomRocketRewardsFromProto(level.GetIgniterRewards()),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func roomTreasureLevelsToProto(input []RoomTreasureLevelConfig) []*roomv1.RoomTreasureLevel {
|
||||
out := make([]*roomv1.RoomTreasureLevel, 0, len(input))
|
||||
func roomRocketLevelsToProto(input []RoomRocketLevelConfig) []*roomv1.RoomRocketLevel {
|
||||
out := make([]*roomv1.RoomRocketLevel, 0, len(input))
|
||||
for _, level := range input {
|
||||
out = append(out, &roomv1.RoomTreasureLevel{
|
||||
Level: level.Level,
|
||||
EnergyThreshold: level.EnergyThreshold,
|
||||
CoverUrl: level.CoverURL,
|
||||
AnimationUrl: level.AnimationURL,
|
||||
OpeningAnimationUrl: level.OpeningAnimationURL,
|
||||
OpenedImageUrl: level.OpenedImageURL,
|
||||
InRoomRewards: roomTreasureRewardsToProto(level.InRoomRewards),
|
||||
Top1Rewards: roomTreasureRewardsToProto(level.Top1Rewards),
|
||||
IgniterRewards: roomTreasureRewardsToProto(level.IgniterRewards),
|
||||
out = append(out, &roomv1.RoomRocketLevel{
|
||||
Level: level.Level,
|
||||
FuelThreshold: level.FuelThreshold,
|
||||
CoverUrl: level.CoverURL,
|
||||
AnimationUrl: level.AnimationURL,
|
||||
LaunchAnimationUrl: level.LaunchAnimationURL,
|
||||
LaunchedImageUrl: level.LaunchedImageURL,
|
||||
InRoomRewards: roomRocketRewardsToProto(level.InRoomRewards),
|
||||
Top1Rewards: roomRocketRewardsToProto(level.Top1Rewards),
|
||||
IgniterRewards: roomRocketRewardsToProto(level.IgniterRewards),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func roomTreasureRewardsFromProto(input []*roomv1.RoomTreasureRewardItem) []RoomTreasureRewardItem {
|
||||
out := make([]RoomTreasureRewardItem, 0, len(input))
|
||||
func roomRocketRewardsFromProto(input []*roomv1.RoomRocketRewardItem) []RoomRocketRewardItem {
|
||||
out := make([]RoomRocketRewardItem, 0, len(input))
|
||||
for _, reward := range input {
|
||||
if reward == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, RoomTreasureRewardItem{
|
||||
out = append(out, RoomRocketRewardItem{
|
||||
RewardItemID: reward.GetRewardItemId(),
|
||||
ResourceGroupID: reward.GetResourceGroupId(),
|
||||
Weight: reward.GetWeight(),
|
||||
@ -617,10 +629,10 @@ func roomTreasureRewardsFromProto(input []*roomv1.RoomTreasureRewardItem) []Room
|
||||
return out
|
||||
}
|
||||
|
||||
func roomTreasureRewardsToProto(input []RoomTreasureRewardItem) []*roomv1.RoomTreasureRewardItem {
|
||||
out := make([]*roomv1.RoomTreasureRewardItem, 0, len(input))
|
||||
func roomRocketRewardsToProto(input []RoomRocketRewardItem) []*roomv1.RoomRocketRewardItem {
|
||||
out := make([]*roomv1.RoomRocketRewardItem, 0, len(input))
|
||||
for _, reward := range input {
|
||||
out = append(out, &roomv1.RoomTreasureRewardItem{
|
||||
out = append(out, &roomv1.RoomRocketRewardItem{
|
||||
RewardItemId: reward.RewardItemID,
|
||||
ResourceGroupId: reward.ResourceGroupID,
|
||||
Weight: reward.Weight,
|
||||
@ -631,33 +643,33 @@ func roomTreasureRewardsToProto(input []RoomTreasureRewardItem) []*roomv1.RoomTr
|
||||
return out
|
||||
}
|
||||
|
||||
func roomTreasureEnergyRulesFromProto(input []*roomv1.RoomTreasureGiftEnergyRule) []GiftEnergyRuleConfig {
|
||||
out := make([]GiftEnergyRuleConfig, 0, len(input))
|
||||
func roomRocketFuelRulesFromProto(input []*roomv1.RoomRocketGiftFuelRule) []GiftFuelRuleConfig {
|
||||
out := make([]GiftFuelRuleConfig, 0, len(input))
|
||||
for _, rule := range input {
|
||||
if rule == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, GiftEnergyRuleConfig{
|
||||
out = append(out, GiftFuelRuleConfig{
|
||||
RuleID: rule.GetRuleId(),
|
||||
GiftID: rule.GetGiftId(),
|
||||
GiftTypeCode: rule.GetGiftTypeCode(),
|
||||
MultiplierPPM: rule.GetMultiplierPpm(),
|
||||
FixedEnergy: rule.GetFixedEnergy(),
|
||||
FixedFuel: rule.GetFixedFuel(),
|
||||
Excluded: rule.GetExcluded(),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func roomTreasureEnergyRulesToProto(input []GiftEnergyRuleConfig) []*roomv1.RoomTreasureGiftEnergyRule {
|
||||
out := make([]*roomv1.RoomTreasureGiftEnergyRule, 0, len(input))
|
||||
func roomRocketFuelRulesToProto(input []GiftFuelRuleConfig) []*roomv1.RoomRocketGiftFuelRule {
|
||||
out := make([]*roomv1.RoomRocketGiftFuelRule, 0, len(input))
|
||||
for _, rule := range input {
|
||||
out = append(out, &roomv1.RoomTreasureGiftEnergyRule{
|
||||
out = append(out, &roomv1.RoomRocketGiftFuelRule{
|
||||
RuleId: rule.RuleID,
|
||||
GiftId: rule.GiftID,
|
||||
GiftTypeCode: rule.GiftTypeCode,
|
||||
MultiplierPpm: rule.MultiplierPPM,
|
||||
FixedEnergy: rule.FixedEnergy,
|
||||
FixedFuel: rule.FixedFuel,
|
||||
Excluded: rule.Excluded,
|
||||
})
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
|
||||
type Client interface {
|
||||
GetUser(ctx context.Context, req GetUserRequest) (*User, error)
|
||||
AdminChangeUserCountry(ctx context.Context, req AdminChangeUserCountryRequest) (*ChangeUserCountryResult, error)
|
||||
SetUserStatus(ctx context.Context, req SetUserStatusRequest) (*SetUserStatusResult, error)
|
||||
ResolveDisplayUserID(ctx context.Context, req ResolveDisplayUserIDRequest) (*UserIdentity, error)
|
||||
ListCountries(ctx context.Context, req ListCountriesRequest) ([]*Country, error)
|
||||
@ -28,6 +29,7 @@ type Client interface {
|
||||
GetCoinSellerProfile(ctx context.Context, req GetCoinSellerProfileRequest) (*CoinSellerProfile, error)
|
||||
CreateAgency(ctx context.Context, req CreateAgencyRequest) (*CreateAgencyResult, error)
|
||||
CloseAgency(ctx context.Context, req CloseAgencyRequest) (*Agency, error)
|
||||
DeleteAgency(ctx context.Context, req DeleteAgencyRequest) (*Agency, error)
|
||||
SetAgencyJoinEnabled(ctx context.Context, req SetAgencyJoinEnabledRequest) (*Agency, error)
|
||||
}
|
||||
|
||||
@ -47,6 +49,22 @@ type SetUserStatusRequest struct {
|
||||
Reason string
|
||||
}
|
||||
|
||||
type AdminChangeUserCountryRequest struct {
|
||||
RequestID string
|
||||
Caller string
|
||||
TargetUserID int64
|
||||
Country string
|
||||
AdminUserID int64
|
||||
Reason string
|
||||
}
|
||||
|
||||
type ChangeUserCountryResult struct {
|
||||
User *User `json:"user"`
|
||||
OldRegionID int64 `json:"oldRegionId"`
|
||||
NewRegionID int64 `json:"newRegionId"`
|
||||
RegionChanged bool `json:"regionChanged"`
|
||||
}
|
||||
|
||||
type SetUserStatusResult struct {
|
||||
User *User `json:"user"`
|
||||
RevokedSessionCount int64 `json:"revokedSessionCount"`
|
||||
@ -142,6 +160,25 @@ func (c *GRPCClient) GetUser(ctx context.Context, req GetUserRequest) (*User, er
|
||||
return fromProtoUser(resp.GetUser()), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) AdminChangeUserCountry(ctx context.Context, req AdminChangeUserCountryRequest) (*ChangeUserCountryResult, error) {
|
||||
resp, err := c.client.AdminChangeUserCountry(ctx, &userv1.AdminChangeUserCountryRequest{
|
||||
Meta: requestMeta(ctx, req.RequestID, req.Caller),
|
||||
TargetUserId: req.TargetUserID,
|
||||
Country: req.Country,
|
||||
AdminUserId: req.AdminUserID,
|
||||
Reason: req.Reason,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ChangeUserCountryResult{
|
||||
User: fromProtoUser(resp.GetUser()),
|
||||
OldRegionID: resp.GetOldRegionId(),
|
||||
NewRegionID: resp.GetNewRegionId(),
|
||||
RegionChanged: resp.GetRegionChanged(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) SetUserStatus(ctx context.Context, req SetUserStatusRequest) (*SetUserStatusResult, error) {
|
||||
resp, err := c.client.SetUserStatus(ctx, &userv1.SetUserStatusRequest{
|
||||
Meta: requestMeta(ctx, req.RequestID, req.Caller),
|
||||
@ -260,7 +297,6 @@ type CreateBDLeaderRequest struct {
|
||||
Caller string
|
||||
CommandID string
|
||||
AdminUserID int64
|
||||
RegionID int64
|
||||
TargetUserID int64
|
||||
Reason string
|
||||
}
|
||||
@ -319,7 +355,6 @@ type CreateAgencyRequest struct {
|
||||
ParentBDUserID int64
|
||||
Name string
|
||||
JoinEnabled bool
|
||||
MaxHosts int32
|
||||
Reason string
|
||||
}
|
||||
|
||||
@ -332,6 +367,15 @@ type CloseAgencyRequest struct {
|
||||
Reason string
|
||||
}
|
||||
|
||||
type DeleteAgencyRequest struct {
|
||||
RequestID string
|
||||
Caller string
|
||||
CommandID string
|
||||
AdminUserID int64
|
||||
AgencyID int64
|
||||
Reason string
|
||||
}
|
||||
|
||||
type SetAgencyJoinEnabledRequest struct {
|
||||
RequestID string
|
||||
Caller string
|
||||
@ -347,8 +391,9 @@ type BDProfile struct {
|
||||
Role string `json:"role"`
|
||||
RegionID int64 `json:"regionId"`
|
||||
ParentLeaderUserID int64 `json:"parentLeaderUserId,string"`
|
||||
PositionAlias string `json:"positionAlias"`
|
||||
Status string `json:"status"`
|
||||
CreatedByUserID int64 `json:"createdByUserId"`
|
||||
CreatedByUserID int64 `json:"createdByUserId,string"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
DisplayUserID string `json:"displayUserId"`
|
||||
@ -365,7 +410,7 @@ type BDProfile struct {
|
||||
}
|
||||
|
||||
type Agency struct {
|
||||
AgencyID int64 `json:"agencyId"`
|
||||
AgencyID int64 `json:"agencyId,string"`
|
||||
OwnerUserID int64 `json:"ownerUserId,string"`
|
||||
RegionID int64 `json:"regionId"`
|
||||
ParentBDUserID int64 `json:"parentBdUserId,string"`
|
||||
@ -373,7 +418,7 @@ type Agency struct {
|
||||
Status string `json:"status"`
|
||||
JoinEnabled bool `json:"joinEnabled"`
|
||||
MaxHosts int32 `json:"maxHosts"`
|
||||
CreatedByUserID int64 `json:"createdByUserId"`
|
||||
CreatedByUserID int64 `json:"createdByUserId,string"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
OwnerDisplayUserID string `json:"ownerDisplayUserId"`
|
||||
@ -386,34 +431,39 @@ type Agency struct {
|
||||
}
|
||||
|
||||
type HostProfile struct {
|
||||
UserID int64 `json:"userId,string"`
|
||||
Status string `json:"status"`
|
||||
RegionID int64 `json:"regionId"`
|
||||
CurrentAgencyID int64 `json:"currentAgencyId"`
|
||||
CurrentMembershipID int64 `json:"currentMembershipId"`
|
||||
Source string `json:"source"`
|
||||
FirstBecameHostAtMs int64 `json:"firstBecameHostAtMs"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
DisplayUserID string `json:"displayUserId"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
RegionName string `json:"regionName"`
|
||||
CurrentAgencyName string `json:"currentAgencyName"`
|
||||
UserID int64 `json:"userId,string"`
|
||||
Status string `json:"status"`
|
||||
RegionID int64 `json:"regionId"`
|
||||
CurrentAgencyID int64 `json:"currentAgencyId,string"`
|
||||
CurrentMembershipID int64 `json:"currentMembershipId,string"`
|
||||
Source string `json:"source"`
|
||||
FirstBecameHostAtMs int64 `json:"firstBecameHostAtMs"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
DisplayUserID string `json:"displayUserId"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
RegionName string `json:"regionName"`
|
||||
CurrentAgencyName string `json:"currentAgencyName"`
|
||||
Diamond int64 `json:"diamond"`
|
||||
CurrentAgencyOwnerUserID int64 `json:"currentAgencyOwnerUserId,string"`
|
||||
CurrentAgencyOwnerDisplayUserID string `json:"currentAgencyOwnerDisplayUserId"`
|
||||
CurrentAgencyOwnerUsername string `json:"currentAgencyOwnerUsername"`
|
||||
CurrentAgencyOwnerAvatar string `json:"currentAgencyOwnerAvatar"`
|
||||
}
|
||||
|
||||
type CoinSellerProfile struct {
|
||||
UserID int64 `json:"userId,string"`
|
||||
Status string `json:"status"`
|
||||
MerchantAssetType string `json:"merchantAssetType"`
|
||||
CreatedByUserID int64 `json:"createdByUserId"`
|
||||
CreatedByUserID int64 `json:"createdByUserId,string"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
type AgencyMembership struct {
|
||||
MembershipID int64 `json:"membershipId"`
|
||||
AgencyID int64 `json:"agencyId"`
|
||||
MembershipID int64 `json:"membershipId,string"`
|
||||
AgencyID int64 `json:"agencyId,string"`
|
||||
HostUserID int64 `json:"hostUserId,string"`
|
||||
RegionID int64 `json:"regionId"`
|
||||
MembershipType string `json:"membershipType"`
|
||||
@ -437,7 +487,6 @@ func (c *GRPCClient) CreateBDLeader(ctx context.Context, req CreateBDLeaderReque
|
||||
Meta: requestMeta(ctx, req.RequestID, req.Caller),
|
||||
CommandId: req.CommandID,
|
||||
AdminUserId: req.AdminUserID,
|
||||
RegionId: req.RegionID,
|
||||
TargetUserId: req.TargetUserID,
|
||||
Reason: req.Reason,
|
||||
})
|
||||
@ -526,7 +575,6 @@ func (c *GRPCClient) CreateAgency(ctx context.Context, req CreateAgencyRequest)
|
||||
ParentBdUserId: req.ParentBDUserID,
|
||||
Name: req.Name,
|
||||
JoinEnabled: req.JoinEnabled,
|
||||
MaxHosts: req.MaxHosts,
|
||||
Reason: req.Reason,
|
||||
})
|
||||
if err != nil {
|
||||
@ -553,6 +601,20 @@ func (c *GRPCClient) CloseAgency(ctx context.Context, req CloseAgencyRequest) (*
|
||||
return fromProtoAgency(resp.GetAgency()), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) DeleteAgency(ctx context.Context, req DeleteAgencyRequest) (*Agency, error) {
|
||||
resp, err := c.hostAdminClient.DeleteAgency(ctx, &userv1.DeleteAgencyRequest{
|
||||
Meta: requestMeta(ctx, req.RequestID, req.Caller),
|
||||
CommandId: req.CommandID,
|
||||
AdminUserId: req.AdminUserID,
|
||||
AgencyId: req.AgencyID,
|
||||
Reason: req.Reason,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fromProtoAgency(resp.GetAgency()), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) SetAgencyJoinEnabled(ctx context.Context, req SetAgencyJoinEnabledRequest) (*Agency, error) {
|
||||
resp, err := c.hostAdminClient.SetAgencyJoinEnabled(ctx, &userv1.SetAgencyJoinEnabledRequest{
|
||||
Meta: requestMeta(ctx, req.RequestID, req.Caller),
|
||||
|
||||
34
server/admin/internal/integration/userclient/client_test.go
Normal file
34
server/admin/internal/integration/userclient/client_test.go
Normal file
@ -0,0 +1,34 @@
|
||||
package userclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAgencyJSONKeepsSnowflakeIDsAsStrings(t *testing.T) {
|
||||
agency := Agency{
|
||||
AgencyID: 321170072154411009,
|
||||
OwnerUserID: 316033326332776448,
|
||||
ParentBDUserID: 320470609978986496,
|
||||
CreatedByUserID: 320470609978986496,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(agency)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal agency: %v", err)
|
||||
}
|
||||
body := string(payload)
|
||||
|
||||
// 后台前端运行在浏览器里,雪花 ID 超过 JavaScript 安全整数上限;这里必须以字符串返回,避免 321170072154411009 被解析成 321170072154411000 后再提交给删除接口。
|
||||
for _, want := range []string{
|
||||
`"agencyId":"321170072154411009"`,
|
||||
`"ownerUserId":"316033326332776448"`,
|
||||
`"parentBdUserId":"320470609978986496"`,
|
||||
`"createdByUserId":"320470609978986496"`,
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("agency json should contain %s, got %s", want, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -99,7 +99,7 @@ func (h *Handler) UpdateUser(c *gin.Context) {
|
||||
response.BadRequest(c, "用户参数不正确")
|
||||
return
|
||||
}
|
||||
user, err := h.service.UpdateUser(c.Request.Context(), userID, middleware.CurrentRequestID(c), req)
|
||||
user, err := h.service.UpdateUser(c.Request.Context(), userID, int64(middleware.CurrentUserID(c)), middleware.CurrentRequestID(c), req)
|
||||
if err != nil {
|
||||
writeMutationError(c, err, "更新 App 用户失败")
|
||||
return
|
||||
@ -154,11 +154,19 @@ func (h *Handler) setUserStatus(c *gin.Context, status string, action string, su
|
||||
|
||||
func parseListQuery(c *gin.Context) listQuery {
|
||||
options := shared.ListOptions(c)
|
||||
regionID, regionIDSet := queryOptionalInt64(c, "region_id", "regionId")
|
||||
return normalizeListQuery(listQuery{
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
Keyword: options.Keyword,
|
||||
Status: options.Status,
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
Country: firstQuery(c, "country", "country_code", "countryCode"),
|
||||
Keyword: options.Keyword,
|
||||
RegionID: regionID,
|
||||
RegionIDSet: regionIDSet,
|
||||
Status: options.Status,
|
||||
SortBy: firstQuery(c, "sort_by", "sortBy"),
|
||||
SortDirection: firstQuery(c, "sort_direction", "sortDirection", "order"),
|
||||
StartMs: queryInt64(c, "start_ms", "startMs", "start_at_ms", "startAtMs"),
|
||||
EndMs: queryInt64(c, "end_ms", "endMs", "end_at_ms", "endAtMs"),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
package appuser
|
||||
|
||||
type listQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Keyword string
|
||||
Status string
|
||||
Page int
|
||||
PageSize int
|
||||
Country string
|
||||
Keyword string
|
||||
RegionID int64
|
||||
RegionIDSet bool
|
||||
Status string
|
||||
SortBy string
|
||||
SortDirection string
|
||||
StartMs int64
|
||||
EndMs int64
|
||||
}
|
||||
|
||||
type loginLogQuery struct {
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -72,22 +73,17 @@ func (s *Service) ListUsers(ctx context.Context, query listQuery) ([]AppUser, in
|
||||
}
|
||||
query = normalizeListQuery(query)
|
||||
appCode := appctx.FromContext(ctx)
|
||||
whereSQL := "FROM users u WHERE u.app_code = ?"
|
||||
args := []any{appCode}
|
||||
if query.Status != "" {
|
||||
whereSQL += " AND u.status = ?"
|
||||
args = append(args, query.Status)
|
||||
}
|
||||
if query.Keyword != "" {
|
||||
like := "%" + query.Keyword + "%"
|
||||
whereSQL += " AND (CAST(u.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ?)"
|
||||
args = append(args, like, like, like)
|
||||
}
|
||||
whereSQL, args := appUserListWhereSQL(appCode, query)
|
||||
|
||||
total, err := countRows(ctx, s.userDB, whereSQL, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if query.SortBy == "coin" {
|
||||
// 金币余额来自 wallet 库,必须先补齐当前筛选结果的余额再排序分页,避免只按当前页局部重排。
|
||||
items, err := s.listUsersSortedByCoin(ctx, query, whereSQL, args)
|
||||
return items, total, err
|
||||
}
|
||||
rows, err := s.userDB.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT u.user_id,
|
||||
u.current_display_user_id,
|
||||
@ -128,16 +124,85 @@ func (s *Service) ListUsers(ctx context.Context, query listQuery) ([]AppUser, in
|
||||
COALESCE((SELECT MAX(l.created_at_ms) FROM login_audit l WHERE l.app_code = u.app_code AND l.user_id = u.user_id), 0)
|
||||
)
|
||||
%s
|
||||
ORDER BY u.created_at_ms DESC, u.user_id DESC
|
||||
%s
|
||||
LIMIT ? OFFSET ?
|
||||
`, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
||||
`, whereSQL, appUserOrderSQL(query)), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]AppUser, 0, query.PageSize)
|
||||
userIDs := make([]int64, 0, query.PageSize)
|
||||
items, userIDs, err := scanAppUserRows(rows, query.PageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := s.fillBalances(ctx, items, userIDs); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) listUsersSortedByCoin(ctx context.Context, query listQuery, whereSQL string, args []any) ([]AppUser, error) {
|
||||
rows, err := s.userDB.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT u.user_id,
|
||||
u.current_display_user_id,
|
||||
COALESCE(u.username, ''),
|
||||
COALESCE(u.avatar, ''),
|
||||
COALESCE(u.gender, ''),
|
||||
COALESCE(u.country, ''),
|
||||
COALESCE((SELECT c.country_name FROM countries c WHERE c.app_code = u.app_code AND c.country_code = u.country LIMIT 1), ''),
|
||||
COALESCE((SELECT c.country_display_name FROM countries c WHERE c.app_code = u.app_code AND c.country_code = u.country LIMIT 1), ''),
|
||||
CASE
|
||||
WHEN COALESCE(u.region_id, 0) = 0 THEN 0
|
||||
WHEN EXISTS (
|
||||
SELECT 1 FROM regions rg
|
||||
WHERE rg.app_code = u.app_code
|
||||
AND rg.region_id = u.region_id
|
||||
AND rg.status = 'active'
|
||||
AND rg.region_code NOT IN ('Australia and New Zealand', 'Caribbean', 'Melanesia', 'Micronesia', 'Polynesia', 'Southern Africa', 'UNSPECIFIED')
|
||||
LIMIT 1
|
||||
) THEN u.region_id
|
||||
ELSE 0
|
||||
END,
|
||||
CASE
|
||||
WHEN COALESCE(u.region_id, 0) = 0 OR NOT EXISTS (
|
||||
SELECT 1 FROM regions rg
|
||||
WHERE rg.app_code = u.app_code
|
||||
AND rg.region_id = u.region_id
|
||||
AND rg.status = 'active'
|
||||
AND rg.region_code NOT IN ('Australia and New Zealand', 'Caribbean', 'Melanesia', 'Micronesia', 'Polynesia', 'Southern Africa', 'UNSPECIFIED')
|
||||
LIMIT 1
|
||||
) THEN 'GLOBAL'
|
||||
ELSE COALESCE((SELECT rg.name FROM regions rg WHERE rg.app_code = u.app_code AND rg.region_id = u.region_id LIMIT 1), '')
|
||||
END,
|
||||
u.status,
|
||||
u.created_at_ms,
|
||||
u.updated_at_ms,
|
||||
GREATEST(
|
||||
COALESCE((SELECT MAX(s.updated_at_ms) FROM auth_sessions s WHERE s.app_code = u.app_code AND s.user_id = u.user_id), 0),
|
||||
COALESCE((SELECT MAX(l.created_at_ms) FROM login_audit l WHERE l.app_code = u.app_code AND l.user_id = u.user_id), 0)
|
||||
)
|
||||
%s
|
||||
`, whereSQL), args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items, userIDs, err := scanAppUserRows(rows, query.PageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.fillBalances(ctx, items, userIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sortAppUsersByCoin(items, query.SortDirection)
|
||||
return paginateAppUsers(items, query.Page, query.PageSize), nil
|
||||
}
|
||||
|
||||
func scanAppUserRows(rows *sql.Rows, capacity int) ([]AppUser, []int64, error) {
|
||||
items := make([]AppUser, 0, capacity)
|
||||
userIDs := make([]int64, 0, capacity)
|
||||
for rows.Next() {
|
||||
var item AppUser
|
||||
var userID int64
|
||||
@ -157,19 +222,16 @@ func (s *Service) ListUsers(ctx context.Context, query listQuery) ([]AppUser, in
|
||||
&item.UpdatedAtMs,
|
||||
&item.LastActiveAtMs,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
return nil, nil, err
|
||||
}
|
||||
item.UserID = strconv.FormatInt(userID, 10)
|
||||
items = append(items, item)
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := s.fillBalances(ctx, items, userIDs); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
return items, userIDs, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetUser(ctx context.Context, userID int64) (AppUser, error) {
|
||||
@ -250,11 +312,11 @@ func (s *Service) GetUser(ctx context.Context, userID int64) (AppUser, error) {
|
||||
return items[0], nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateUser(ctx context.Context, userID int64, requestID string, req updateUserRequest) (AppUser, error) {
|
||||
func (s *Service) UpdateUser(ctx context.Context, userID int64, adminUserID int64, requestID string, req updateUserRequest) (AppUser, error) {
|
||||
if s.userDB == nil {
|
||||
return AppUser{}, fmt.Errorf("user mysql is not configured")
|
||||
}
|
||||
sets := make([]string, 0, 6)
|
||||
sets := make([]string, 0, 4)
|
||||
args := make([]any, 0, 8)
|
||||
if req.Username != nil {
|
||||
value := strings.TrimSpace(*req.Username)
|
||||
@ -280,47 +342,48 @@ func (s *Service) UpdateUser(ctx context.Context, userID int64, requestID string
|
||||
sets = append(sets, "gender = ?")
|
||||
args = append(args, nullableString(value))
|
||||
}
|
||||
oldRegionID := int64(0)
|
||||
if req.Country != nil {
|
||||
before, err := s.GetUser(ctx, userID)
|
||||
|
||||
if len(sets) > 0 {
|
||||
sets = append(sets, "updated_at_ms = ?")
|
||||
appCode := appctx.FromContext(ctx)
|
||||
args = append(args, nowMillis())
|
||||
args = append(args, appCode, userID)
|
||||
result, err := s.userDB.ExecContext(ctx, "UPDATE users SET "+strings.Join(sets, ", ")+" WHERE app_code = ? AND user_id = ?", args...)
|
||||
if err != nil {
|
||||
return AppUser{}, err
|
||||
}
|
||||
oldRegionID = before.RegionID
|
||||
country, regionID, err := s.resolveCountryRegion(ctx, *req.Country)
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return AppUser{}, err
|
||||
}
|
||||
sets = append(sets, "country = ?", "region_id = ?")
|
||||
args = append(args, nullableString(country), nullableInt64(regionID))
|
||||
}
|
||||
if len(sets) == 0 {
|
||||
return s.GetUser(ctx, userID)
|
||||
if affected == 0 {
|
||||
return AppUser{}, ErrNotFound
|
||||
}
|
||||
}
|
||||
|
||||
sets = append(sets, "updated_at_ms = ?")
|
||||
appCode := appctx.FromContext(ctx)
|
||||
args = append(args, nowMillis())
|
||||
args = append(args, appCode, userID)
|
||||
result, err := s.userDB.ExecContext(ctx, "UPDATE users SET "+strings.Join(sets, ", ")+" WHERE app_code = ? AND user_id = ?", args...)
|
||||
if err != nil {
|
||||
return AppUser{}, err
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return AppUser{}, err
|
||||
}
|
||||
if affected == 0 {
|
||||
return AppUser{}, ErrNotFound
|
||||
}
|
||||
updated, err := s.GetUser(ctx, userID)
|
||||
if err != nil {
|
||||
return AppUser{}, err
|
||||
}
|
||||
if req.Country != nil {
|
||||
s.removeOldRegionBroadcastMemberBestEffort(ctx, userID, oldRegionID, updated.RegionID, strings.TrimSpace(requestID), "admin_user_country_changed")
|
||||
if s.userClient == nil {
|
||||
return AppUser{}, fmt.Errorf("user client is not configured")
|
||||
}
|
||||
change, err := s.userClient.AdminChangeUserCountry(ctx, userclient.AdminChangeUserCountryRequest{
|
||||
RequestID: strings.TrimSpace(requestID),
|
||||
Caller: "hyapp-admin-server",
|
||||
TargetUserID: userID,
|
||||
Country: *req.Country,
|
||||
AdminUserID: adminUserID,
|
||||
Reason: "admin_user_country_changed",
|
||||
})
|
||||
if err != nil {
|
||||
return AppUser{}, err
|
||||
}
|
||||
if change != nil && change.RegionChanged {
|
||||
s.removeOldRegionBroadcastMemberBestEffort(ctx, userID, change.OldRegionID, change.NewRegionID, strings.TrimSpace(requestID), "admin_user_country_changed")
|
||||
}
|
||||
}
|
||||
return updated, nil
|
||||
if len(sets) == 0 && req.Country == nil {
|
||||
return s.GetUser(ctx, userID)
|
||||
}
|
||||
return s.GetUser(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *Service) removeOldRegionBroadcastMemberBestEffort(ctx context.Context, userID int64, oldRegionID int64, newRegionID int64, requestID string, reason string) {
|
||||
@ -419,87 +482,64 @@ func (s *Service) SetPassword(ctx context.Context, userID int64, password string
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) resolveCountryRegion(ctx context.Context, input string) (string, int64, error) {
|
||||
country := normalizeCountryCode(input)
|
||||
if country == "" {
|
||||
return "", 0, fmt.Errorf("%w: 请选择国家", ErrInvalidArgument)
|
||||
}
|
||||
if !validCountryCode(country) {
|
||||
return "", 0, fmt.Errorf("%w: 国家码必须是 2 到 3 位大写字母", ErrInvalidArgument)
|
||||
}
|
||||
|
||||
appCode := appctx.FromContext(ctx)
|
||||
var canonical string
|
||||
var regionID int64
|
||||
err := s.userDB.QueryRowContext(ctx, `
|
||||
SELECT c.country_code,
|
||||
COALESCE((
|
||||
SELECT rg.region_id
|
||||
FROM region_countries rc
|
||||
INNER JOIN regions rg ON rg.region_id = rc.region_id
|
||||
WHERE rc.app_code = c.app_code
|
||||
AND rg.app_code = c.app_code
|
||||
AND rc.country_code = c.country_code
|
||||
AND rc.status = 'active'
|
||||
AND rg.status = 'active'
|
||||
AND rg.region_code NOT IN ('Australia and New Zealand', 'Caribbean', 'Melanesia', 'Micronesia', 'Polynesia', 'Southern Africa', 'UNSPECIFIED')
|
||||
ORDER BY rg.sort_order ASC, rg.region_id ASC
|
||||
LIMIT 1
|
||||
), 0)
|
||||
FROM countries c
|
||||
WHERE c.app_code = ? AND c.country_code = ? AND c.enabled = TRUE
|
||||
`, appCode, country).Scan(&canonical, ®ionID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", 0, fmt.Errorf("%w: 国家不存在或已停用", ErrInvalidArgument)
|
||||
}
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return canonical, regionID, nil
|
||||
}
|
||||
|
||||
func (s *Service) fillBalances(ctx context.Context, items []AppUser, userIDs []int64) error {
|
||||
if s.walletDB == nil || len(userIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
placeholders := strings.TrimRight(strings.Repeat("?,", len(userIDs)), ",")
|
||||
args := make([]any, 0, len(userIDs)+1)
|
||||
args = append(args, appctx.FromContext(ctx))
|
||||
for _, id := range userIDs {
|
||||
args = append(args, id)
|
||||
}
|
||||
args = append(args, "COIN")
|
||||
rows, err := s.walletDB.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT user_id, asset_type, available_amount
|
||||
FROM wallet_accounts
|
||||
WHERE app_code = ? AND user_id IN (%s) AND asset_type = ?
|
||||
`, placeholders), args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
index := make(map[int64]int, len(userIDs))
|
||||
for i, id := range userIDs {
|
||||
index[id] = i
|
||||
}
|
||||
for rows.Next() {
|
||||
var userID int64
|
||||
var assetType string
|
||||
var amount int64
|
||||
if err := rows.Scan(&userID, &assetType, &amount); err != nil {
|
||||
appCode := appctx.FromContext(ctx)
|
||||
const chunkSize = 500
|
||||
for start := 0; start < len(userIDs); start += chunkSize {
|
||||
end := start + chunkSize
|
||||
if end > len(userIDs) {
|
||||
end = len(userIDs)
|
||||
}
|
||||
// 用户列表金币排序会对完整筛选集补余额,按块查询可以避免 IN 参数过长导致后台列表失败。
|
||||
chunk := userIDs[start:end]
|
||||
placeholders := strings.TrimRight(strings.Repeat("?,", len(chunk)), ",")
|
||||
args := make([]any, 0, len(chunk)+2)
|
||||
args = append(args, appCode)
|
||||
for _, id := range chunk {
|
||||
args = append(args, id)
|
||||
}
|
||||
args = append(args, "COIN")
|
||||
rows, err := s.walletDB.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT user_id, asset_type, available_amount
|
||||
FROM wallet_accounts
|
||||
WHERE app_code = ? AND user_id IN (%s) AND asset_type = ?
|
||||
`, placeholders), args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i, ok := index[userID]
|
||||
if !ok {
|
||||
continue
|
||||
for rows.Next() {
|
||||
var userID int64
|
||||
var assetType string
|
||||
var amount int64
|
||||
if err := rows.Scan(&userID, &assetType, &amount); err != nil {
|
||||
_ = rows.Close()
|
||||
return err
|
||||
}
|
||||
i, ok := index[userID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch strings.ToUpper(assetType) {
|
||||
case "COIN":
|
||||
items[i].Coin = amount
|
||||
}
|
||||
}
|
||||
switch strings.ToUpper(assetType) {
|
||||
case "COIN":
|
||||
items[i].Coin = amount
|
||||
if err := rows.Err(); err != nil {
|
||||
_ = rows.Close()
|
||||
return err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return rows.Err()
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeListQuery(query listQuery) listQuery {
|
||||
@ -512,11 +552,132 @@ func normalizeListQuery(query listQuery) listQuery {
|
||||
if query.PageSize > 100 {
|
||||
query.PageSize = 100
|
||||
}
|
||||
query.Country = normalizeCountryCode(query.Country)
|
||||
query.Keyword = strings.TrimSpace(query.Keyword)
|
||||
if query.RegionID < 0 {
|
||||
query.RegionID = 0
|
||||
query.RegionIDSet = false
|
||||
}
|
||||
if query.StartMs < 0 {
|
||||
query.StartMs = 0
|
||||
}
|
||||
if query.EndMs < 0 {
|
||||
query.EndMs = 0
|
||||
}
|
||||
query.Status = strings.ToLower(strings.TrimSpace(query.Status))
|
||||
query.SortBy = normalizeAppUserSortBy(query.SortBy)
|
||||
query.SortDirection = normalizeSortDirection(query.SortDirection)
|
||||
return query
|
||||
}
|
||||
|
||||
func appUserListWhereSQL(appCode string, query listQuery) (string, []any) {
|
||||
whereSQL := "FROM users u WHERE u.app_code = ?"
|
||||
args := []any{appCode}
|
||||
if query.Status != "" {
|
||||
whereSQL += " AND u.status = ?"
|
||||
args = append(args, query.Status)
|
||||
}
|
||||
if query.Keyword != "" {
|
||||
like := "%" + query.Keyword + "%"
|
||||
whereSQL += " AND (CAST(u.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ?)"
|
||||
args = append(args, like, like, like)
|
||||
}
|
||||
if query.Country != "" {
|
||||
// 国家筛选用 users.country 的 ISO code 精确命中;前端传展示名时不会被猜测转换,避免误筛到错误国家。
|
||||
whereSQL += " AND UPPER(u.country) = ?"
|
||||
args = append(args, query.Country)
|
||||
}
|
||||
if query.RegionIDSet {
|
||||
if query.RegionID == 0 {
|
||||
// 列表展示里无区域或无有效业务区域都会显示为 GLOBAL;筛选 GLOBAL 时保持同一套展示口径。
|
||||
whereSQL += " AND (COALESCE(u.region_id, 0) = 0 OR NOT " + appUserValidRegionExistsSQL("u") + ")"
|
||||
} else {
|
||||
whereSQL += " AND u.region_id = ?"
|
||||
args = append(args, query.RegionID)
|
||||
}
|
||||
}
|
||||
if query.StartMs > 0 {
|
||||
// 时间区间筛选的是用户创建时间,和列表“创建 / 活跃”列的第一行保持一致。
|
||||
whereSQL += " AND u.created_at_ms >= ?"
|
||||
args = append(args, query.StartMs)
|
||||
}
|
||||
if query.EndMs > 0 {
|
||||
whereSQL += " AND u.created_at_ms < ?"
|
||||
args = append(args, query.EndMs)
|
||||
}
|
||||
return whereSQL, args
|
||||
}
|
||||
|
||||
func appUserValidRegionExistsSQL(userAlias string) string {
|
||||
return fmt.Sprintf(`EXISTS (
|
||||
SELECT 1 FROM regions rg
|
||||
WHERE rg.app_code = %[1]s.app_code
|
||||
AND rg.region_id = %[1]s.region_id
|
||||
AND rg.status = 'active'
|
||||
AND rg.region_code NOT IN ('Australia and New Zealand', 'Caribbean', 'Melanesia', 'Micronesia', 'Polynesia', 'Southern Africa', 'UNSPECIFIED')
|
||||
LIMIT 1
|
||||
)`, userAlias)
|
||||
}
|
||||
|
||||
func normalizeAppUserSortBy(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "coin", "coins":
|
||||
return "coin"
|
||||
case "created_at", "createdat", "created_at_ms", "createdatms", "created":
|
||||
return "created_at"
|
||||
default:
|
||||
return "created_at"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeSortDirection(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "asc":
|
||||
return "asc"
|
||||
default:
|
||||
return "desc"
|
||||
}
|
||||
}
|
||||
|
||||
func appUserOrderSQL(query listQuery) string {
|
||||
if query.SortBy == "created_at" && query.SortDirection == "asc" {
|
||||
return "ORDER BY u.created_at_ms ASC, u.user_id ASC"
|
||||
}
|
||||
return "ORDER BY u.created_at_ms DESC, u.user_id DESC"
|
||||
}
|
||||
|
||||
func sortAppUsersByCoin(items []AppUser, direction string) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
left := items[i]
|
||||
right := items[j]
|
||||
if left.Coin != right.Coin {
|
||||
if direction == "asc" {
|
||||
return left.Coin < right.Coin
|
||||
}
|
||||
return left.Coin > right.Coin
|
||||
}
|
||||
// 金币相同的时候用内部用户 ID 做稳定排序,避免同余额用户在翻页和刷新时抖动。
|
||||
leftID, _ := strconv.ParseInt(left.UserID, 10, 64)
|
||||
rightID, _ := strconv.ParseInt(right.UserID, 10, 64)
|
||||
if direction == "asc" {
|
||||
return leftID < rightID
|
||||
}
|
||||
return leftID > rightID
|
||||
})
|
||||
}
|
||||
|
||||
func paginateAppUsers(items []AppUser, page int, pageSize int) []AppUser {
|
||||
start := offset(page, pageSize)
|
||||
if start >= len(items) {
|
||||
return []AppUser{}
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
return items[start:end]
|
||||
}
|
||||
|
||||
func countRows(ctx context.Context, db *sql.DB, whereSQL string, args ...any) (int64, error) {
|
||||
var total int64
|
||||
err := db.QueryRowContext(ctx, "SELECT COUNT(*) "+whereSQL, args...).Scan(&total)
|
||||
@ -531,26 +692,10 @@ func nullableString(value string) sql.Null[string] {
|
||||
return sql.Null[string]{V: value, Valid: value != ""}
|
||||
}
|
||||
|
||||
func nullableInt64(value int64) sql.Null[int64] {
|
||||
return sql.Null[int64]{V: value, Valid: value > 0}
|
||||
}
|
||||
|
||||
func normalizeCountryCode(value string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func validCountryCode(value string) bool {
|
||||
if len(value) < 2 || len(value) > 3 {
|
||||
return false
|
||||
}
|
||||
for _, char := range value {
|
||||
if char < 'A' || char > 'Z' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func nowMillis() int64 {
|
||||
return time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
88
server/admin/internal/modules/appuser/service_test.go
Normal file
88
server/admin/internal/modules/appuser/service_test.go
Normal file
@ -0,0 +1,88 @@
|
||||
package appuser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeListQuerySort(t *testing.T) {
|
||||
query := normalizeListQuery(listQuery{Page: 0, PageSize: 1000, SortBy: "coins", SortDirection: "asc"})
|
||||
if query.Page != 1 || query.PageSize != 100 || query.SortBy != "coin" || query.SortDirection != "asc" {
|
||||
t.Fatalf("coin sort query mismatch: %+v", query)
|
||||
}
|
||||
|
||||
query = normalizeListQuery(listQuery{SortBy: "createdAtMs", SortDirection: "unknown"})
|
||||
if query.SortBy != "created_at" || query.SortDirection != "desc" {
|
||||
t.Fatalf("created sort default mismatch: %+v", query)
|
||||
}
|
||||
|
||||
query = normalizeListQuery(listQuery{Country: " ph ", RegionID: -3, RegionIDSet: true, StartMs: -1, EndMs: -2})
|
||||
if query.Country != "PH" || query.RegionIDSet || query.StartMs != 0 || query.EndMs != 0 {
|
||||
t.Fatalf("filter normalization mismatch: %+v", query)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppUserListWhereSQLFilters(t *testing.T) {
|
||||
whereSQL, args := appUserListWhereSQL("lalu", normalizeListQuery(listQuery{
|
||||
Country: "ph",
|
||||
Keyword: "hunter",
|
||||
RegionID: 3,
|
||||
RegionIDSet: true,
|
||||
StartMs: 1000,
|
||||
EndMs: 2000,
|
||||
Status: "active",
|
||||
}))
|
||||
for _, want := range []string{
|
||||
"u.status = ?",
|
||||
"CAST(u.user_id AS CHAR) LIKE ?",
|
||||
"UPPER(u.country) = ?",
|
||||
"u.region_id = ?",
|
||||
"u.created_at_ms >= ?",
|
||||
"u.created_at_ms < ?",
|
||||
} {
|
||||
if !strings.Contains(whereSQL, want) {
|
||||
t.Fatalf("where sql missing %q: %s", want, whereSQL)
|
||||
}
|
||||
}
|
||||
if len(args) != 9 || args[0] != "lalu" || args[5] != "PH" || args[6] != int64(3) || args[7] != int64(1000) || args[8] != int64(2000) {
|
||||
t.Fatalf("where args mismatch: %+v", args)
|
||||
}
|
||||
|
||||
whereSQL, args = appUserListWhereSQL("lalu", normalizeListQuery(listQuery{RegionID: 0, RegionIDSet: true}))
|
||||
if !strings.Contains(whereSQL, "COALESCE(u.region_id, 0) = 0") || !strings.Contains(whereSQL, "NOT EXISTS") {
|
||||
t.Fatalf("global region sql mismatch: %s", whereSQL)
|
||||
}
|
||||
if len(args) != 1 || args[0] != "lalu" {
|
||||
t.Fatalf("global region args mismatch: %+v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppUserOrderSQL(t *testing.T) {
|
||||
asc := appUserOrderSQL(listQuery{SortBy: "created_at", SortDirection: "asc"})
|
||||
if asc != "ORDER BY u.created_at_ms ASC, u.user_id ASC" {
|
||||
t.Fatalf("created asc order mismatch: %s", asc)
|
||||
}
|
||||
|
||||
desc := appUserOrderSQL(listQuery{SortBy: "created_at", SortDirection: "desc"})
|
||||
if desc != "ORDER BY u.created_at_ms DESC, u.user_id DESC" {
|
||||
t.Fatalf("created desc order mismatch: %s", desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortAppUsersByCoin(t *testing.T) {
|
||||
items := []AppUser{
|
||||
{UserID: "1001", Coin: 10},
|
||||
{UserID: "1003", Coin: 10},
|
||||
{UserID: "1002", Coin: 80},
|
||||
}
|
||||
|
||||
sortAppUsersByCoin(items, "desc")
|
||||
if got := []string{items[0].UserID, items[1].UserID, items[2].UserID}; got[0] != "1002" || got[1] != "1003" || got[2] != "1001" {
|
||||
t.Fatalf("coin desc order mismatch: %+v", got)
|
||||
}
|
||||
|
||||
sortAppUsersByCoin(items, "asc")
|
||||
if got := []string{items[0].UserID, items[1].UserID, items[2].UserID}; got[0] != "1001" || got[1] != "1003" || got[2] != "1002" {
|
||||
t.Fatalf("coin asc order mismatch: %+v", got)
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,12 @@ type coinLedgerUserDTO struct {
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
type CSVExport struct {
|
||||
FileName string
|
||||
Content []byte
|
||||
Count int
|
||||
}
|
||||
|
||||
type coinLedgerEntryDTO struct {
|
||||
EntryID int64 `json:"entryId"`
|
||||
TransactionID string `json:"transactionId"`
|
||||
@ -25,6 +31,25 @@ type coinLedgerEntryDTO struct {
|
||||
CreatedAtMS int64 `json:"createdAtMs"`
|
||||
}
|
||||
|
||||
type coinSellerLedgerDTO struct {
|
||||
EntryID int64 `json:"entryId"`
|
||||
TransactionID string `json:"transactionId"`
|
||||
CommandID string `json:"commandId"`
|
||||
LedgerType string `json:"ledgerType"`
|
||||
BizType string `json:"bizType"`
|
||||
Seller coinLedgerUserDTO `json:"seller"`
|
||||
Receiver coinLedgerUserDTO `json:"receiver"`
|
||||
Amount int64 `json:"amount"`
|
||||
Direction string `json:"direction"`
|
||||
AvailableDelta int64 `json:"availableDelta"`
|
||||
SellerBalanceAfter int64 `json:"sellerBalanceAfter"`
|
||||
CounterpartyUserID string `json:"counterpartyUserId"`
|
||||
OperatorUserID string `json:"operatorUserId"`
|
||||
Operator coinAdjustmentOperatorDTO `json:"operator"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
CreatedAtMS int64 `json:"createdAtMs"`
|
||||
}
|
||||
|
||||
type coinAdjustmentOperatorDTO struct {
|
||||
AdminID string `json:"adminId"`
|
||||
Username string `json:"username"`
|
||||
|
||||
@ -38,6 +38,45 @@ func (h *Handler) ListCoinLedger(c *gin.Context) {
|
||||
response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
|
||||
}
|
||||
|
||||
func (h *Handler) ListCoinSellerLedger(c *gin.Context) {
|
||||
query, ok := parseCoinSellerLedgerQuery(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, total, err := h.service.ListCoinSellerLedger(c.Request.Context(), appctx.FromContext(c.Request.Context()), query)
|
||||
if err != nil {
|
||||
if errors.Is(err, errInvalidCoinSellerLedgerType) {
|
||||
response.BadRequest(c, "流水类型不正确")
|
||||
return
|
||||
}
|
||||
response.ServerError(c, "获取币商流水失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
|
||||
}
|
||||
|
||||
func (h *Handler) ExportCoinSellerLedger(c *gin.Context) {
|
||||
query, ok := parseCoinSellerLedgerQuery(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
export, err := h.service.ExportCoinSellerLedger(c.Request.Context(), appctx.FromContext(c.Request.Context()), query)
|
||||
if err != nil {
|
||||
if errors.Is(err, errInvalidCoinSellerLedgerType) {
|
||||
response.BadRequest(c, "流水类型不正确")
|
||||
return
|
||||
}
|
||||
response.ServerError(c, "导出币商流水失败")
|
||||
return
|
||||
}
|
||||
|
||||
shared.OperationLog(c, h.audit, "export-coin-seller-ledger", "wallet_entries", "success", fmt.Sprintf("%d entries", export.Count))
|
||||
c.Header("Content-Disposition", "attachment; filename="+export.FileName)
|
||||
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||
c.Writer.WriteHeader(200)
|
||||
_, _ = c.Writer.Write(export.Content)
|
||||
}
|
||||
|
||||
func (h *Handler) ListCoinAdjustments(c *gin.Context) {
|
||||
query, ok := parseListQuery(c)
|
||||
if !ok {
|
||||
@ -115,6 +154,48 @@ func parseListQuery(c *gin.Context) (listQuery, bool) {
|
||||
}), true
|
||||
}
|
||||
|
||||
func parseCoinSellerLedgerQuery(c *gin.Context) (coinSellerLedgerQuery, bool) {
|
||||
options := shared.ListOptions(c)
|
||||
// 行内抽屉传 seller_user_id 做精确锁定,二级页面不传该字段时才走 seller_keyword 的币商本人搜索;
|
||||
// 两个入口共用同一个查询结构,可以保证页面筛选和行内抽屉使用完全一致的流水类型、时间边界和分页语义。
|
||||
sellerUserID, ok := optionalInt64(c, "seller_user_id", "sellerUserId")
|
||||
if !ok {
|
||||
response.BadRequest(c, "币商 ID 不正确")
|
||||
return coinSellerLedgerQuery{}, false
|
||||
}
|
||||
startAtMS, ok := optionalInt64(c, "start_at_ms", "startAtMs")
|
||||
if !ok {
|
||||
response.BadRequest(c, "开始时间不正确")
|
||||
return coinSellerLedgerQuery{}, false
|
||||
}
|
||||
endAtMS, ok := optionalInt64(c, "end_at_ms", "endAtMs")
|
||||
if !ok {
|
||||
response.BadRequest(c, "结束时间不正确")
|
||||
return coinSellerLedgerQuery{}, false
|
||||
}
|
||||
if startAtMS > 0 && endAtMS > 0 && startAtMS >= endAtMS {
|
||||
response.BadRequest(c, "时间区间不正确")
|
||||
return coinSellerLedgerQuery{}, false
|
||||
}
|
||||
|
||||
// 列表时间统一保持 [start_at_ms, end_at_ms),开始毫秒包含、结束毫秒不包含,避免相邻筛选区间重复展示同一条分录;
|
||||
// 非法流水类型在入口层直接拒绝,避免 SQL 拼出无意义类型条件,也让调用方尽早拿到明确的 400 错误。
|
||||
query := normalizeCoinSellerLedgerQuery(coinSellerLedgerQuery{
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
SellerUserID: sellerUserID,
|
||||
SellerKeyword: firstQuery(c, "seller_keyword", "sellerKeyword", "keyword"),
|
||||
LedgerType: firstQuery(c, "ledger_type", "ledgerType"),
|
||||
StartAtMS: startAtMS,
|
||||
EndAtMS: endAtMS,
|
||||
})
|
||||
if _, err := coinSellerLedgerBizTypes(query.LedgerType); err != nil {
|
||||
response.BadRequest(c, "流水类型不正确")
|
||||
return coinSellerLedgerQuery{}, false
|
||||
}
|
||||
return query, true
|
||||
}
|
||||
|
||||
func optionalInt64(c *gin.Context, keys ...string) (int64, bool) {
|
||||
value := strings.TrimSpace(firstQuery(c, keys...))
|
||||
if value == "" {
|
||||
|
||||
@ -8,6 +8,16 @@ type listQuery struct {
|
||||
EndAtMS int64
|
||||
}
|
||||
|
||||
type coinSellerLedgerQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
SellerUserID int64
|
||||
SellerKeyword string
|
||||
LedgerType string
|
||||
StartAtMS int64
|
||||
EndAtMS int64
|
||||
}
|
||||
|
||||
type coinAdjustmentRequest struct {
|
||||
CommandID string `json:"commandId"`
|
||||
TargetUserID any `json:"targetUserId"`
|
||||
|
||||
@ -12,6 +12,8 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
}
|
||||
|
||||
protected.GET("/admin/operations/coin-ledger", middleware.RequirePermission("coin-ledger:view"), h.ListCoinLedger)
|
||||
protected.GET("/admin/operations/coin-seller-ledger", middleware.RequirePermission("coin-seller-ledger:view"), h.ListCoinSellerLedger)
|
||||
protected.GET("/admin/operations/coin-seller-ledger/export", middleware.RequirePermission("coin-seller-ledger:view"), h.ExportCoinSellerLedger)
|
||||
protected.GET("/admin/operations/coin-adjustments", middleware.RequirePermission("coin-adjustment:view"), h.ListCoinAdjustments)
|
||||
protected.GET("/admin/operations/coin-adjustments/target", middleware.RequirePermission("coin-adjustment:create"), h.LookupCoinAdjustmentTarget)
|
||||
protected.POST("/admin/operations/coin-adjustments", middleware.RequirePermission("coin-adjustment:create"), h.CreateCoinAdjustment)
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
package coinledger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -15,13 +17,24 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
coinAssetType = "COIN"
|
||||
coinManualCreditBizType = "manual_credit"
|
||||
directionIn = "income"
|
||||
directionOut = "expense"
|
||||
coinAssetType = "COIN"
|
||||
coinSellerAssetType = "COIN_SELLER_COIN"
|
||||
coinManualCreditBizType = "manual_credit"
|
||||
coinSellerTransferBizType = "coin_seller_transfer"
|
||||
coinSellerStockPurchaseBizType = "coin_seller_stock_purchase"
|
||||
coinSellerCoinCompensationBizType = "coin_seller_coin_compensation"
|
||||
salaryTransferToCoinSellerBizType = "salary_transfer_to_coin_seller"
|
||||
coinSellerLedgerTypeAdminStockCredit = "admin_stock_credit"
|
||||
coinSellerLedgerTypeSellerTransfer = "seller_transfer"
|
||||
coinSellerLedgerTypeSalaryTransferIncome = "salary_transfer_received"
|
||||
directionIn = "income"
|
||||
directionOut = "expense"
|
||||
)
|
||||
|
||||
var errCoinAdjustmentTargetNotFound = errors.New("coin adjustment target user not found")
|
||||
var (
|
||||
errCoinAdjustmentTargetNotFound = errors.New("coin adjustment target user not found")
|
||||
errInvalidCoinSellerLedgerType = errors.New("coin seller ledger type is invalid")
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
userDB *sql.DB
|
||||
@ -142,6 +155,177 @@ func (s *Service) ListCoinLedger(ctx context.Context, appCode string, query list
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListCoinSellerLedger(ctx context.Context, appCode string, query coinSellerLedgerQuery) ([]coinSellerLedgerDTO, int64, error) {
|
||||
return s.listCoinSellerLedger(ctx, appCode, query, true)
|
||||
}
|
||||
|
||||
func (s *Service) ExportCoinSellerLedger(ctx context.Context, appCode string, query coinSellerLedgerQuery) (CSVExport, error) {
|
||||
items, total, err := s.listCoinSellerLedger(ctx, appCode, query, false)
|
||||
if err != nil {
|
||||
return CSVExport{}, err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
writer := csv.NewWriter(&buf)
|
||||
_ = writer.Write([]string{"币商名称", "币商短ID", "币商长ID", "流水类型", "转账金额", "收款人名称", "收款人短ID", "收款人长ID", "操作人", "操作人ID", "币商余额", "转账时间", "交易ID", "命令ID"})
|
||||
for _, item := range items {
|
||||
operatorName, operatorID := coinSellerLedgerOperatorExportFields(item)
|
||||
_ = writer.Write([]string{
|
||||
item.Seller.Username,
|
||||
item.Seller.DisplayUserID,
|
||||
item.Seller.UserID,
|
||||
coinSellerLedgerLabel(item),
|
||||
strconv.FormatInt(signedCoinSellerLedgerAmount(item), 10),
|
||||
item.Receiver.Username,
|
||||
item.Receiver.DisplayUserID,
|
||||
item.Receiver.UserID,
|
||||
operatorName,
|
||||
operatorID,
|
||||
strconv.FormatInt(item.SellerBalanceAfter, 10),
|
||||
time.UnixMilli(item.CreatedAtMS).UTC().Format(time.RFC3339),
|
||||
item.TransactionID,
|
||||
item.CommandID,
|
||||
})
|
||||
}
|
||||
writer.Flush()
|
||||
return CSVExport{FileName: "hyapp-coin-seller-ledger.csv", Content: buf.Bytes(), Count: int(total)}, writer.Error()
|
||||
}
|
||||
|
||||
func (s *Service) listCoinSellerLedger(ctx context.Context, appCode string, query coinSellerLedgerQuery, paginated bool) ([]coinSellerLedgerDTO, int64, error) {
|
||||
query = normalizeCoinSellerLedgerQuery(query)
|
||||
if s == nil || s.walletDB == nil {
|
||||
return nil, 0, fmt.Errorf("wallet mysql is not configured")
|
||||
}
|
||||
|
||||
// 先把前端筛选解析成币商 user_id 集合,再进入 wallet_entries 查询;
|
||||
// 这样 seller_keyword 只影响币商本人范围,不会因为收款用户昵称或短 ID 命中而把其他币商流水带出来。
|
||||
sellerIDs, sellerFiltered, err := s.resolveCoinSellerFilter(ctx, appCode, query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if sellerFiltered && len(sellerIDs) == 0 {
|
||||
// 用户库已经确认没有匹配币商时直接返回空分页,避免对 wallet 库做没有结果意义的全量账本扫描。
|
||||
return []coinSellerLedgerDTO{}, 0, nil
|
||||
}
|
||||
|
||||
// 币商流水只读币商专用金币分录;wallet-service 仍是账务事实 owner,后台不回写余额、不补账、不改变交易状态,
|
||||
// 这里只按运营筛选把 wallet_entries 与 wallet_transactions 的事实组装成后台展示投影。
|
||||
whereSQL, args, err := coinSellerLedgerWhere(appCode, query, sellerIDs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var total int64
|
||||
if paginated {
|
||||
if err := s.walletDB.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*)
|
||||
FROM wallet_entries e
|
||||
JOIN wallet_transactions wt ON wt.app_code = e.app_code AND wt.transaction_id = e.transaction_id
|
||||
`+whereSQL,
|
||||
args...,
|
||||
).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// 明细查询只取币商侧 COIN_SELLER_COIN 分录:available_delta 表示币商库存可用变化,
|
||||
// available_after 表示该币商分录落账后的库存余额,receiver 再按 biz_type 从 metadata/counterparty 补出。
|
||||
querySQL := `
|
||||
SELECT e.entry_id, e.transaction_id, wt.command_id, e.user_id, wt.biz_type,
|
||||
e.available_delta, e.available_after, e.counterparty_user_id,
|
||||
COALESCE(CAST(wt.metadata_json AS CHAR), '{}'), e.created_at_ms
|
||||
FROM wallet_entries e
|
||||
JOIN wallet_transactions wt ON wt.app_code = e.app_code AND wt.transaction_id = e.transaction_id
|
||||
` + whereSQL + `
|
||||
ORDER BY e.created_at_ms DESC, e.entry_id DESC`
|
||||
queryArgs := args
|
||||
if paginated {
|
||||
querySQL += `
|
||||
LIMIT ? OFFSET ?`
|
||||
queryArgs = append(queryArgs, query.PageSize, offset(query.Page, query.PageSize))
|
||||
}
|
||||
rows, err := s.walletDB.QueryContext(ctx, querySQL, queryArgs...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]coinSellerLedgerDTO, 0, query.PageSize)
|
||||
profileIDs := make([]int64, 0, query.PageSize*2)
|
||||
operatorIDs := make([]int64, 0, query.PageSize)
|
||||
for rows.Next() {
|
||||
var item coinSellerLedgerDTO
|
||||
var sellerUserID int64
|
||||
var counterpartyUserID int64
|
||||
var metadataJSON string
|
||||
if err := rows.Scan(
|
||||
&item.EntryID,
|
||||
&item.TransactionID,
|
||||
&item.CommandID,
|
||||
&sellerUserID,
|
||||
&item.BizType,
|
||||
&item.AvailableDelta,
|
||||
&item.SellerBalanceAfter,
|
||||
&counterpartyUserID,
|
||||
&metadataJSON,
|
||||
&item.CreatedAtMS,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
metadata, err := parseMetadataJSON(metadataJSON)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
operatorUserID := metadataInt64(metadata, "operator_user_id", "operatorUserId")
|
||||
// 收款人按产品口径取实际收款人:币商转用户优先取 metadata.target_user_id,老数据没有 metadata 时再用 counterparty_user_id;
|
||||
// 后台入账和工资转币商的实际收款人都是币商本人,所以不能被 counterparty 字段误导成付款方或操作方。
|
||||
receiverUserID := coinSellerLedgerReceiverUserID(item.BizType, sellerUserID, counterpartyUserID, metadata)
|
||||
item.LedgerType = coinSellerLedgerTypeForBizType(item.BizType)
|
||||
item.Direction = directionForDelta(item.AvailableDelta)
|
||||
item.Amount = absInt64(item.AvailableDelta)
|
||||
item.CounterpartyUserID = formatOptionalID(counterpartyUserID)
|
||||
item.OperatorUserID = formatOptionalID(operatorUserID)
|
||||
item.Metadata = metadata
|
||||
item.Seller = coinLedgerUserDTO{UserID: strconv.FormatInt(sellerUserID, 10)}
|
||||
item.Receiver = coinLedgerUserDTO{UserID: strconv.FormatInt(receiverUserID, 10)}
|
||||
items = append(items, item)
|
||||
profileIDs = append(profileIDs, sellerUserID, receiverUserID)
|
||||
if operatorUserID > 0 && item.LedgerType == coinSellerLedgerTypeAdminStockCredit {
|
||||
operatorIDs = append(operatorIDs, operatorUserID)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if !paginated {
|
||||
total = int64(len(items))
|
||||
}
|
||||
|
||||
profiles, err := s.userProfiles(ctx, appCode, profileIDs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
operators, err := s.adminProfiles(ctx, operatorIDs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for i := range items {
|
||||
sellerUserID, _ := strconv.ParseInt(items[i].Seller.UserID, 10, 64)
|
||||
receiverUserID, _ := strconv.ParseInt(items[i].Receiver.UserID, 10, 64)
|
||||
// 用户资料只做头像、短 ID 和昵称展示补全;资料缺失时仍返回账本里的 user_id,
|
||||
// 避免用户资料迁移、删除或延迟同步把真实账务事实从后台列表里吞掉。
|
||||
if profile, ok := profiles[sellerUserID]; ok {
|
||||
items[i].Seller = userDTOFromProfile(profile)
|
||||
}
|
||||
if profile, ok := profiles[receiverUserID]; ok {
|
||||
items[i].Receiver = userDTOFromProfile(profile)
|
||||
}
|
||||
operatorUserID, _ := strconv.ParseInt(items[i].OperatorUserID, 10, 64)
|
||||
if operator, ok := operators[operatorUserID]; ok {
|
||||
items[i].Operator = operator
|
||||
}
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListCoinAdjustments(ctx context.Context, appCode string, query listQuery) ([]coinAdjustmentDTO, int64, error) {
|
||||
query = normalizeListQuery(query)
|
||||
if s == nil || s.walletDB == nil {
|
||||
@ -396,6 +580,59 @@ func (s *Service) resolveUserFilter(ctx context.Context, appCode string, keyword
|
||||
return uniqueInt64s(userIDs), true, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Service) resolveCoinSellerFilter(ctx context.Context, appCode string, query coinSellerLedgerQuery) ([]int64, bool, error) {
|
||||
if query.SellerUserID > 0 {
|
||||
// seller_user_id 是 Coin Saller 行按钮传入的精确条件,直接使用账本 user_id 过滤;
|
||||
// 即使用户资料暂时查不到,也允许账本事实被查出来,避免抽屉入口被资料缺失阻断。
|
||||
return []int64{query.SellerUserID}, true, nil
|
||||
}
|
||||
keyword := strings.TrimSpace(query.SellerKeyword)
|
||||
if keyword == "" {
|
||||
// 未传币商筛选时返回 userFiltered=false,调用方会查询所有币商流水,但仍被 biz_type、asset_type 和分页限制住。
|
||||
return nil, false, nil
|
||||
}
|
||||
if s == nil || s.userDB == nil {
|
||||
return nil, true, fmt.Errorf("user mysql is not configured")
|
||||
}
|
||||
|
||||
// 币商关键字只命中 coin_seller_profiles 里的币商本人,LEFT JOIN users 只用于短 ID 和昵称补充;
|
||||
// 这里故意不搜索 receiver/counterparty,避免“收款用户命中”把其他币商的流水带进当前币商筛选结果。
|
||||
args := []any{appCode}
|
||||
where := "WHERE csp.app_code = ? AND (u.current_display_user_id = ?"
|
||||
args = append(args, keyword)
|
||||
if numeric, err := strconv.ParseInt(keyword, 10, 64); err == nil && numeric > 0 {
|
||||
// 数字关键字按币商长 ID 精确匹配,不做 LIKE,防止短数字造成过宽的 user_id 扫描。
|
||||
where += " OR csp.user_id = ?"
|
||||
args = append(args, numeric)
|
||||
}
|
||||
where += " OR u.username LIKE ? ESCAPE '\\\\')"
|
||||
args = append(args, "%"+escapeLike(keyword)+"%")
|
||||
|
||||
rows, err := s.userDB.QueryContext(ctx, `
|
||||
SELECT csp.user_id
|
||||
FROM coin_seller_profiles csp
|
||||
LEFT JOIN users u ON u.app_code = csp.app_code AND u.user_id = csp.user_id
|
||||
`+where+`
|
||||
ORDER BY csp.updated_at_ms DESC, csp.user_id DESC
|
||||
LIMIT 1000`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
userIDs := make([]int64, 0)
|
||||
for rows.Next() {
|
||||
var userID int64
|
||||
if err := rows.Scan(&userID); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
return uniqueInt64s(userIDs), true, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Service) userProfiles(ctx context.Context, appCode string, userIDs []int64) (map[int64]userProfile, error) {
|
||||
result := make(map[int64]userProfile, len(userIDs))
|
||||
if s == nil || s.userDB == nil || len(userIDs) == 0 {
|
||||
@ -427,6 +664,15 @@ func (s *Service) userProfiles(ctx context.Context, appCode string, userIDs []in
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func userDTOFromProfile(profile userProfile) coinLedgerUserDTO {
|
||||
return coinLedgerUserDTO{
|
||||
UserID: strconv.FormatInt(profile.UserID, 10),
|
||||
DisplayUserID: profile.DisplayUserID,
|
||||
Username: profile.Username,
|
||||
Avatar: profile.Avatar,
|
||||
}
|
||||
}
|
||||
|
||||
func coinLedgerWhere(appCode string, query listQuery, userIDs []int64) (string, []any) {
|
||||
where := "WHERE e.app_code = ? AND e.asset_type = ?"
|
||||
args := []any{appCode, coinAssetType}
|
||||
@ -447,6 +693,46 @@ func coinLedgerWhere(appCode string, query listQuery, userIDs []int64) (string,
|
||||
return where, args
|
||||
}
|
||||
|
||||
func coinSellerLedgerWhere(appCode string, query coinSellerLedgerQuery, sellerIDs []int64) (string, []any, error) {
|
||||
// 所有币商流水都必须落在当前 app_code 和 COIN_SELLER_COIN 资产上;
|
||||
// 普通 COIN 分录属于用户金币流水,不能混入币商库存流水展示。
|
||||
where := "WHERE e.app_code = ? AND e.asset_type = ?"
|
||||
args := []any{appCode, coinSellerAssetType}
|
||||
bizTypes, err := coinSellerLedgerBizTypes(query.LedgerType)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if len(bizTypes) == 1 {
|
||||
// 单一类型用等值条件,保持 SQL 简洁,也方便后续索引按 biz_type 命中。
|
||||
where += " AND wt.biz_type = ?"
|
||||
args = append(args, bizTypes[0])
|
||||
} else {
|
||||
// 后台入账和“全部”都可能映射多个底层 biz_type,只展开白名单占位符,不拼接用户输入。
|
||||
where += fmt.Sprintf(" AND wt.biz_type IN (%s)", placeholders(len(bizTypes)))
|
||||
for _, bizType := range bizTypes {
|
||||
args = append(args, bizType)
|
||||
}
|
||||
}
|
||||
if query.StartAtMS > 0 {
|
||||
// 开始边界包含,和所有后台列表的 [start_at_ms, end_at_ms) 约定保持一致。
|
||||
where += " AND e.created_at_ms >= ?"
|
||||
args = append(args, query.StartAtMS)
|
||||
}
|
||||
if query.EndAtMS > 0 {
|
||||
// 结束边界不包含,避免按小时/天连续筛选时重复命中边界毫秒的同一条账本分录。
|
||||
where += " AND e.created_at_ms < ?"
|
||||
args = append(args, query.EndAtMS)
|
||||
}
|
||||
if len(sellerIDs) > 0 {
|
||||
// sellerIDs 来自精确 seller_user_id 或 coin_seller_profiles 搜索结果,只用于限制币商分录的 e.user_id。
|
||||
where += fmt.Sprintf(" AND e.user_id IN (%s)", placeholders(len(sellerIDs)))
|
||||
for _, id := range sellerIDs {
|
||||
args = append(args, id)
|
||||
}
|
||||
}
|
||||
return where, args, nil
|
||||
}
|
||||
|
||||
func coinAdjustmentWhere(appCode string, query listQuery, userIDs []int64) (string, []any) {
|
||||
where := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type = ?"
|
||||
args := []any{appCode, coinAssetType, coinManualCreditBizType}
|
||||
@ -481,6 +767,112 @@ func normalizeListQuery(query listQuery) listQuery {
|
||||
return query
|
||||
}
|
||||
|
||||
func normalizeCoinSellerLedgerQuery(query coinSellerLedgerQuery) coinSellerLedgerQuery {
|
||||
if query.Page < 1 {
|
||||
query.Page = 1
|
||||
}
|
||||
if query.PageSize < 1 {
|
||||
query.PageSize = 20
|
||||
}
|
||||
if query.PageSize > 100 {
|
||||
query.PageSize = 100
|
||||
}
|
||||
query.SellerKeyword = strings.TrimSpace(query.SellerKeyword)
|
||||
query.LedgerType = strings.TrimSpace(query.LedgerType)
|
||||
return query
|
||||
}
|
||||
|
||||
func coinSellerLedgerBizTypes(ledgerType string) ([]string, error) {
|
||||
switch strings.TrimSpace(ledgerType) {
|
||||
case "":
|
||||
// 空类型表示“全部币商流水”,但仍只允许当前产品定义的三个类型口径,不能把其他币商库存内部账带出来。
|
||||
return []string{
|
||||
coinSellerStockPurchaseBizType,
|
||||
coinSellerCoinCompensationBizType,
|
||||
coinSellerTransferBizType,
|
||||
salaryTransferToCoinSellerBizType,
|
||||
}, nil
|
||||
case coinSellerLedgerTypeAdminStockCredit:
|
||||
// 后台入账是运营口径,底层包含正常进货和金币补偿两种账务 biz_type。
|
||||
return []string{coinSellerStockPurchaseBizType, coinSellerCoinCompensationBizType}, nil
|
||||
case coinSellerLedgerTypeSellerTransfer:
|
||||
// 币商转用户只展示币商侧出账分录,收款用户资料在展示投影里补齐。
|
||||
return []string{coinSellerTransferBizType}, nil
|
||||
case coinSellerLedgerTypeSalaryTransferIncome:
|
||||
// 工资转币商是用户工资资产换入币商库存,币商侧表现为 COIN_SELLER_COIN 入账。
|
||||
return []string{salaryTransferToCoinSellerBizType}, nil
|
||||
default:
|
||||
return nil, errInvalidCoinSellerLedgerType
|
||||
}
|
||||
}
|
||||
|
||||
func coinSellerLedgerTypeForBizType(bizType string) string {
|
||||
switch bizType {
|
||||
case coinSellerStockPurchaseBizType, coinSellerCoinCompensationBizType:
|
||||
return coinSellerLedgerTypeAdminStockCredit
|
||||
case coinSellerTransferBizType:
|
||||
return coinSellerLedgerTypeSellerTransfer
|
||||
case salaryTransferToCoinSellerBizType:
|
||||
return coinSellerLedgerTypeSalaryTransferIncome
|
||||
default:
|
||||
return bizType
|
||||
}
|
||||
}
|
||||
|
||||
func coinSellerLedgerLabel(item coinSellerLedgerDTO) string {
|
||||
switch item.LedgerType {
|
||||
case coinSellerLedgerTypeAdminStockCredit:
|
||||
return "后台入账"
|
||||
case coinSellerLedgerTypeSellerTransfer:
|
||||
return "币商转用户"
|
||||
case coinSellerLedgerTypeSalaryTransferIncome:
|
||||
return "工资转币商"
|
||||
default:
|
||||
switch item.BizType {
|
||||
case coinSellerStockPurchaseBizType:
|
||||
return "币商进货"
|
||||
case coinSellerCoinCompensationBizType:
|
||||
return "金币补偿"
|
||||
case coinSellerTransferBizType:
|
||||
return "币商转用户"
|
||||
case salaryTransferToCoinSellerBizType:
|
||||
return "工资转币商"
|
||||
default:
|
||||
return firstNonEmpty(item.LedgerType, item.BizType, "-")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func signedCoinSellerLedgerAmount(item coinSellerLedgerDTO) int64 {
|
||||
if item.Direction == directionOut || item.AvailableDelta < 0 {
|
||||
return -absInt64(item.Amount)
|
||||
}
|
||||
return absInt64(item.Amount)
|
||||
}
|
||||
|
||||
func coinSellerLedgerOperatorExportFields(item coinSellerLedgerDTO) (string, string) {
|
||||
if item.LedgerType != coinSellerLedgerTypeAdminStockCredit {
|
||||
return "", ""
|
||||
}
|
||||
operatorID := firstNonEmpty(item.Operator.AdminID, item.OperatorUserID)
|
||||
return firstNonEmpty(item.Operator.Username, item.Operator.Name, operatorID), operatorID
|
||||
}
|
||||
|
||||
func coinSellerLedgerReceiverUserID(bizType string, sellerUserID int64, counterpartyUserID int64, metadata map[string]any) int64 {
|
||||
if bizType == coinSellerTransferBizType {
|
||||
// 新数据把真实收款用户写在 metadata.target_user_id,优先使用它,避免 counterparty 语义随老交易实现变化。
|
||||
if targetUserID := metadataInt64(metadata, "target_user_id", "targetUserId"); targetUserID > 0 {
|
||||
return targetUserID
|
||||
}
|
||||
// 老数据没有 target_user_id 时使用 counterparty_user_id 兜底;只有币商转用户才允许这样兜底。
|
||||
if counterpartyUserID > 0 {
|
||||
return counterpartyUserID
|
||||
}
|
||||
}
|
||||
// 后台入账和工资转币商都展示币商本人为收款人,这是页面“实际收款人”的产品口径。
|
||||
return sellerUserID
|
||||
}
|
||||
|
||||
func directionForDelta(delta int64) string {
|
||||
if delta < 0 {
|
||||
return directionOut
|
||||
@ -553,6 +945,16 @@ func metadataInt64(metadata map[string]any, keys ...string) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseFlexibleUserID(value any) (int64, error) {
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
|
||||
@ -31,6 +31,92 @@ func TestCoinAdjustmentWhereLimitsManualCreditCoinEntries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoinSellerLedgerWhereUsesExactSellerAndType(t *testing.T) {
|
||||
query := coinSellerLedgerQuery{SellerUserID: 3001, LedgerType: coinSellerLedgerTypeSellerTransfer, StartAtMS: 100, EndAtMS: 200}
|
||||
where, args, err := coinSellerLedgerWhere("lalu", query, []int64{3001})
|
||||
if err != nil {
|
||||
t.Fatalf("coin seller ledger where failed: %v", err)
|
||||
}
|
||||
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type = ? AND e.created_at_ms >= ? AND e.created_at_ms < ? AND e.user_id IN (?)"; where != want {
|
||||
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
|
||||
}
|
||||
if len(args) != 6 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerTransferBizType || args[3] != int64(100) || args[4] != int64(200) || args[5] != int64(3001) {
|
||||
t.Fatalf("args mismatch: %#v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoinSellerLedgerWhereMapsAdminStockCredit(t *testing.T) {
|
||||
query := coinSellerLedgerQuery{LedgerType: coinSellerLedgerTypeAdminStockCredit}
|
||||
where, args, err := coinSellerLedgerWhere("lalu", query, []int64{3001, 3002})
|
||||
if err != nil {
|
||||
t.Fatalf("coin seller ledger where failed: %v", err)
|
||||
}
|
||||
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?) AND e.user_id IN (?,?)"; where != want {
|
||||
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
|
||||
}
|
||||
if len(args) != 6 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerCoinCompensationBizType || args[4] != int64(3001) || args[5] != int64(3002) {
|
||||
t.Fatalf("args mismatch: %#v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoinSellerLedgerWhereEmptyTypeUsesAllPublicTypes(t *testing.T) {
|
||||
where, args, err := coinSellerLedgerWhere("lalu", coinSellerLedgerQuery{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("coin seller ledger where failed: %v", err)
|
||||
}
|
||||
if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?,?)"; where != want {
|
||||
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
|
||||
}
|
||||
if len(args) != 6 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerCoinCompensationBizType || args[4] != coinSellerTransferBizType || args[5] != salaryTransferToCoinSellerBizType {
|
||||
t.Fatalf("args mismatch: %#v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoinSellerLedgerWhereRejectsInvalidType(t *testing.T) {
|
||||
_, _, err := coinSellerLedgerWhere("lalu", coinSellerLedgerQuery{LedgerType: "bad_type"}, nil)
|
||||
if err == nil {
|
||||
t.Fatalf("expected invalid ledger type error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoinSellerLedgerReceiverUserIDUsesActualReceiver(t *testing.T) {
|
||||
metadata := map[string]any{"target_user_id": float64(4001)}
|
||||
if got := coinSellerLedgerReceiverUserID(coinSellerTransferBizType, 3001, 0, metadata); got != 4001 {
|
||||
t.Fatalf("seller transfer receiver mismatch: %d", got)
|
||||
}
|
||||
if got := coinSellerLedgerReceiverUserID(coinSellerStockPurchaseBizType, 3001, 4001, metadata); got != 3001 {
|
||||
t.Fatalf("admin stock receiver mismatch: %d", got)
|
||||
}
|
||||
if got := coinSellerLedgerReceiverUserID(salaryTransferToCoinSellerBizType, 3001, 4001, metadata); got != 3001 {
|
||||
t.Fatalf("salary transfer receiver mismatch: %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoinSellerLedgerOperatorExportFieldsOnlyForAdminStock(t *testing.T) {
|
||||
adminItem := coinSellerLedgerDTO{
|
||||
LedgerType: coinSellerLedgerTypeAdminStockCredit,
|
||||
OperatorUserID: "7",
|
||||
Operator: coinAdjustmentOperatorDTO{AdminID: "7", Username: "hyappadmin", Name: "Admin"},
|
||||
}
|
||||
operatorName, operatorID := coinSellerLedgerOperatorExportFields(adminItem)
|
||||
if operatorName != "hyappadmin" || operatorID != "7" {
|
||||
t.Fatalf("operator fields mismatch: name=%q id=%q", operatorName, operatorID)
|
||||
}
|
||||
|
||||
transferItem := coinSellerLedgerDTO{LedgerType: coinSellerLedgerTypeSellerTransfer, OperatorUserID: "7"}
|
||||
operatorName, operatorID = coinSellerLedgerOperatorExportFields(transferItem)
|
||||
if operatorName != "" || operatorID != "" {
|
||||
t.Fatalf("seller transfer should not export operator: name=%q id=%q", operatorName, operatorID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeCoinSellerLedgerQueryCapsPageSize(t *testing.T) {
|
||||
query := normalizeCoinSellerLedgerQuery(coinSellerLedgerQuery{Page: -1, PageSize: 1000, SellerKeyword: " 164425 ", LedgerType: " seller_transfer "})
|
||||
if query.Page != 1 || query.PageSize != 100 || query.SellerKeyword != "164425" || query.LedgerType != coinSellerLedgerTypeSellerTransfer {
|
||||
t.Fatalf("normalized query mismatch: %+v", query)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectionAndAmountForDelta(t *testing.T) {
|
||||
if directionForDelta(-9) != directionOut || absInt64(-9) != 9 {
|
||||
t.Fatalf("expense projection mismatch")
|
||||
|
||||
@ -0,0 +1,356 @@
|
||||
package cumulativerechargereward
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
"hyapp-admin-server/internal/integration/activityclient"
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
"hyapp-admin-server/internal/modules/shared"
|
||||
"hyapp-admin-server/internal/response"
|
||||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
activity activityclient.Client
|
||||
userDB *sql.DB
|
||||
audit shared.OperationLogger
|
||||
}
|
||||
|
||||
func New(activity activityclient.Client, userDB *sql.DB, audit shared.OperationLogger) *Handler {
|
||||
return &Handler{activity: activity, userDB: userDB, audit: audit}
|
||||
}
|
||||
|
||||
type configRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Tiers []tierDTO `json:"tiers"`
|
||||
}
|
||||
|
||||
type configDTO struct {
|
||||
AppCode string `json:"app_code"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Tiers []tierDTO `json:"tiers"`
|
||||
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
type tierDTO struct {
|
||||
TierID int64 `json:"tier_id"`
|
||||
TierCode string `json:"tier_code"`
|
||||
TierName string `json:"tier_name"`
|
||||
ThresholdUSDMinor int64 `json:"threshold_usd_minor"`
|
||||
ResourceGroupID int64 `json:"resource_group_id"`
|
||||
Status string `json:"status"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
type grantDTO struct {
|
||||
GrantID string `json:"grant_id"`
|
||||
AppCode string `json:"app_code"`
|
||||
CycleKey string `json:"cycle_key"`
|
||||
UserID int64 `json:"user_id"`
|
||||
User *userDTO `json:"user,omitempty"`
|
||||
EventID string `json:"event_id"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
CommandID string `json:"command_id"`
|
||||
TierID int64 `json:"tier_id"`
|
||||
TierCode string `json:"tier_code"`
|
||||
ThresholdUSDMinor int64 `json:"threshold_usd_minor"`
|
||||
ResourceGroupID int64 `json:"resource_group_id"`
|
||||
ReachedUSDMinor int64 `json:"reached_usd_minor"`
|
||||
QualifyingUSDMinor int64 `json:"qualifying_usd_minor"`
|
||||
RechargeCoinAmount int64 `json:"recharge_coin_amount"`
|
||||
RechargeType string `json:"recharge_type"`
|
||||
Status string `json:"status"`
|
||||
WalletCommandID string `json:"wallet_command_id"`
|
||||
WalletGrantID string `json:"wallet_grant_id"`
|
||||
FailureReason string `json:"failure_reason"`
|
||||
GrantedAtMS int64 `json:"granted_at_ms"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
type userDTO struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
DisplayUserID string `json:"display_user_id"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
func (h *Handler) GetConfig(c *gin.Context) {
|
||||
resp, err := h.activity.GetCumulativeRechargeRewardConfig(c.Request.Context(), &activityv1.GetCumulativeRechargeRewardConfigRequest{Meta: h.meta(c)})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取累充奖励配置失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, configFromProto(resp.GetConfig()))
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateConfig(c *gin.Context) {
|
||||
var req configRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "累充奖励配置参数不正确")
|
||||
return
|
||||
}
|
||||
// admin-server 只做输入透传和操作人注入,档位有效性、重复门槛和启用规则统一交给 activity-service 校验。
|
||||
tiers := make([]*activityv1.CumulativeRechargeRewardTier, 0, len(req.Tiers))
|
||||
for _, tier := range req.Tiers {
|
||||
tiers = append(tiers, &activityv1.CumulativeRechargeRewardTier{
|
||||
TierId: tier.TierID,
|
||||
TierCode: strings.TrimSpace(tier.TierCode),
|
||||
TierName: strings.TrimSpace(tier.TierName),
|
||||
ThresholdUsdMinor: tier.ThresholdUSDMinor,
|
||||
ResourceGroupId: tier.ResourceGroupID,
|
||||
Status: strings.TrimSpace(tier.Status),
|
||||
SortOrder: tier.SortOrder,
|
||||
})
|
||||
}
|
||||
resp, err := h.activity.UpdateCumulativeRechargeRewardConfig(c.Request.Context(), &activityv1.UpdateCumulativeRechargeRewardConfigRequest{
|
||||
Meta: h.meta(c),
|
||||
Enabled: req.Enabled,
|
||||
Tiers: tiers,
|
||||
OperatorAdminId: int64(middleware.CurrentUserID(c)),
|
||||
})
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
item := configFromProto(resp.GetConfig())
|
||||
shared.OperationLogWithResourceID(c, h.audit, "update-cumulative-recharge-reward", "cumulative_recharge_reward_configs", item.AppCode, "success", "")
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) ListGrants(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
// 发放记录主表只存 user_id;后台 keyword 先在 users 表解析成精确用户,再下推给 activity-service 查询。
|
||||
userID, matched, ok := h.resolveGrantUserID(c.Request.Context(), strings.TrimSpace(options.Keyword))
|
||||
if !ok {
|
||||
response.ServerError(c, "查询用户信息失败")
|
||||
return
|
||||
}
|
||||
if options.Keyword != "" && !matched {
|
||||
response.OK(c, response.Page{Items: []grantDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
|
||||
return
|
||||
}
|
||||
resp, err := h.activity.ListCumulativeRechargeRewardGrants(c.Request.Context(), &activityv1.ListCumulativeRechargeRewardGrantsRequest{
|
||||
Meta: h.meta(c),
|
||||
Status: strings.TrimSpace(options.Status),
|
||||
UserId: userID,
|
||||
CycleKey: strings.TrimSpace(c.Query("cycle_key")),
|
||||
Page: int32(options.Page),
|
||||
PageSize: int32(options.PageSize),
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取累充奖励发放记录失败")
|
||||
return
|
||||
}
|
||||
items := make([]grantDTO, 0, len(resp.GetGrants()))
|
||||
for _, grant := range resp.GetGrants() {
|
||||
items = append(items, grantFromProto(grant))
|
||||
}
|
||||
if err := h.fillGrantUsers(c.Request.Context(), items); err != nil {
|
||||
response.ServerError(c, "补全用户信息失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||||
}
|
||||
|
||||
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
|
||||
return &activityv1.RequestMeta{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
Caller: "admin-server",
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
SentAtMs: time.Now().UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) resolveGrantUserID(ctx context.Context, keyword string) (int64, bool, bool) {
|
||||
if keyword == "" {
|
||||
return 0, false, true
|
||||
}
|
||||
if h.userDB == nil {
|
||||
return 0, false, true
|
||||
}
|
||||
// keyword 支持真实 user_id、短 ID 和昵称模糊匹配;排序优先精确 ID,避免昵称碰撞查到错误用户。
|
||||
appCode := appctx.FromContext(ctx)
|
||||
rows, err := h.userDB.QueryContext(ctx, `
|
||||
SELECT user_id
|
||||
FROM users
|
||||
WHERE app_code = ?
|
||||
AND (
|
||||
CAST(user_id AS CHAR) = ?
|
||||
OR current_display_user_id = ?
|
||||
OR username LIKE ?
|
||||
)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN CAST(user_id AS CHAR) = ? THEN 0
|
||||
WHEN current_display_user_id = ? THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
user_id DESC
|
||||
LIMIT 1`,
|
||||
appCode, keyword, keyword, "%"+keyword+"%", keyword, keyword,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, false, false
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
return 0, false, rows.Err() == nil
|
||||
}
|
||||
var userID int64
|
||||
if err := rows.Scan(&userID); err != nil {
|
||||
return 0, false, false
|
||||
}
|
||||
return userID, true, rows.Err() == nil
|
||||
}
|
||||
|
||||
func (h *Handler) fillGrantUsers(ctx context.Context, grants []grantDTO) error {
|
||||
if h.userDB == nil || len(grants) == 0 {
|
||||
return nil
|
||||
}
|
||||
// 列表补用户资料是展示增强,找不到用户时仍返回 grant 原始 user_id,避免审计记录因为用户资料缺失而不可见。
|
||||
ids := collectGrantUserIDs(grants)
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
args := append([]any{appctx.FromContext(ctx)}, int64Args(ids)...)
|
||||
rows, err := h.userDB.QueryContext(ctx, `
|
||||
SELECT user_id, current_display_user_id, COALESCE(username, ''), COALESCE(avatar, '')
|
||||
FROM users
|
||||
WHERE app_code = ? AND user_id IN (`+placeholders(len(ids))+`)`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
users := make(map[int64]userDTO, len(ids))
|
||||
for rows.Next() {
|
||||
var user userDTO
|
||||
if err := rows.Scan(&user.UserID, &user.DisplayUserID, &user.Username, &user.Avatar); err != nil {
|
||||
return err
|
||||
}
|
||||
users[user.UserID] = user
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
for index := range grants {
|
||||
if user, ok := users[grants[index].UserID]; ok {
|
||||
grants[index].User = &user
|
||||
continue
|
||||
}
|
||||
grants[index].User = &userDTO{UserID: grants[index].UserID}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func configFromProto(config *activityv1.CumulativeRechargeRewardConfig) configDTO {
|
||||
if config == nil {
|
||||
return configDTO{Tiers: []tierDTO{}}
|
||||
}
|
||||
tiers := make([]tierDTO, 0, len(config.GetTiers()))
|
||||
for _, tier := range config.GetTiers() {
|
||||
tiers = append(tiers, tierFromProto(tier))
|
||||
}
|
||||
return configDTO{
|
||||
AppCode: config.GetAppCode(),
|
||||
Enabled: config.GetEnabled(),
|
||||
Tiers: tiers,
|
||||
UpdatedByAdminID: config.GetUpdatedByAdminId(),
|
||||
CreatedAtMS: config.GetCreatedAtMs(),
|
||||
UpdatedAtMS: config.GetUpdatedAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func tierFromProto(tier *activityv1.CumulativeRechargeRewardTier) tierDTO {
|
||||
if tier == nil {
|
||||
return tierDTO{}
|
||||
}
|
||||
return tierDTO{
|
||||
TierID: tier.GetTierId(),
|
||||
TierCode: tier.GetTierCode(),
|
||||
TierName: tier.GetTierName(),
|
||||
ThresholdUSDMinor: tier.GetThresholdUsdMinor(),
|
||||
ResourceGroupID: tier.GetResourceGroupId(),
|
||||
Status: tier.GetStatus(),
|
||||
SortOrder: tier.GetSortOrder(),
|
||||
CreatedAtMS: tier.GetCreatedAtMs(),
|
||||
UpdatedAtMS: tier.GetUpdatedAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func grantFromProto(grant *activityv1.CumulativeRechargeRewardGrant) grantDTO {
|
||||
if grant == nil {
|
||||
return grantDTO{}
|
||||
}
|
||||
return grantDTO{
|
||||
GrantID: grant.GetGrantId(),
|
||||
AppCode: grant.GetAppCode(),
|
||||
CycleKey: grant.GetCycleKey(),
|
||||
UserID: grant.GetUserId(),
|
||||
EventID: grant.GetEventId(),
|
||||
TransactionID: grant.GetTransactionId(),
|
||||
CommandID: grant.GetCommandId(),
|
||||
TierID: grant.GetTierId(),
|
||||
TierCode: grant.GetTierCode(),
|
||||
ThresholdUSDMinor: grant.GetThresholdUsdMinor(),
|
||||
ResourceGroupID: grant.GetResourceGroupId(),
|
||||
ReachedUSDMinor: grant.GetReachedUsdMinor(),
|
||||
QualifyingUSDMinor: grant.GetQualifyingUsdMinor(),
|
||||
RechargeCoinAmount: grant.GetRechargeCoinAmount(),
|
||||
RechargeType: grant.GetRechargeType(),
|
||||
Status: grant.GetStatus(),
|
||||
WalletCommandID: grant.GetWalletCommandId(),
|
||||
WalletGrantID: grant.GetWalletGrantId(),
|
||||
FailureReason: grant.GetFailureReason(),
|
||||
GrantedAtMS: grant.GetGrantedAtMs(),
|
||||
CreatedAtMS: grant.GetCreatedAtMs(),
|
||||
UpdatedAtMS: grant.GetUpdatedAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func collectGrantUserIDs(grants []grantDTO) []int64 {
|
||||
seen := make(map[int64]struct{}, len(grants))
|
||||
ids := make([]int64, 0, len(grants))
|
||||
for _, grant := range grants {
|
||||
if grant.UserID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[grant.UserID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[grant.UserID] = struct{}{}
|
||||
ids = append(ids, grant.UserID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func int64Args(ids []int64) []any {
|
||||
args := make([]any, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
args = append(args, id)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func placeholders(count int) string {
|
||||
if count <= 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, count)
|
||||
for i := range parts {
|
||||
parts[i] = "?"
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package cumulativerechargereward
|
||||
|
||||
import (
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
protected.GET("/admin/activity/cumulative-recharge-reward/config", middleware.RequirePermission("cumulative-recharge-reward:view"), h.GetConfig)
|
||||
protected.PUT("/admin/activity/cumulative-recharge-reward/config", middleware.RequirePermission("cumulative-recharge-reward:update"), h.UpdateConfig)
|
||||
protected.GET("/admin/activity/cumulative-recharge-reward/grants", middleware.RequirePermission("cumulative-recharge-reward:view"), h.ListGrants)
|
||||
}
|
||||
@ -28,10 +28,13 @@ func (h *Handler) DashboardOverview(c *gin.Context) {
|
||||
|
||||
func (h *Handler) StatisticsOverview(c *gin.Context) {
|
||||
overview, err := h.service.StatisticsOverview(c.Request.Context(), StatisticsQuery{
|
||||
AppCode: c.DefaultQuery("app_code", "lalu"),
|
||||
StartMS: parseInt64(c.Query("start_ms")),
|
||||
EndMS: parseInt64(c.Query("end_ms")),
|
||||
CountryID: parseInt64(c.Query("country_id")),
|
||||
AppCode: c.DefaultQuery("app_code", "lalu"),
|
||||
StartMS: parseInt64(c.Query("start_ms")),
|
||||
EndMS: parseInt64(c.Query("end_ms")),
|
||||
SeriesStartMS: parseInt64(c.Query("series_start_ms")),
|
||||
SeriesEndMS: parseInt64(c.Query("series_end_ms")),
|
||||
RegionID: parseInt64(c.Query("region_id")),
|
||||
CountryID: parseInt64(c.Query("country_id")),
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取统计总览失败")
|
||||
|
||||
@ -19,10 +19,13 @@ type DashboardService struct {
|
||||
}
|
||||
|
||||
type StatisticsQuery struct {
|
||||
AppCode string
|
||||
StartMS int64
|
||||
EndMS int64
|
||||
CountryID int64
|
||||
AppCode string
|
||||
StartMS int64
|
||||
EndMS int64
|
||||
SeriesStartMS int64
|
||||
SeriesEndMS int64
|
||||
RegionID int64
|
||||
CountryID int64
|
||||
}
|
||||
|
||||
func NewService(store *repository.Store, cfg config.Config) *DashboardService {
|
||||
@ -47,6 +50,15 @@ func (s *DashboardService) StatisticsOverview(ctx context.Context, query Statist
|
||||
if query.EndMS > 0 {
|
||||
values.Set("end_ms", strconv.FormatInt(query.EndMS, 10))
|
||||
}
|
||||
if query.SeriesStartMS > 0 {
|
||||
values.Set("series_start_ms", strconv.FormatInt(query.SeriesStartMS, 10))
|
||||
}
|
||||
if query.SeriesEndMS > 0 {
|
||||
values.Set("series_end_ms", strconv.FormatInt(query.SeriesEndMS, 10))
|
||||
}
|
||||
if query.RegionID > 0 {
|
||||
values.Set("region_id", strconv.FormatInt(query.RegionID, 10))
|
||||
}
|
||||
if query.CountryID > 0 {
|
||||
values.Set("country_id", strconv.FormatInt(query.CountryID, 10))
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ import (
|
||||
const (
|
||||
adapterBaishunV1 = "baishun_v1"
|
||||
adapterZeeOneV1 = "zeeone_v1"
|
||||
adapterVivaGamesV1 = "vivagames_v1"
|
||||
defaultGameStatus = "disabled"
|
||||
defaultGameCategory = "casino"
|
||||
defaultLaunchMode = "full_screen"
|
||||
@ -131,6 +132,8 @@ func fetchProviderGameSyncPlan(ctx context.Context, platform *gamev1.GamePlatfor
|
||||
return fetchBaishunGameSyncPlan(ctx, platform, req)
|
||||
case adapterZeeOneV1:
|
||||
return fetchZeeOneGameSyncPlan(platform, req)
|
||||
case adapterVivaGamesV1:
|
||||
return fetchVivaGamesGameSyncPlan(platform, req)
|
||||
default:
|
||||
// 不在配置文件写死游戏:每家厂商新增“列表 API”时只加一个 adapter fetcher,后台入口保持不变。
|
||||
return providerGameSyncPlan{}, fmt.Errorf("当前适配器暂未接入厂商游戏列表 API: %s", platform.GetAdapterType())
|
||||
@ -177,6 +180,14 @@ type zeeoneSyncConfig struct {
|
||||
GameCovers map[string]string `json:"game_covers"`
|
||||
}
|
||||
|
||||
type vivaGamesSyncConfig struct {
|
||||
// VIVAGAMES 文档说明游戏 URL 由厂商提供,后台用这个映射把 URL 转成可勾选的本地目录候选项。
|
||||
GameURLs map[string]string `json:"game_urls"`
|
||||
GameNames map[string]string `json:"game_names"`
|
||||
GameIcons map[string]string `json:"game_icons"`
|
||||
GameCovers map[string]string `json:"game_covers"`
|
||||
}
|
||||
|
||||
func fetchZeeOneGameSyncPlan(platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) {
|
||||
config, err := decodeZeeOneSyncConfig(platform.GetAdapterConfigJson())
|
||||
if err != nil {
|
||||
@ -257,6 +268,67 @@ func zeeoneGameNameFromURL(raw string) string {
|
||||
return titleGameName(candidate)
|
||||
}
|
||||
|
||||
func fetchVivaGamesGameSyncPlan(platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) {
|
||||
config, err := decodeVivaGamesSyncConfig(platform.GetAdapterConfigJson())
|
||||
if err != nil {
|
||||
return providerGameSyncPlan{}, err
|
||||
}
|
||||
games, gameURLs := vivaGamesCatalogItems(platform.GetPlatformCode(), config, req)
|
||||
if len(games) == 0 {
|
||||
return providerGameSyncPlan{}, fmt.Errorf("VIVAGAMES 游戏列表为空:请在 adapterConfigJson.game_urls 配置游戏 ID 到 H5 URL 的映射")
|
||||
}
|
||||
return providerGameSyncPlan{
|
||||
Games: games,
|
||||
GameURLs: gameURLs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeVivaGamesSyncConfig(raw string) (vivaGamesSyncConfig, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return vivaGamesSyncConfig{}, nil
|
||||
}
|
||||
var config vivaGamesSyncConfig
|
||||
if err := json.Unmarshal([]byte(raw), &config); err != nil {
|
||||
return vivaGamesSyncConfig{}, fmt.Errorf("VIVAGAMES 适配器配置 JSON 不合法")
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func vivaGamesCatalogItems(platformCode string, config vivaGamesSyncConfig, req syncGamesRequest) ([]catalogRequest, map[string]string) {
|
||||
keys := make([]string, 0, len(config.GameURLs))
|
||||
for providerGameID, launchURL := range config.GameURLs {
|
||||
if strings.TrimSpace(providerGameID) != "" && strings.TrimSpace(launchURL) != "" {
|
||||
keys = append(keys, strings.TrimSpace(providerGameID))
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
items := make([]catalogRequest, 0, len(keys))
|
||||
gameURLs := make(map[string]string, len(keys))
|
||||
for index, providerGameID := range keys {
|
||||
launchURL := strings.TrimSpace(config.GameURLs[providerGameID])
|
||||
gameURLs[providerGameID] = launchURL
|
||||
name := firstNonEmpty(config.GameNames[providerGameID], zeeoneGameNameFromURL(launchURL), providerGameID)
|
||||
iconURL := strings.TrimSpace(config.GameIcons[providerGameID])
|
||||
coverURL := firstNonEmpty(config.GameCovers[providerGameID], iconURL)
|
||||
items = append(items, catalogRequest{
|
||||
GameID: stableGameID(platformCode, providerGameID),
|
||||
PlatformCode: strings.TrimSpace(platformCode),
|
||||
ProviderGameID: providerGameID,
|
||||
GameName: name,
|
||||
Category: defaulted(req.Category, defaultGameCategory),
|
||||
IconURL: iconURL,
|
||||
CoverURL: coverURL,
|
||||
LaunchMode: defaulted(req.LaunchMode, defaultLaunchMode),
|
||||
Orientation: defaultOrientation,
|
||||
MinCoin: req.MinCoin,
|
||||
Status: defaulted(req.Status, defaultGameStatus),
|
||||
SortOrder: int32((index + 1) * 10),
|
||||
Tags: compactTags(append([]string{strings.TrimSpace(platformCode), adapterVivaGamesV1}, req.Tags...)),
|
||||
})
|
||||
}
|
||||
return items, gameURLs
|
||||
}
|
||||
|
||||
func titleGameName(value string) string {
|
||||
tokens := splitGameNameTokens(value)
|
||||
if len(tokens) == 0 {
|
||||
|
||||
@ -105,6 +105,37 @@ func TestFetchZeeOneGameSyncPlanReadsConfiguredGameURLs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchVivaGamesGameSyncPlanReadsConfiguredGameURLs(t *testing.T) {
|
||||
plan, err := fetchProviderGameSyncPlan(t.Context(), &gamev1.GamePlatform{
|
||||
PlatformCode: "vivagames",
|
||||
AdapterType: adapterVivaGamesV1,
|
||||
AdapterConfigJson: `{
|
||||
"game_urls": {
|
||||
"2": "https://dev-rich2.s3.ap-southeast-1.amazonaws.com/richForever.html?game_id=2&isShow=1"
|
||||
},
|
||||
"game_names": {
|
||||
"2": "Rich Forever"
|
||||
}
|
||||
}`,
|
||||
}, syncGamesRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("fetchProviderGameSyncPlan failed: %v", err)
|
||||
}
|
||||
if len(plan.Games) != 1 {
|
||||
t.Fatalf("games len = %d, want 1", len(plan.Games))
|
||||
}
|
||||
game := plan.Games[0]
|
||||
if game.GameID != "vivagames_2" || game.ProviderGameID != "2" || game.GameName != "Rich Forever" {
|
||||
t.Fatalf("vivagames game mismatch: %+v", game)
|
||||
}
|
||||
if plan.GameURLs["2"] != "https://dev-rich2.s3.ap-southeast-1.amazonaws.com/richForever.html?game_id=2&isShow=1" {
|
||||
t.Fatalf("game url mismatch: %+v", plan.GameURLs)
|
||||
}
|
||||
if game.Status != "disabled" || game.LaunchMode != "full_screen" || game.Orientation != "portrait" {
|
||||
t.Fatalf("default fields mismatch: %+v", game)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectProviderGamesKeepsOnlySelectedIDs(t *testing.T) {
|
||||
games := []catalogRequest{
|
||||
{ProviderGameID: "1001", GameName: "one"},
|
||||
|
||||
@ -91,7 +91,7 @@ func (s *Service) resolveRatio(ctx context.Context, appCode string, regionID int
|
||||
return item, err
|
||||
}
|
||||
}
|
||||
return ratioDTO{GiftTypeCode: giftType, RatioPercent: "100.00", RegionID: regionID, EffectiveRegionID: 0, Status: statusActive}, nil
|
||||
return ratioDTO{GiftTypeCode: giftType, RatioPercent: defaultRatioPercent(giftType), RegionID: regionID, EffectiveRegionID: 0, Status: statusActive}, nil
|
||||
}
|
||||
|
||||
func (s *Service) getRatio(ctx context.Context, appCode string, regionID int64, giftType string) (ratioDTO, bool, error) {
|
||||
@ -133,6 +133,17 @@ func formatPercentString(raw string) string {
|
||||
return fmt.Sprintf("%.2f", value)
|
||||
}
|
||||
|
||||
func defaultRatioPercent(giftType string) string {
|
||||
switch strings.TrimSpace(giftType) {
|
||||
case "lucky":
|
||||
return "10.00"
|
||||
case "super_lucky":
|
||||
return "1.00"
|
||||
default:
|
||||
return "100.00"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ensureSchema(ctx context.Context) error {
|
||||
if s == nil || s.db == nil {
|
||||
return errors.New("wallet db is not configured")
|
||||
@ -140,17 +151,17 @@ func (s *Service) ensureSchema(ctx context.Context) error {
|
||||
if _, err := s.db.ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS gift_diamond_ratio_configs (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '送礼用户区域,0 表示全局默认',
|
||||
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '房间可见区域,0 表示全局默认',
|
||||
gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态',
|
||||
ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 100.00 COMMENT '主播周期钻石入账比例,百分比',
|
||||
ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 100.00 COMMENT '房间贡献热度和主播周期钻石入账比例,百分比',
|
||||
created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台用户 ID',
|
||||
updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新后台用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, region_id, gift_type_code),
|
||||
KEY idx_gift_diamond_ratio_region (app_code, region_id, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物入主播周期钻石比例配置表'`); err != nil {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物钻石和房间贡献比例配置表'`); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
@ -159,8 +170,8 @@ func (s *Service) ensureSchema(ctx context.Context) error {
|
||||
created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES
|
||||
(?, 0, 'normal', 'active', 100.00, 0, 0, 0, 0),
|
||||
(?, 0, 'lucky', 'active', 100.00, 0, 0, 0, 0),
|
||||
(?, 0, 'super_lucky', 'active', 100.00, 0, 0, 0, 0)`,
|
||||
(?, 0, 'lucky', 'active', 10.00, 0, 0, 0, 0),
|
||||
(?, 0, 'super_lucky', 'active', 1.00, 0, 0, 0, 0)`,
|
||||
"lalu", "lalu", "lalu")
|
||||
return err
|
||||
}
|
||||
|
||||
@ -27,7 +27,8 @@ const (
|
||||
settlementTriggerAutomatic = "automatic"
|
||||
settlementTriggerManual = "manual"
|
||||
|
||||
// 礼物金币转主播钻石默认 1:1;月底剩余钻石转美元默认不开启,由后台显式配置。
|
||||
// 礼物金币转主播钻石由 gift_diamond_ratio_configs 统一控制;工资政策保留 1:1 快照只用于运行表字段完整。
|
||||
// 月底剩余钻石转美元默认不开启,由后台显式配置。
|
||||
defaultGiftCoinToDiamondRatio = "1"
|
||||
defaultResidualDiamondRate = "0"
|
||||
|
||||
@ -304,9 +305,9 @@ func policyModelFromRequest(appCode string, actorID uint, req policyRequest) (mo
|
||||
if req.EffectiveFromMS < 0 || req.EffectiveToMS < 0 || (req.EffectiveToMS > 0 && req.EffectiveToMS <= req.EffectiveFromMS) {
|
||||
return model.HostAgencySalaryPolicy{}, errors.New("政策生效时间不正确")
|
||||
}
|
||||
// 比例字段用 decimal 字符串入库,避免 float 在金币/钻石/美元换算中产生精度噪声。
|
||||
giftRatio := firstNonBlank(req.GiftCoinToDiamondRatio, defaultGiftCoinToDiamondRatio)
|
||||
giftRatio, _, err := parseFixedDecimal(giftRatio, 6, false, "金币转钻石比例")
|
||||
// 送礼入主播周期钻石的实际比例已经由“礼物钻石”配置在 wallet 扣礼物时写入周期账户;
|
||||
// 这里不再接受工资政策单独覆盖,避免同一笔礼物在入账和结算阶段出现两套钻石口径。
|
||||
giftRatio, _, err := parseFixedDecimal(defaultGiftCoinToDiamondRatio, 6, false, "金币转钻石比例")
|
||||
if err != nil {
|
||||
return model.HostAgencySalaryPolicy{}, err
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ func TestPolicyModelFromRequestNormalizesAndSortsLevels(t *testing.T) {
|
||||
Status: "active",
|
||||
SettlementMode: "half-month",
|
||||
SettlementTriggerMode: "manual",
|
||||
GiftCoinToDiamondRatio: "1",
|
||||
GiftCoinToDiamondRatio: "9",
|
||||
ResidualDiamondToUSDRate: "0.000001",
|
||||
Levels: []levelRequest{
|
||||
{Level: 2, RequiredDiamonds: 700000, HostSalaryUSD: "3", HostCoinReward: 198000, AgencySalaryUSD: "0.8"},
|
||||
@ -22,6 +22,7 @@ func TestPolicyModelFromRequestNormalizesAndSortsLevels(t *testing.T) {
|
||||
if item.AppCode != "lalu" || item.CreatedByAdminID != 7 || item.SettlementMode != settlementModeHalfMonth || item.SettlementTriggerMode != settlementTriggerManual {
|
||||
t.Fatalf("policy fields mismatch: %+v", item)
|
||||
}
|
||||
// 工资政策不能单独改送礼入主播周期钻石的比例;该比例只由礼物钻石配置页控制。
|
||||
if item.GiftCoinToDiamondRatio != "1.000000" || item.ResidualDiamondToUSDRate != "0.000001000000" {
|
||||
t.Fatalf("ratio fields mismatch: %+v", item)
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ func (h *Handler) CreateBDLeader(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
writeHostOrgAuditLog(c, h.audit, "create-bd-leader", "bd_profiles", profile.UserID,
|
||||
fmt.Sprintf("command_id=%s user_id=%d region_id=%d reason=%q", strings.TrimSpace(req.CommandID), profile.UserID, req.RegionID, strings.TrimSpace(req.Reason)))
|
||||
fmt.Sprintf("command_id=%s user_id=%d region_id=%d position_alias=%q reason=%q", strings.TrimSpace(req.CommandID), profile.UserID, profile.RegionID, strings.TrimSpace(req.PositionAlias), strings.TrimSpace(req.Reason)))
|
||||
response.Created(c, profile)
|
||||
}
|
||||
|
||||
@ -141,6 +141,26 @@ func (h *Handler) SetBDStatus(c *gin.Context) {
|
||||
response.OK(c, profile)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateBDLeaderPositionAlias(c *gin.Context) {
|
||||
targetUserID, ok := parseInt64ID(c, "user_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req bdLeaderPositionAliasRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "职位别名参数不正确")
|
||||
return
|
||||
}
|
||||
profile, err := h.service.UpdateBDLeaderPositionAlias(c.Request.Context(), adminActorID(c), targetUserID, req)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
writeHostOrgAuditLog(c, h.audit, "set-bd-leader-position-alias", "admin_bd_leader_position_aliases", profile.UserID,
|
||||
fmt.Sprintf("command_id=%s user_id=%d position_alias=%q reason=%q", strings.TrimSpace(req.CommandID), profile.UserID, profile.PositionAlias, strings.TrimSpace(req.Reason)))
|
||||
response.OK(c, profile)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteBDLeader(c *gin.Context) {
|
||||
targetUserID, ok := parseInt64ID(c, "user_id")
|
||||
if !ok {
|
||||
@ -213,6 +233,39 @@ func (h *Handler) CreditCoinSellerStock(c *gin.Context) {
|
||||
response.Created(c, coinSellerStockCreditFromProto(result))
|
||||
}
|
||||
|
||||
func (h *Handler) GetCoinSellerSalaryRates(c *gin.Context) {
|
||||
regionID, ok := parseInt64ID(c, "region_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.service.GetCoinSellerSalaryRates(c.Request.Context(), regionID)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"regionId": regionID, "tiers": items})
|
||||
}
|
||||
|
||||
func (h *Handler) ReplaceCoinSellerSalaryRates(c *gin.Context) {
|
||||
regionID, ok := parseInt64ID(c, "region_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req replaceCoinSellerSalaryRatesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "币商工资兑换比例参数不正确")
|
||||
return
|
||||
}
|
||||
items, err := h.service.ReplaceCoinSellerSalaryRates(c.Request.Context(), adminActorID(c), regionID, req)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
writeHostOrgAuditLog(c, h.audit, "coin-seller-salary-rates", "coin_seller_salary_exchange_rate_tiers", regionID,
|
||||
fmt.Sprintf("region_id=%d tiers=%d", regionID, len(items)))
|
||||
response.OK(c, gin.H{"regionId": regionID, "tiers": items})
|
||||
}
|
||||
|
||||
func (h *Handler) CreateAgency(c *gin.Context) {
|
||||
var req createAgencyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@ -250,6 +303,26 @@ func (h *Handler) CloseAgency(c *gin.Context) {
|
||||
response.OK(c, agency)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteAgency(c *gin.Context) {
|
||||
agencyID, ok := parseInt64ID(c, "agency_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req agencyDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "删除 Agency 参数不正确")
|
||||
return
|
||||
}
|
||||
agency, err := h.service.DeleteAgency(c.Request.Context(), adminActorID(c), agencyID, middleware.CurrentRequestID(c), req)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
writeHostOrgAuditLog(c, h.audit, "delete-agency", "agencies", agency.AgencyID,
|
||||
fmt.Sprintf("command_id=%s agency_id=%d status=%s reason=%q", strings.TrimSpace(req.CommandID), agency.AgencyID, agency.Status, strings.TrimSpace(req.Reason)))
|
||||
response.OK(c, agency)
|
||||
}
|
||||
|
||||
func (h *Handler) SetAgencyJoinEnabled(c *gin.Context) {
|
||||
agencyID, ok := parseInt64ID(c, "agency_id")
|
||||
if !ok {
|
||||
@ -285,10 +358,12 @@ func parseInt64ID(c *gin.Context, name string) (int64, bool) {
|
||||
func parseListQuery(c *gin.Context) (listQuery, bool) {
|
||||
options := shared.ListOptions(c)
|
||||
query := listQuery{
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
Keyword: options.Keyword,
|
||||
Status: options.Status,
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
Keyword: options.Keyword,
|
||||
Status: options.Status,
|
||||
SortBy: firstNonBlank(c.Query("sortBy"), c.Query("sort_by")),
|
||||
SortDirection: firstNonBlank(c.Query("sortDirection"), c.Query("sort_direction"), c.Query("order")),
|
||||
}
|
||||
var ok bool
|
||||
if query.RegionID, ok = parseOptionalInt64Query(c, "regionId", "region_id"); !ok {
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -26,15 +27,30 @@ type CoinSellerListItem struct {
|
||||
Status string `json:"status"`
|
||||
MerchantAssetType string `json:"merchantAssetType"`
|
||||
MerchantBalance int64 `json:"merchantBalance"`
|
||||
Contact string `json:"contact"`
|
||||
DisplayUserID string `json:"displayUserId"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
RegionID int64 `json:"regionId"`
|
||||
RegionName string `json:"regionName"`
|
||||
CreatedByUserID int64 `json:"createdByUserId"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
// TotalRechargeUSDTMicro 是币商 USDT 进货累计付款微单位,只统计 counts_as_seller_recharge 的库存记录。
|
||||
TotalRechargeUSDTMicro int64 `json:"totalRechargeUsdtMicro"`
|
||||
Contact string `json:"contact"`
|
||||
DisplayUserID string `json:"displayUserId"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
RegionID int64 `json:"regionId"`
|
||||
RegionName string `json:"regionName"`
|
||||
CreatedByUserID int64 `json:"createdByUserId"`
|
||||
CreatedAtMs int64 `json:"createdAtMs"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
type CoinSellerSalaryRateTier struct {
|
||||
RegionID int64 `json:"regionId,string"`
|
||||
MinUSDMinor int64 `json:"minUsdMinor"`
|
||||
MaxUSDMinor int64 `json:"maxUsdMinor"`
|
||||
MinUSD string `json:"minUsd"`
|
||||
MaxUSD string `json:"maxUsd"`
|
||||
CoinPerUSD int64 `json:"coinPerUsd"`
|
||||
Status string `json:"status"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
// ListBDProfiles 只读 user-service 的 BD 事实表,避免后台列表需求改动 user-service 发布物。
|
||||
@ -132,9 +148,73 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin
|
||||
if err := r.fillBDProfileCreators(ctx, items); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if role == "bd_leader" {
|
||||
if err := r.fillBDLeaderPositionAliases(ctx, appCode, items); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (r *Reader) GetBDLeader(ctx context.Context, userID int64) (*userclient.BDProfile, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errUserDBNotConfigured()
|
||||
}
|
||||
appCode := appctx.FromContext(ctx)
|
||||
item := &userclient.BDProfile{}
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT bp.user_id, bp.role, bp.region_id, COALESCE(bp.parent_leader_user_id, 0),
|
||||
bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms,
|
||||
COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''),
|
||||
COALESCE(u.avatar, ''), COALESCE(r.name, ''),
|
||||
COALESCE(creator.current_display_user_id, ''), COALESCE(creator.username, ''),
|
||||
COALESCE(creator.avatar, ''),
|
||||
(
|
||||
SELECT COUNT(1)
|
||||
FROM bd_profiles child
|
||||
WHERE child.app_code = bp.app_code
|
||||
AND child.role = 'bd'
|
||||
AND child.parent_leader_user_id = bp.user_id
|
||||
)
|
||||
FROM bd_profiles bp
|
||||
LEFT JOIN users u ON u.app_code = bp.app_code AND u.user_id = bp.user_id
|
||||
LEFT JOIN users creator ON creator.app_code = bp.app_code AND creator.user_id = bp.created_by_user_id
|
||||
LEFT JOIN regions r ON r.app_code = bp.app_code AND r.region_id = bp.region_id
|
||||
WHERE bp.app_code = ? AND bp.user_id = ? AND bp.role = 'bd_leader'
|
||||
`, appCode, userID).Scan(
|
||||
&item.UserID,
|
||||
&item.Role,
|
||||
&item.RegionID,
|
||||
&item.ParentLeaderUserID,
|
||||
&item.Status,
|
||||
&item.CreatedByUserID,
|
||||
&item.CreatedAtMs,
|
||||
&item.UpdatedAtMs,
|
||||
&item.DisplayUserID,
|
||||
&item.Username,
|
||||
&item.Avatar,
|
||||
&item.RegionName,
|
||||
&item.CreatedByDisplayUserID,
|
||||
&item.CreatedByUsername,
|
||||
&item.CreatedByAvatar,
|
||||
&item.SubBDCount,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("bd leader not found")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := []*userclient.BDProfile{item}
|
||||
if err := r.fillBDProfileCreators(ctx, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.fillBDLeaderPositionAliases(ctx, appCode, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (r *Reader) fillBDProfileCreators(ctx context.Context, items []*userclient.BDProfile) error {
|
||||
if r == nil || r.adminDB == nil || len(items) == 0 {
|
||||
return nil
|
||||
@ -192,6 +272,78 @@ func (r *Reader) fillBDProfileCreators(ctx context.Context, items []*userclient.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reader) fillBDLeaderPositionAliases(ctx context.Context, appCode string, items []*userclient.BDProfile) error {
|
||||
if r == nil || r.adminDB == nil || len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
ids := make([]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.UserID > 0 {
|
||||
ids = append(ids, item.UserID)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
args := append([]any{appCode}, ids...)
|
||||
rows, err := r.adminDB.QueryContext(ctx, `
|
||||
SELECT user_id, position_alias
|
||||
FROM admin_bd_leader_position_aliases
|
||||
WHERE app_code = ? AND user_id IN (`+sqlPlaceholders(len(ids))+`)
|
||||
`, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
aliases := make(map[int64]string, len(ids))
|
||||
for rows.Next() {
|
||||
var userID int64
|
||||
var alias string
|
||||
if err := rows.Scan(&userID, &alias); err != nil {
|
||||
return err
|
||||
}
|
||||
aliases[userID] = strings.TrimSpace(alias)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range items {
|
||||
item.PositionAlias = aliases[item.UserID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reader) SaveBDLeaderPositionAlias(ctx context.Context, userID int64, actorID int64, alias string) error {
|
||||
if r == nil || r.adminDB == nil {
|
||||
return fmt.Errorf("admin mysql is not configured")
|
||||
}
|
||||
if userID <= 0 {
|
||||
return fmt.Errorf("user_id is required")
|
||||
}
|
||||
appCode := appctx.FromContext(ctx)
|
||||
positionAlias := strings.TrimSpace(alias)
|
||||
if positionAlias == "" {
|
||||
// 空别名表示恢复 H5 配置表里的默认 admin 展示名,删除行比保留空值更清晰。
|
||||
_, err := r.adminDB.ExecContext(ctx, `
|
||||
DELETE FROM admin_bd_leader_position_aliases
|
||||
WHERE app_code = ? AND user_id = ?
|
||||
`, appCode, userID)
|
||||
return err
|
||||
}
|
||||
nowMs := time.Now().UnixMilli()
|
||||
_, err := r.adminDB.ExecContext(ctx, `
|
||||
INSERT INTO admin_bd_leader_position_aliases (
|
||||
app_code, user_id, position_alias, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
position_alias = VALUES(position_alias),
|
||||
updated_by_admin_id = VALUES(updated_by_admin_id),
|
||||
updated_at_ms = VALUES(updated_at_ms)
|
||||
`, appCode, userID, positionAlias, actorID, actorID, nowMs, nowMs)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Reader) DeleteBDLeader(ctx context.Context, userID int64) (*userclient.BDProfile, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errUserDBNotConfigured()
|
||||
@ -233,6 +385,12 @@ func (r *Reader) DeleteBDLeader(ctx context.Context, userID int64) (*userclient.
|
||||
if affected == 0 {
|
||||
return nil, fmt.Errorf("bd leader not found")
|
||||
}
|
||||
if r.adminDB != nil {
|
||||
_, _ = r.adminDB.ExecContext(ctx, `
|
||||
DELETE FROM admin_bd_leader_position_aliases
|
||||
WHERE app_code = ? AND user_id = ?
|
||||
`, appCode, userID)
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
@ -252,6 +410,9 @@ func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclie
|
||||
if query.Status != "" {
|
||||
whereSQL += " AND a.status = ?"
|
||||
args = append(args, query.Status)
|
||||
} else {
|
||||
whereSQL += " AND a.status <> ?"
|
||||
args = append(args, "deleted")
|
||||
}
|
||||
if query.RegionID > 0 {
|
||||
whereSQL += " AND a.region_id = ?"
|
||||
@ -328,6 +489,7 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user
|
||||
FROM host_profiles hp
|
||||
LEFT JOIN users u ON u.app_code = hp.app_code AND u.user_id = hp.user_id
|
||||
LEFT JOIN agencies a ON a.app_code = hp.app_code AND a.agency_id = hp.current_agency_id
|
||||
LEFT JOIN users agency_owner ON agency_owner.app_code = a.app_code AND agency_owner.user_id = a.owner_user_id
|
||||
LEFT JOIN regions r ON r.app_code = hp.app_code AND r.region_id = hp.region_id
|
||||
WHERE hp.app_code = ?`
|
||||
args := []any{appCode}
|
||||
@ -345,30 +507,39 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user
|
||||
}
|
||||
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
whereSQL += " AND (CAST(hp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ? OR a.name LIKE ? OR r.name LIKE ?)"
|
||||
args = append(args, like, like, like, like, like)
|
||||
whereSQL += " AND (CAST(hp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ? OR a.name LIKE ? OR agency_owner.current_display_user_id LIKE ? OR agency_owner.username LIKE ? OR r.name LIKE ?)"
|
||||
args = append(args, like, like, like, like, like, like, like)
|
||||
}
|
||||
|
||||
total, err := countRows(ctx, r.db, whereSQL, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
limitSQL := "LIMIT ? OFFSET ?"
|
||||
queryArgs := append(args, query.PageSize, offset(query.Page, query.PageSize))
|
||||
if query.SortBy == "diamond" {
|
||||
limitSQL = ""
|
||||
queryArgs = args
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT hp.user_id, hp.status, hp.region_id, COALESCE(hp.current_agency_id, 0),
|
||||
COALESCE(hp.current_membership_id, 0), hp.source,
|
||||
hp.first_became_host_at_ms, hp.created_at_ms, hp.updated_at_ms,
|
||||
COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''),
|
||||
COALESCE(u.avatar, ''), COALESCE(r.name, ''), COALESCE(a.name, '')
|
||||
COALESCE(u.avatar, ''), COALESCE(r.name, ''), COALESCE(a.name, ''),
|
||||
COALESCE(a.owner_user_id, 0), COALESCE(agency_owner.current_display_user_id, ''),
|
||||
COALESCE(agency_owner.username, ''), COALESCE(agency_owner.avatar, '')
|
||||
%s
|
||||
ORDER BY hp.created_at_ms DESC, hp.user_id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
||||
%s
|
||||
`, whereSQL, limitSQL), queryArgs...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]*userclient.HostProfile, 0, query.PageSize)
|
||||
userIDs := make([]int64, 0, query.PageSize)
|
||||
for rows.Next() {
|
||||
item := &userclient.HostProfile{}
|
||||
if err := rows.Scan(
|
||||
@ -386,12 +557,31 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user
|
||||
&item.Avatar,
|
||||
&item.RegionName,
|
||||
&item.CurrentAgencyName,
|
||||
&item.CurrentAgencyOwnerUserID,
|
||||
&item.CurrentAgencyOwnerDisplayUserID,
|
||||
&item.CurrentAgencyOwnerUsername,
|
||||
&item.CurrentAgencyOwnerAvatar,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items = append(items, item)
|
||||
userIDs = append(userIDs, item.UserID)
|
||||
}
|
||||
return items, total, rows.Err()
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
diamonds, err := r.hostPeriodDiamonds(ctx, appCode, userIDs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for _, item := range items {
|
||||
item.Diamond = diamonds[item.UserID]
|
||||
}
|
||||
if query.SortBy == "diamond" {
|
||||
sortHostProfilesByDiamond(items, query.SortDirection)
|
||||
items = paginateHostProfiles(items, query.Page, query.PageSize)
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
// ListCoinSellers 聚合 user-service 身份事实和 wallet-service 币商余额,只做后台展示读模型。
|
||||
@ -427,6 +617,13 @@ func (r *Reader) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinS
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
limitSQL := "LIMIT ? OFFSET ?"
|
||||
queryArgs := append(args, query.PageSize, offset(query.Page, query.PageSize))
|
||||
if isCoinSellerComputedSort(query.SortBy) {
|
||||
// 币商余额和累充 USDT 都来自 wallet-service 聚合,必须先读取所有匹配币商再排序分页,避免只排序当前页。
|
||||
limitSQL = ""
|
||||
queryArgs = args
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT csp.user_id, csp.status, csp.merchant_asset_type,
|
||||
COALESCE(csp.contact_info, ''),
|
||||
@ -436,8 +633,8 @@ func (r *Reader) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinS
|
||||
COALESCE(r.name, '')
|
||||
%s
|
||||
ORDER BY csp.created_at_ms DESC, csp.user_id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
||||
%s
|
||||
`, whereSQL, limitSQL), queryArgs...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@ -473,8 +670,17 @@ func (r *Reader) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinS
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rechargeTotals, err := r.coinSellerRechargeTotals(ctx, appCode, userIDs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for _, item := range items {
|
||||
item.MerchantBalance = balances[item.UserID]
|
||||
item.TotalRechargeUSDTMicro = rechargeTotals[item.UserID]
|
||||
}
|
||||
if isCoinSellerComputedSort(query.SortBy) {
|
||||
sortCoinSellerListItems(items, query.SortBy, query.SortDirection)
|
||||
items = paginateCoinSellerListItems(items, query.Page, query.PageSize)
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
@ -494,6 +700,124 @@ func (r *Reader) UpdateCoinSellerContact(ctx context.Context, userID int64, cont
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Reader) ListCoinSellerSalaryRates(ctx context.Context, regionID int64) ([]CoinSellerSalaryRateTier, error) {
|
||||
if r == nil || r.walletDB == nil {
|
||||
return nil, fmt.Errorf("wallet mysql is not configured")
|
||||
}
|
||||
if regionID <= 0 {
|
||||
return nil, fmt.Errorf("region_id is required")
|
||||
}
|
||||
if err := r.ensureCoinSellerSalaryRateTable(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := r.walletDB.QueryContext(ctx, `
|
||||
SELECT region_id, min_usd_minor, max_usd_minor, coin_per_usd, status, sort_order, updated_at_ms
|
||||
FROM coin_seller_salary_exchange_rate_tiers
|
||||
WHERE app_code = ? AND region_id = ?
|
||||
ORDER BY sort_order ASC, min_usd_minor ASC, tier_id ASC
|
||||
`, appctx.FromContext(ctx), regionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]CoinSellerSalaryRateTier, 0)
|
||||
for rows.Next() {
|
||||
var item CoinSellerSalaryRateTier
|
||||
if err := rows.Scan(&item.RegionID, &item.MinUSDMinor, &item.MaxUSDMinor, &item.CoinPerUSD, &item.Status, &item.SortOrder, &item.UpdatedAtMs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Enabled = item.Status == "active"
|
||||
item.MinUSD = formatUSDMinor(item.MinUSDMinor)
|
||||
item.MaxUSD = formatUSDMinor(item.MaxUSDMinor)
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *Reader) ReplaceCoinSellerSalaryRates(ctx context.Context, regionID int64, actorID int64, tiers []CoinSellerSalaryRateTier) ([]CoinSellerSalaryRateTier, error) {
|
||||
if r == nil || r.walletDB == nil {
|
||||
return nil, fmt.Errorf("wallet mysql is not configured")
|
||||
}
|
||||
if regionID <= 0 {
|
||||
return nil, fmt.Errorf("region_id is required")
|
||||
}
|
||||
if err := r.ensureCoinSellerSalaryRateTable(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx, err := r.walletDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
appCode := appctx.FromContext(ctx)
|
||||
nowMs := time.Now().UnixMilli()
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM coin_seller_salary_exchange_rate_tiers
|
||||
WHERE app_code = ? AND region_id = ?
|
||||
`, appCode, regionID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for index, tier := range tiers {
|
||||
status := strings.ToLower(strings.TrimSpace(tier.Status))
|
||||
if status == "" {
|
||||
if tier.Enabled {
|
||||
status = "active"
|
||||
} else {
|
||||
status = "disabled"
|
||||
}
|
||||
}
|
||||
if status != "active" && status != "disabled" {
|
||||
return nil, fmt.Errorf("rate status is invalid")
|
||||
}
|
||||
sortOrder := tier.SortOrder
|
||||
if sortOrder == 0 {
|
||||
sortOrder = (index + 1) * 10
|
||||
}
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO coin_seller_salary_exchange_rate_tiers (
|
||||
app_code, region_id, min_usd_minor, max_usd_minor, coin_per_usd,
|
||||
status, sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, appCode, regionID, tier.MinUSDMinor, tier.MaxUSDMinor, tier.CoinPerUSD, status, sortOrder, actorID, actorID, nowMs, nowMs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.ListCoinSellerSalaryRates(ctx, regionID)
|
||||
}
|
||||
|
||||
func (r *Reader) ensureCoinSellerSalaryRateTable(ctx context.Context) error {
|
||||
if _, err := r.walletDB.ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS coin_seller_salary_exchange_rate_tiers (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
region_id BIGINT NOT NULL COMMENT '币商所属区域 ID',
|
||||
tier_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '区间 ID',
|
||||
min_usd_minor BIGINT NOT NULL COMMENT '起始美元金额,单位美分,包含',
|
||||
max_usd_minor BIGINT NOT NULL COMMENT '结束美元金额,单位美分,包含',
|
||||
coin_per_usd BIGINT NOT NULL COMMENT '每 1 USD 工资可兑换的币商专用金币数量',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT '状态:active/disabled',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
|
||||
created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
|
||||
updated_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新人用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (tier_id),
|
||||
KEY idx_coin_seller_salary_rates_region (app_code, region_id, status, min_usd_minor, max_usd_minor),
|
||||
KEY idx_coin_seller_salary_rates_sort (app_code, region_id, status, sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='币商工资兑换比例区间表'`); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reader) ensureCoinSellerContactColumn(ctx context.Context) error {
|
||||
var count int
|
||||
if err := r.db.QueryRowContext(ctx, `
|
||||
@ -545,6 +869,152 @@ func (r *Reader) coinSellerBalances(ctx context.Context, appCode string, userIDs
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
// coinSellerRechargeTotals 从币商进货专表汇总 USDT 付款,金币补偿不会计入累充口径。
|
||||
func (r *Reader) coinSellerRechargeTotals(ctx context.Context, appCode string, userIDs []int64) (map[int64]int64, error) {
|
||||
result := make(map[int64]int64, len(userIDs))
|
||||
if r == nil || r.walletDB == nil || len(userIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
placeholders := sqlPlaceholders(len(userIDs))
|
||||
args := []any{appCode, paidCurrencyUSDT}
|
||||
for _, userID := range userIDs {
|
||||
args = append(args, userID)
|
||||
}
|
||||
rows, err := r.walletDB.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT seller_user_id, COALESCE(SUM(paid_amount_micro), 0)
|
||||
FROM coin_seller_stock_records
|
||||
WHERE app_code = ?
|
||||
AND counts_as_seller_recharge = TRUE
|
||||
AND paid_currency_code = ?
|
||||
AND seller_user_id IN (%s)
|
||||
GROUP BY seller_user_id
|
||||
`, placeholders), args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var userID int64
|
||||
var amount int64
|
||||
if err := rows.Scan(&userID, &amount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[userID] = amount
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (r *Reader) hostPeriodDiamonds(ctx context.Context, appCode string, userIDs []int64) (map[int64]int64, error) {
|
||||
result := make(map[int64]int64, len(userIDs))
|
||||
if r == nil || r.walletDB == nil || len(userIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
placeholders := strings.TrimRight(strings.Repeat("?,", len(userIDs)), ",")
|
||||
args := []any{appCode, time.Now().UTC().Format("2006-01")}
|
||||
for _, userID := range userIDs {
|
||||
args = append(args, userID)
|
||||
}
|
||||
rows, err := r.walletDB.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT user_id, total_diamonds
|
||||
FROM host_period_diamond_accounts
|
||||
WHERE app_code = ? AND cycle_key = ? AND user_id IN (%s)
|
||||
`, placeholders), args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var userID int64
|
||||
var amount int64
|
||||
if err := rows.Scan(&userID, &amount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[userID] = amount
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func isCoinSellerComputedSort(sortBy string) bool {
|
||||
return sortBy == sortByMerchantBalance || sortBy == sortByTotalRechargeUSDT
|
||||
}
|
||||
|
||||
// sortCoinSellerListItems 只处理 wallet 聚合字段排序,基础身份字段仍使用 SQL 默认创建时间排序。
|
||||
func sortCoinSellerListItems(items []*CoinSellerListItem, sortBy string, direction string) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
left := coinSellerSortValue(items[i], sortBy)
|
||||
right := coinSellerSortValue(items[j], sortBy)
|
||||
if left == right {
|
||||
return false
|
||||
}
|
||||
if direction == "asc" {
|
||||
return left < right
|
||||
}
|
||||
return left > right
|
||||
})
|
||||
}
|
||||
|
||||
func coinSellerSortValue(item *CoinSellerListItem, sortBy string) int64 {
|
||||
if item == nil {
|
||||
return 0
|
||||
}
|
||||
switch sortBy {
|
||||
case sortByTotalRechargeUSDT:
|
||||
return item.TotalRechargeUSDTMicro
|
||||
case sortByMerchantBalance:
|
||||
return item.MerchantBalance
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func sortHostProfilesByDiamond(items []*userclient.HostProfile, direction string) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if items[i].Diamond == items[j].Diamond {
|
||||
return false
|
||||
}
|
||||
if direction == "asc" {
|
||||
return items[i].Diamond < items[j].Diamond
|
||||
}
|
||||
return items[i].Diamond > items[j].Diamond
|
||||
})
|
||||
}
|
||||
|
||||
func paginateCoinSellerListItems(items []*CoinSellerListItem, page int, pageSize int) []*CoinSellerListItem {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
start := (page - 1) * pageSize
|
||||
if start >= len(items) {
|
||||
return []*CoinSellerListItem{}
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
return items[start:end]
|
||||
}
|
||||
|
||||
func paginateHostProfiles(items []*userclient.HostProfile, page int, pageSize int) []*userclient.HostProfile {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
start := (page - 1) * pageSize
|
||||
if start >= len(items) {
|
||||
return []*userclient.HostProfile{}
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
return items[start:end]
|
||||
}
|
||||
|
||||
func sqlPlaceholders(count int) string {
|
||||
if count <= 0 {
|
||||
return ""
|
||||
|
||||
@ -9,19 +9,21 @@ type listQuery struct {
|
||||
AgencyID int64
|
||||
ParentBDUserID int64
|
||||
ParentLeaderUserID int64
|
||||
SortBy string
|
||||
SortDirection string
|
||||
}
|
||||
|
||||
type createBDLeaderRequest struct {
|
||||
CommandID string `json:"commandId" binding:"required"`
|
||||
TargetUserID int64 `json:"targetUserId" binding:"required"`
|
||||
RegionID int64 `json:"regionId" binding:"required"`
|
||||
Reason string `json:"reason"`
|
||||
CommandID string `json:"commandId" binding:"required"`
|
||||
TargetUserID int64 `json:"targetUserId" binding:"required"`
|
||||
PositionAlias string `json:"positionAlias"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type createBDRequest struct {
|
||||
CommandID string `json:"commandId" binding:"required"`
|
||||
TargetUserID int64 `json:"targetUserId" binding:"required"`
|
||||
ParentLeaderUserID int64 `json:"parentLeaderUserId" binding:"required"`
|
||||
ParentLeaderUserID int64 `json:"parentLeaderUserId"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
@ -31,6 +33,12 @@ type bdStatusRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type bdLeaderPositionAliasRequest struct {
|
||||
CommandID string `json:"commandId" binding:"required"`
|
||||
PositionAlias string `json:"positionAlias"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type createCoinSellerRequest struct {
|
||||
CommandID string `json:"commandId" binding:"required"`
|
||||
Contact string `json:"contact"`
|
||||
@ -55,13 +63,27 @@ type coinSellerStockCreditRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type replaceCoinSellerSalaryRatesRequest struct {
|
||||
Tiers []coinSellerSalaryRateTierRequest `json:"tiers" binding:"required"`
|
||||
}
|
||||
|
||||
type coinSellerSalaryRateTierRequest struct {
|
||||
MinUSD string `json:"minUsd"`
|
||||
MaxUSD string `json:"maxUsd"`
|
||||
MinUSDMinor int64 `json:"minUsdMinor"`
|
||||
MaxUSDMinor int64 `json:"maxUsdMinor"`
|
||||
CoinPerUSD int64 `json:"coinPerUsd" binding:"required"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Status string `json:"status"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
type createAgencyRequest struct {
|
||||
CommandID string `json:"commandId" binding:"required"`
|
||||
OwnerUserID int64 `json:"ownerUserId" binding:"required"`
|
||||
ParentBDUserID int64 `json:"parentBdUserId" binding:"required"`
|
||||
ParentBDUserID int64 `json:"parentBdUserId"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
JoinEnabled *bool `json:"joinEnabled" binding:"required"`
|
||||
MaxHosts int32 `json:"maxHosts"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
@ -75,3 +97,8 @@ type agencyCloseRequest struct {
|
||||
CommandID string `json:"commandId" binding:"required"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type agencyDeleteRequest struct {
|
||||
CommandID string `json:"commandId" binding:"required"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
protected.GET("/admin/bd-leaders", middleware.RequirePermission("bd:view"), h.ListBDLeaders)
|
||||
protected.POST("/admin/bd-leaders", middleware.RequirePermission("bd:create"), h.CreateBDLeader)
|
||||
protected.PATCH("/admin/bd-leaders/:user_id/status", middleware.RequirePermission("bd:update"), h.SetBDStatus)
|
||||
protected.PATCH("/admin/bd-leaders/:user_id/position-alias", middleware.RequirePermission("bd:update"), h.UpdateBDLeaderPositionAlias)
|
||||
protected.DELETE("/admin/bd-leaders/:user_id", middleware.RequirePermission("bd:update"), h.DeleteBDLeader)
|
||||
protected.GET("/admin/bds", middleware.RequirePermission("bd:view"), h.ListBDs)
|
||||
protected.POST("/admin/bds", middleware.RequirePermission("bd:create"), h.CreateBD)
|
||||
@ -17,10 +18,13 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
protected.GET("/admin/agencies", middleware.RequirePermission("agency:view"), h.ListAgencies)
|
||||
protected.POST("/admin/agencies", middleware.RequirePermission("agency:create"), h.CreateAgency)
|
||||
protected.POST("/admin/agencies/:agency_id/close", middleware.RequirePermission("agency:status"), h.CloseAgency)
|
||||
protected.POST("/admin/agencies/:agency_id/delete", middleware.RequirePermission("agency:delete"), h.DeleteAgency)
|
||||
protected.POST("/admin/agencies/:agency_id/join-enabled", middleware.RequirePermission("agency:status"), h.SetAgencyJoinEnabled)
|
||||
protected.GET("/admin/hosts", middleware.RequirePermission("host:view"), h.ListHosts)
|
||||
protected.GET("/admin/coin-sellers", middleware.RequirePermission("coin-seller:view"), h.ListCoinSellers)
|
||||
protected.POST("/admin/coin-sellers", middleware.RequirePermission("coin-seller:create"), h.CreateCoinSeller)
|
||||
protected.PATCH("/admin/coin-sellers/:user_id/status", middleware.RequirePermission("coin-seller:update"), h.SetCoinSellerStatus)
|
||||
protected.POST("/admin/coin-sellers/:user_id/stock-credits", middleware.RequirePermission("coin-seller:stock-credit"), h.CreditCoinSellerStock)
|
||||
protected.GET("/admin/coin-seller-salary-rates/:region_id", middleware.RequirePermission("coin-seller:exchange-rate"), h.GetCoinSellerSalaryRates)
|
||||
protected.PUT("/admin/coin-seller-salary-rates/:region_id", middleware.RequirePermission("coin-seller:exchange-rate"), h.ReplaceCoinSellerSalaryRates)
|
||||
}
|
||||
|
||||
@ -18,6 +18,10 @@ const (
|
||||
coinSellerStockTypePurchase = "usdt_purchase"
|
||||
coinSellerStockTypeCompensate = "coin_compensation"
|
||||
paidCurrencyUSDT = "USDT"
|
||||
sortByDiamond = "diamond"
|
||||
sortByMerchantBalance = "merchant_balance"
|
||||
sortByTotalRechargeUSDT = "total_recharge_usdt"
|
||||
bdLeaderPositionAliasMaxRunes = 64
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@ -55,21 +59,111 @@ func (s *Service) ListCoinSellers(ctx context.Context, query listQuery) ([]*Coin
|
||||
return s.reader.ListCoinSellers(ctx, query)
|
||||
}
|
||||
|
||||
func (s *Service) GetCoinSellerSalaryRates(ctx context.Context, regionID int64) ([]CoinSellerSalaryRateTier, error) {
|
||||
return s.reader.ListCoinSellerSalaryRates(ctx, regionID)
|
||||
}
|
||||
|
||||
func (s *Service) ReplaceCoinSellerSalaryRates(ctx context.Context, actorID int64, regionID int64, req replaceCoinSellerSalaryRatesRequest) ([]CoinSellerSalaryRateTier, error) {
|
||||
tiers := make([]CoinSellerSalaryRateTier, 0, len(req.Tiers))
|
||||
for index, item := range req.Tiers {
|
||||
minUSDMinor, err := salaryRateUSDMinor(item.MinUSDMinor, item.MinUSD)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("min_usd is invalid")
|
||||
}
|
||||
maxUSDMinor, err := salaryRateUSDMinor(item.MaxUSDMinor, item.MaxUSD)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("max_usd is invalid")
|
||||
}
|
||||
enabled := item.Status == "active"
|
||||
if item.Enabled != nil {
|
||||
enabled = *item.Enabled
|
||||
}
|
||||
status := strings.ToLower(strings.TrimSpace(item.Status))
|
||||
if status == "" {
|
||||
if enabled {
|
||||
status = "active"
|
||||
} else {
|
||||
status = "disabled"
|
||||
}
|
||||
}
|
||||
if status != "active" && status != "disabled" {
|
||||
return nil, fmt.Errorf("status is invalid")
|
||||
}
|
||||
if minUSDMinor <= 0 || maxUSDMinor < minUSDMinor {
|
||||
return nil, fmt.Errorf("usd range is invalid")
|
||||
}
|
||||
if item.CoinPerUSD <= 0 || item.CoinPerUSD%100 != 0 {
|
||||
return nil, fmt.Errorf("coin_per_usd must be a positive multiple of 100")
|
||||
}
|
||||
sortOrder := item.SortOrder
|
||||
if sortOrder == 0 {
|
||||
sortOrder = (index + 1) * 10
|
||||
}
|
||||
tiers = append(tiers, CoinSellerSalaryRateTier{
|
||||
RegionID: regionID,
|
||||
MinUSDMinor: minUSDMinor,
|
||||
MaxUSDMinor: maxUSDMinor,
|
||||
MinUSD: formatUSDMinor(minUSDMinor),
|
||||
MaxUSD: formatUSDMinor(maxUSDMinor),
|
||||
CoinPerUSD: item.CoinPerUSD,
|
||||
Status: status,
|
||||
Enabled: status == "active",
|
||||
SortOrder: sortOrder,
|
||||
})
|
||||
}
|
||||
if err := validateCoinSellerSalaryRateTiers(tiers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.reader.ReplaceCoinSellerSalaryRates(ctx, regionID, actorID, tiers)
|
||||
}
|
||||
|
||||
func (s *Service) CreateBDLeader(ctx context.Context, actorID int64, requestID string, req createBDLeaderRequest) (*userclient.BDProfile, error) {
|
||||
targetUserID, err := s.resolveDisplayUserID(ctx, requestID, req.TargetUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// BD Leader 创建时后台选择的区域是显式运营归属,由 user-service 在同一事务里更新 users.region_id 和 bd_profiles.region_id。
|
||||
return s.userClient.CreateBDLeader(ctx, userclient.CreateBDLeaderRequest{
|
||||
positionAlias, err := normalizeBDLeaderPositionAlias(req.PositionAlias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// BD Leader 区域由目标用户当前 users.region_id 派生;后台不再传区域,避免创建角色时顺手迁移用户国家/区域。
|
||||
profile, err := s.userClient.CreateBDLeader(ctx, userclient.CreateBDLeaderRequest{
|
||||
RequestID: requestID,
|
||||
Caller: "hyapp-admin-server",
|
||||
CommandID: strings.TrimSpace(req.CommandID),
|
||||
AdminUserID: actorID,
|
||||
RegionID: req.RegionID,
|
||||
TargetUserID: targetUserID,
|
||||
Reason: strings.TrimSpace(req.Reason),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 职位别名只影响 Flutter H5 配置里 admin 入口的展示名,不参与 user-service 的身份创建事务,避免把后台展示字段扩散到 App 身份主协议。
|
||||
if err := s.reader.SaveBDLeaderPositionAlias(ctx, targetUserID, actorID, positionAlias); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile.PositionAlias = positionAlias
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateBDLeaderPositionAlias(ctx context.Context, actorID int64, targetUserID int64, req bdLeaderPositionAliasRequest) (*userclient.BDProfile, error) {
|
||||
if targetUserID <= 0 {
|
||||
return nil, fmt.Errorf("target_user_id is required")
|
||||
}
|
||||
positionAlias, err := normalizeBDLeaderPositionAlias(req.PositionAlias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile, err := s.reader.GetBDLeader(ctx, targetUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 行内编辑只修改后台维护的展示别名;先确认 BD Leader 身份存在,再写覆盖表,避免产生无法在列表里追踪的孤立别名。
|
||||
if err := s.reader.SaveBDLeaderPositionAlias(ctx, targetUserID, actorID, positionAlias); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile.PositionAlias = positionAlias
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateBD(ctx context.Context, actorID int64, requestID string, req createBDRequest) (*userclient.BDProfile, error) {
|
||||
@ -77,9 +171,16 @@ func (s *Service) CreateBD(ctx context.Context, actorID int64, requestID string,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentLeaderUserID, err := s.resolveDisplayUserID(ctx, requestID, req.ParentLeaderUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
parentLeaderUserID := int64(0)
|
||||
if req.ParentLeaderUserID < 0 {
|
||||
return nil, fmt.Errorf("parent_leader_user_id must not be negative")
|
||||
}
|
||||
if req.ParentLeaderUserID > 0 {
|
||||
// 父级 Leader 是可选关系;传入短 ID 时才解析成内部 user_id,未传时保留 0 表示独立 BD。
|
||||
parentLeaderUserID, err = s.resolveDisplayUserID(ctx, requestID, req.ParentLeaderUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s.userClient.CreateBD(ctx, userclient.CreateBDRequest{
|
||||
RequestID: requestID,
|
||||
@ -202,10 +303,23 @@ func (s *Service) CreditCoinSellerStock(ctx context.Context, actorID int64, sell
|
||||
if profile == nil || profile.Status != coinSellerStatusActive || profile.MerchantAssetType != coinSellerMerchantAssetType {
|
||||
return nil, fmt.Errorf("target user is not an active coin seller")
|
||||
}
|
||||
seller, err := s.userClient.GetUser(ctx, userclient.GetUserRequest{
|
||||
RequestID: requestID,
|
||||
Caller: "hyapp-admin-server",
|
||||
UserID: sellerUserID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if seller == nil || seller.CountryID <= 0 || seller.RegionID <= 0 {
|
||||
return nil, fmt.Errorf("coin seller country and region are required")
|
||||
}
|
||||
|
||||
return s.walletClient.AdminCreditCoinSellerStock(ctx, &walletv1.AdminCreditCoinSellerStockRequest{
|
||||
CommandId: commandID,
|
||||
SellerUserId: sellerUserID,
|
||||
SellerCountryId: seller.CountryID,
|
||||
SellerRegionId: seller.RegionID,
|
||||
StockType: stockType,
|
||||
CoinAmount: req.CoinAmount,
|
||||
PaidCurrencyCode: paidCurrencyCode,
|
||||
@ -223,9 +337,16 @@ func (s *Service) CreateAgency(ctx context.Context, actorID int64, requestID str
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentBDUserID, err := s.resolveDisplayUserID(ctx, requestID, req.ParentBDUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
parentBDUserID := int64(0)
|
||||
if req.ParentBDUserID < 0 {
|
||||
return nil, fmt.Errorf("parent_bd_user_id must not be negative")
|
||||
}
|
||||
if req.ParentBDUserID > 0 {
|
||||
// 父级 BD 是可选关系;传入短 ID 时才解析成内部 user_id,未传时保留 0 表示独立 Agency。
|
||||
parentBDUserID, err = s.resolveDisplayUserID(ctx, requestID, req.ParentBDUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
joinEnabled := false
|
||||
if req.JoinEnabled != nil {
|
||||
@ -240,7 +361,6 @@ func (s *Service) CreateAgency(ctx context.Context, actorID int64, requestID str
|
||||
ParentBDUserID: parentBDUserID,
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
JoinEnabled: joinEnabled,
|
||||
MaxHosts: req.MaxHosts,
|
||||
Reason: strings.TrimSpace(req.Reason),
|
||||
})
|
||||
}
|
||||
@ -256,6 +376,17 @@ func (s *Service) CloseAgency(ctx context.Context, actorID int64, agencyID int64
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) DeleteAgency(ctx context.Context, actorID int64, agencyID int64, requestID string, req agencyDeleteRequest) (*userclient.Agency, error) {
|
||||
return s.userClient.DeleteAgency(ctx, userclient.DeleteAgencyRequest{
|
||||
RequestID: requestID,
|
||||
Caller: "hyapp-admin-server",
|
||||
CommandID: strings.TrimSpace(req.CommandID),
|
||||
AdminUserID: actorID,
|
||||
AgencyID: agencyID,
|
||||
Reason: strings.TrimSpace(req.Reason),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) resolveDisplayUserID(ctx context.Context, requestID string, displayUserID int64) (int64, error) {
|
||||
if displayUserID <= 0 {
|
||||
return 0, fmt.Errorf("display_user_id is required")
|
||||
@ -274,6 +405,14 @@ func (s *Service) resolveDisplayUserID(ctx context.Context, requestID string, di
|
||||
return identity.UserID, nil
|
||||
}
|
||||
|
||||
func normalizeBDLeaderPositionAlias(value string) (string, error) {
|
||||
positionAlias := strings.TrimSpace(value)
|
||||
if len([]rune(positionAlias)) > bdLeaderPositionAliasMaxRunes {
|
||||
return "", fmt.Errorf("position_alias must not exceed %d characters", bdLeaderPositionAliasMaxRunes)
|
||||
}
|
||||
return positionAlias, nil
|
||||
}
|
||||
|
||||
func (s *Service) SetAgencyJoinEnabled(ctx context.Context, actorID int64, agencyID int64, requestID string, req agencyJoinEnabledRequest) (*userclient.Agency, error) {
|
||||
joinEnabled := false
|
||||
if req.JoinEnabled != nil {
|
||||
@ -326,6 +465,68 @@ func parseUSDTAmountMicro(raw string) (int64, error) {
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
func salaryRateUSDMinor(minor int64, raw string) (int64, error) {
|
||||
if minor > 0 {
|
||||
return minor, nil
|
||||
}
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return 0, fmt.Errorf("usd amount is required")
|
||||
}
|
||||
parts := strings.Split(raw, ".")
|
||||
if len(parts) > 2 || parts[0] == "" {
|
||||
return 0, fmt.Errorf("usd amount is invalid")
|
||||
}
|
||||
digits := parts[0]
|
||||
fraction := ""
|
||||
if len(parts) == 2 {
|
||||
fraction = parts[1]
|
||||
if len(fraction) > 2 {
|
||||
return 0, fmt.Errorf("usd amount supports up to 2 decimals")
|
||||
}
|
||||
}
|
||||
digits += fraction + strings.Repeat("0", 2-len(fraction))
|
||||
var amount int64
|
||||
const maxInt64 = int64(^uint64(0) >> 1)
|
||||
for _, char := range digits {
|
||||
if char < '0' || char > '9' {
|
||||
return 0, fmt.Errorf("usd amount is invalid")
|
||||
}
|
||||
next := int64(char - '0')
|
||||
if amount > (maxInt64-next)/10 {
|
||||
return 0, fmt.Errorf("usd amount is too large")
|
||||
}
|
||||
amount = amount*10 + next
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
func validateCoinSellerSalaryRateTiers(tiers []CoinSellerSalaryRateTier) error {
|
||||
active := make([]CoinSellerSalaryRateTier, 0, len(tiers))
|
||||
for _, tier := range tiers {
|
||||
if tier.Status == "active" {
|
||||
active = append(active, tier)
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(active); i++ {
|
||||
for j := i + 1; j < len(active); j++ {
|
||||
if active[i].MinUSDMinor <= active[j].MaxUSDMinor && active[j].MinUSDMinor <= active[i].MaxUSDMinor {
|
||||
return fmt.Errorf("active usd ranges must not overlap")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatUSDMinor(value int64) string {
|
||||
sign := ""
|
||||
if value < 0 {
|
||||
sign = "-"
|
||||
value = -value
|
||||
}
|
||||
return fmt.Sprintf("%s%d.%02d", sign, value/100, value%100)
|
||||
}
|
||||
|
||||
func normalizeListQuery(query listQuery) listQuery {
|
||||
if query.Page < 1 {
|
||||
query.Page = 1
|
||||
@ -338,6 +539,18 @@ func normalizeListQuery(query listQuery) listQuery {
|
||||
}
|
||||
query.Keyword = strings.TrimSpace(query.Keyword)
|
||||
query.Status = strings.ToLower(strings.TrimSpace(query.Status))
|
||||
query.SortBy = strings.ToLower(strings.TrimSpace(query.SortBy))
|
||||
switch query.SortBy {
|
||||
case sortByDiamond, sortByMerchantBalance, sortByTotalRechargeUSDT:
|
||||
default:
|
||||
query.SortBy = ""
|
||||
query.SortDirection = ""
|
||||
return query
|
||||
}
|
||||
query.SortDirection = strings.ToLower(strings.TrimSpace(query.SortDirection))
|
||||
if query.SortDirection != "asc" {
|
||||
query.SortDirection = "desc"
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
|
||||
32
server/admin/internal/modules/hostorg/service_test.go
Normal file
32
server/admin/internal/modules/hostorg/service_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package hostorg
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestNormalizeListQueryAllowsCoinSellerComputedSorts 锁定币商余额和累充 USDT 排序字段会被后台列表查询接受。
|
||||
func TestNormalizeListQueryAllowsCoinSellerComputedSorts(t *testing.T) {
|
||||
for _, sortBy := range []string{sortByMerchantBalance, sortByTotalRechargeUSDT} {
|
||||
query := normalizeListQuery(listQuery{Page: 1, PageSize: 50, SortBy: sortBy, SortDirection: "asc"})
|
||||
if query.SortBy != sortBy || query.SortDirection != "asc" {
|
||||
t.Fatalf("expected sort %s asc to be kept, got sort_by=%q direction=%q", sortBy, query.SortBy, query.SortDirection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSortCoinSellerListItemsComputedFields 验证 wallet 聚合字段排序只改变展示顺序,不改动币商身份数据。
|
||||
func TestSortCoinSellerListItemsComputedFields(t *testing.T) {
|
||||
items := []*CoinSellerListItem{
|
||||
{UserID: 1, MerchantBalance: 100, TotalRechargeUSDTMicro: 30_000_000},
|
||||
{UserID: 2, MerchantBalance: 300, TotalRechargeUSDTMicro: 10_000_000},
|
||||
{UserID: 3, MerchantBalance: 200, TotalRechargeUSDTMicro: 20_000_000},
|
||||
}
|
||||
|
||||
sortCoinSellerListItems(items, sortByMerchantBalance, "desc")
|
||||
if got := []int64{items[0].UserID, items[1].UserID, items[2].UserID}; got[0] != 2 || got[1] != 3 || got[2] != 1 {
|
||||
t.Fatalf("merchant balance desc order mismatch: %v", got)
|
||||
}
|
||||
|
||||
sortCoinSellerListItems(items, sortByTotalRechargeUSDT, "asc")
|
||||
if got := []int64{items[0].UserID, items[1].UserID, items[2].UserID}; got[0] != 2 || got[1] != 3 || got[2] != 1 {
|
||||
t.Fatalf("total recharge usdt asc order mismatch: %v", got)
|
||||
}
|
||||
}
|
||||
@ -147,7 +147,7 @@ func (h *Handler) UpsertLuckyGiftConfig(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
item := configFromProto(resp.GetConfig())
|
||||
shared.OperationLogWithResourceID(c, h.audit, "upsert-lucky-gift-config", "lucky_gift_rules", item.PoolID, "success", "")
|
||||
shared.OperationLogWithResourceID(c, h.audit, "upsert-lucky-gift-config", "lucky_gift_rule_versions", item.PoolID, "success", "")
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
|
||||
@ -9,24 +9,36 @@ import (
|
||||
)
|
||||
|
||||
type rechargeBillDTO struct {
|
||||
AppCode string `json:"appCode"`
|
||||
TransactionID string `json:"transactionId"`
|
||||
CommandID string `json:"commandId"`
|
||||
RechargeType string `json:"rechargeType"`
|
||||
Status string `json:"status"`
|
||||
ExternalRef string `json:"externalRef"`
|
||||
UserID int64 `json:"userId"`
|
||||
SellerUserID int64 `json:"sellerUserId"`
|
||||
SellerRegionID int64 `json:"sellerRegionId"`
|
||||
TargetRegionID int64 `json:"targetRegionId"`
|
||||
PolicyID int64 `json:"policyId"`
|
||||
PolicyVersion string `json:"policyVersion"`
|
||||
CurrencyCode string `json:"currencyCode"`
|
||||
CoinAmount int64 `json:"coinAmount"`
|
||||
USDMinorAmount int64 `json:"usdMinorAmount"`
|
||||
ExchangeCoinAmount int64 `json:"exchangeCoinAmount"`
|
||||
ExchangeUSDMinorAmount int64 `json:"exchangeUsdMinorAmount"`
|
||||
CreatedAtMS int64 `json:"createdAtMs"`
|
||||
AppCode string `json:"appCode"`
|
||||
TransactionID string `json:"transactionId"`
|
||||
CommandID string `json:"commandId"`
|
||||
RechargeType string `json:"rechargeType"`
|
||||
Status string `json:"status"`
|
||||
ExternalRef string `json:"externalRef"`
|
||||
UserID int64 `json:"userId"`
|
||||
SellerUserID int64 `json:"sellerUserId"`
|
||||
SellerRegionID int64 `json:"sellerRegionId"`
|
||||
TargetRegionID int64 `json:"targetRegionId"`
|
||||
PolicyID int64 `json:"policyId"`
|
||||
PolicyVersion string `json:"policyVersion"`
|
||||
CurrencyCode string `json:"currencyCode"`
|
||||
CoinAmount int64 `json:"coinAmount"`
|
||||
USDMinorAmount int64 `json:"usdMinorAmount"`
|
||||
ExchangeCoinAmount int64 `json:"exchangeCoinAmount"`
|
||||
ExchangeUSDMinorAmount int64 `json:"exchangeUsdMinorAmount"`
|
||||
CreatedAtMS int64 `json:"createdAtMs"`
|
||||
User rechargeBillUserDTO `json:"user"`
|
||||
Seller rechargeBillUserDTO `json:"seller"`
|
||||
}
|
||||
|
||||
type rechargeBillUserDTO struct {
|
||||
UserID string `json:"userId"`
|
||||
DisplayUserID string `json:"displayUserId"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
CountryCode string `json:"countryCode"`
|
||||
CountryName string `json:"countryName"`
|
||||
CountryDisplayName string `json:"countryDisplayName"`
|
||||
}
|
||||
|
||||
type rechargeProductDTO struct {
|
||||
@ -81,6 +93,57 @@ func rechargeBillFromProto(item *walletv1.RechargeBill) rechargeBillDTO {
|
||||
}
|
||||
}
|
||||
|
||||
// collectRechargeBillUserIDs 把付款用户和币商用户放进同一个去重列表;后续只查一次 users 表即可覆盖两列展示。
|
||||
func collectRechargeBillUserIDs(items []rechargeBillDTO) []int64 {
|
||||
seen := make(map[int64]struct{}, len(items)*2)
|
||||
userIDs := make([]int64, 0, len(items)*2)
|
||||
for _, item := range items {
|
||||
for _, userID := range []int64{item.UserID, item.SellerUserID} {
|
||||
if userID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[userID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[userID] = struct{}{}
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
}
|
||||
return userIDs
|
||||
}
|
||||
|
||||
// rechargeBillUserOrFallback 保证用户资料缺失时仍返回长 ID,列表和导出不会因为历史用户资料缺失而显示空白。
|
||||
func rechargeBillUserOrFallback(profile rechargeBillUserDTO, userID int64) rechargeBillUserDTO {
|
||||
if userID <= 0 {
|
||||
return rechargeBillUserDTO{}
|
||||
}
|
||||
if profile.UserID != "" {
|
||||
return profile
|
||||
}
|
||||
return rechargeBillUserDTO{UserID: strconv.FormatInt(userID, 10)}
|
||||
}
|
||||
|
||||
// int64Args 将用户 ID 列表转换为 database/sql 可展开的参数切片,避免为 IN 条件拼接原始数值。
|
||||
func int64Args(values []int64) []any {
|
||||
args := make([]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
args = append(args, value)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// placeholders 只生成固定数量的问号占位符;实际用户 ID 仍通过参数绑定传入,避免 SQL 注入。
|
||||
func placeholders(count int) string {
|
||||
if count <= 0 {
|
||||
return ""
|
||||
}
|
||||
items := make([]string, count)
|
||||
for index := range items {
|
||||
items[index] = "?"
|
||||
}
|
||||
return strings.Join(items, ",")
|
||||
}
|
||||
|
||||
func rechargeProductFromProto(item *walletv1.RechargeProduct) rechargeProductDTO {
|
||||
if item == nil {
|
||||
return rechargeProductDTO{}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
@ -22,18 +24,21 @@ const usdtMicroUnit int64 = 1_000_000
|
||||
|
||||
type Handler struct {
|
||||
wallet walletclient.Client
|
||||
userDB *sql.DB
|
||||
audit shared.OperationLogger
|
||||
}
|
||||
|
||||
func New(wallet walletclient.Client, audit shared.OperationLogger) *Handler {
|
||||
return &Handler{wallet: wallet, audit: audit}
|
||||
// New 组装支付后台 handler;wallet 负责账单和商品事实,userDB 只补后台列表需要的用户展示资料。
|
||||
func New(wallet walletclient.Client, userDB *sql.DB, audit shared.OperationLogger) *Handler {
|
||||
return &Handler{wallet: wallet, userDB: userDB, audit: audit}
|
||||
}
|
||||
|
||||
func (h *Handler) ListRechargeBills(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
appCode := appctx.FromContext(c.Request.Context())
|
||||
resp, err := h.wallet.ListRechargeBills(c.Request.Context(), &walletv1.ListRechargeBillsRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
AppCode: appCode,
|
||||
UserId: queryInt64(c, "user_id", "userId"),
|
||||
SellerUserId: queryInt64(c, "seller_user_id", "sellerUserId"),
|
||||
RegionId: queryInt64(c, "region_id", "regionId"),
|
||||
@ -54,9 +59,74 @@ func (h *Handler) ListRechargeBills(c *gin.Context) {
|
||||
for _, item := range resp.GetBills() {
|
||||
items = append(items, rechargeBillFromProto(item))
|
||||
}
|
||||
// 账单事实仍由 wallet-service 返回;这里只按本页出现的付款用户和币商用户批量补展示资料,避免列表渲染时逐行查用户。
|
||||
if err := h.fillRechargeBillUsers(c.Request.Context(), appCode, items); err != nil {
|
||||
response.ServerError(c, "获取账单用户资料失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||||
}
|
||||
|
||||
// fillRechargeBillUsers 将用户资料写回当前页账单 DTO;缺失资料会在 DTO 层保留长 ID,避免影响账单事实展示。
|
||||
func (h *Handler) fillRechargeBillUsers(ctx context.Context, appCode string, items []rechargeBillDTO) error {
|
||||
userIDs := collectRechargeBillUserIDs(items)
|
||||
if len(userIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
profiles, err := h.loadRechargeBillUsers(ctx, appCode, userIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for index := range items {
|
||||
items[index].User = rechargeBillUserOrFallback(profiles[items[index].UserID], items[index].UserID)
|
||||
items[index].Seller = rechargeBillUserOrFallback(profiles[items[index].SellerUserID], items[index].SellerUserID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadRechargeBillUsers 按 app_code 和用户 ID 批量读取展示资料;国家只服务后台账单展示,账单金额和区域事实仍以 wallet-service 为准。
|
||||
func (h *Handler) loadRechargeBillUsers(ctx context.Context, appCode string, userIDs []int64) (map[int64]rechargeBillUserDTO, error) {
|
||||
if h == nil || h.userDB == nil {
|
||||
return nil, errors.New("user mysql is not configured")
|
||||
}
|
||||
// 本查询只读取 users/countries 表里的展示字段,按照 app_code 和 user_id 精确命中;区域和账单筛选仍以 wallet-service 返回为准。
|
||||
rows, err := h.userDB.QueryContext(ctx, `
|
||||
SELECT u.user_id, COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''), COALESCE(u.avatar, ''),
|
||||
COALESCE(u.country, ''), COALESCE(c.country_name, ''), COALESCE(c.country_display_name, '')
|
||||
FROM users u
|
||||
LEFT JOIN countries c ON c.app_code = u.app_code AND c.country_code = u.country
|
||||
WHERE u.app_code = ? AND u.user_id IN (`+placeholders(len(userIDs))+`)`,
|
||||
append([]any{appCode}, int64Args(userIDs)...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
profiles := make(map[int64]rechargeBillUserDTO, len(userIDs))
|
||||
for rows.Next() {
|
||||
var profile rechargeBillUserDTO
|
||||
var userID int64
|
||||
if err := rows.Scan(
|
||||
&userID,
|
||||
&profile.DisplayUserID,
|
||||
&profile.Username,
|
||||
&profile.Avatar,
|
||||
&profile.CountryCode,
|
||||
&profile.CountryName,
|
||||
&profile.CountryDisplayName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
profile.UserID = strconv.FormatInt(userID, 10)
|
||||
profiles[userID] = profile
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (h *Handler) ListRechargeProducts(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
resp, err := h.wallet.ListAdminRechargeProducts(c.Request.Context(), &walletv1.ListAdminRechargeProductsRequest{
|
||||
|
||||
@ -3,6 +3,7 @@ package registrationreward
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -72,6 +73,14 @@ type userDTO struct {
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
type claimsPageDTO struct {
|
||||
Items []claimDTO `json:"items"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Total int64 `json:"total"`
|
||||
TodayClaimedCount int64 `json:"today_claimed_count"`
|
||||
}
|
||||
|
||||
func (h *Handler) GetRegistrationRewardConfig(c *gin.Context) {
|
||||
resp, err := h.activity.GetRegistrationRewardConfig(c.Request.Context(), &activityv1.GetRegistrationRewardConfigRequest{Meta: h.meta(c)})
|
||||
if err != nil {
|
||||
@ -107,21 +116,29 @@ func (h *Handler) UpdateRegistrationRewardConfig(c *gin.Context) {
|
||||
|
||||
func (h *Handler) ListRegistrationRewardClaims(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
claimedStartMS := queryInt64(c, "claimed_start_ms", "claimedStartMs", "start_ms", "startMs")
|
||||
claimedEndMS := queryInt64(c, "claimed_end_ms", "claimedEndMs", "end_ms", "endMs")
|
||||
userID, matched, ok := h.resolveClaimUserID(c.Request.Context(), strings.TrimSpace(options.Keyword))
|
||||
if !ok {
|
||||
response.ServerError(c, "查询用户信息失败")
|
||||
return
|
||||
}
|
||||
if options.Keyword != "" && !matched {
|
||||
response.OK(c, response.Page{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
|
||||
todayClaimedCount, ok := h.loadTodayClaimedCount(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
response.OK(c, claimsPageDTO{Items: []claimDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0, TodayClaimedCount: todayClaimedCount})
|
||||
return
|
||||
}
|
||||
resp, err := h.activity.ListRegistrationRewardClaims(c.Request.Context(), &activityv1.ListRegistrationRewardClaimsRequest{
|
||||
Meta: h.meta(c),
|
||||
Status: strings.TrimSpace(options.Status),
|
||||
UserId: userID,
|
||||
Page: int32(options.Page),
|
||||
PageSize: int32(options.PageSize),
|
||||
Meta: h.meta(c),
|
||||
Status: strings.TrimSpace(options.Status),
|
||||
UserId: userID,
|
||||
ClaimedStartMs: claimedStartMS,
|
||||
ClaimedEndMs: claimedEndMS,
|
||||
Page: int32(options.Page),
|
||||
PageSize: int32(options.PageSize),
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取注册奖励领取记录失败")
|
||||
@ -135,7 +152,26 @@ func (h *Handler) ListRegistrationRewardClaims(c *gin.Context) {
|
||||
response.ServerError(c, "补全用户信息失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||||
response.OK(c, claimsPageDTO{
|
||||
Items: items,
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
Total: resp.GetTotal(),
|
||||
TodayClaimedCount: resp.GetTodayClaimedCount(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) loadTodayClaimedCount(c *gin.Context) (int64, bool) {
|
||||
resp, err := h.activity.ListRegistrationRewardClaims(c.Request.Context(), &activityv1.ListRegistrationRewardClaimsRequest{
|
||||
Meta: h.meta(c),
|
||||
Page: 1,
|
||||
PageSize: 1,
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取注册奖励今日领取数量失败")
|
||||
return 0, false
|
||||
}
|
||||
return resp.GetTodayClaimedCount(), true
|
||||
}
|
||||
|
||||
func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta {
|
||||
@ -303,3 +339,17 @@ func int64Args(values []int64) []any {
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func queryInt64(c *gin.Context, names ...string) int64 {
|
||||
for _, name := range names {
|
||||
value := strings.TrimSpace(c.Query(name))
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err == nil && parsed > 0 {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -16,7 +16,6 @@ type resourceDTO struct {
|
||||
WalletAssetAmount int64 `json:"walletAssetAmount"`
|
||||
PriceType string `json:"priceType"`
|
||||
CoinPrice int64 `json:"coinPrice"`
|
||||
GiftPointAmount int64 `json:"giftPointAmount"`
|
||||
BadgeForm string `json:"badgeForm,omitempty"`
|
||||
BadgeKind string `json:"badgeKind,omitempty"`
|
||||
LevelTrack string `json:"levelTrack,omitempty"`
|
||||
@ -74,8 +73,6 @@ type giftDTO struct {
|
||||
GiftTypeCode string `json:"giftTypeCode"`
|
||||
ChargeAssetType string `json:"chargeAssetType"`
|
||||
CoinPrice int64 `json:"coinPrice"`
|
||||
GiftPointAmount int64 `json:"giftPointAmount"`
|
||||
HeatValue int64 `json:"heatValue"`
|
||||
EffectiveFromMS int64 `json:"effectiveFromMs"`
|
||||
EffectiveToMS int64 `json:"effectiveToMs"`
|
||||
EffectTypes []string `json:"effectTypes"`
|
||||
@ -198,7 +195,6 @@ func resourceFromProto(item *walletv1.Resource) resourceDTO {
|
||||
WalletAssetAmount: item.GetWalletAssetAmount(),
|
||||
PriceType: item.GetPriceType(),
|
||||
CoinPrice: item.GetCoinPrice(),
|
||||
GiftPointAmount: item.GetGiftPointAmount(),
|
||||
BadgeForm: badgeFormForResource(item.GetResourceType(), item.GetMetadataJson()),
|
||||
BadgeKind: badgeKindForResource(item.GetResourceType(), item.GetMetadataJson()),
|
||||
LevelTrack: badgeLevelTrackForResource(item.GetResourceType(), item.GetMetadataJson()),
|
||||
@ -299,8 +295,6 @@ func giftFromProto(gift *walletv1.GiftConfig) giftDTO {
|
||||
GiftTypeCode: gift.GetGiftTypeCode(),
|
||||
ChargeAssetType: gift.GetChargeAssetType(),
|
||||
CoinPrice: gift.GetCoinPrice(),
|
||||
GiftPointAmount: gift.GetGiftPointAmount(),
|
||||
HeatValue: gift.GetHeatValue(),
|
||||
EffectiveFromMS: gift.GetEffectiveFromMs(),
|
||||
EffectiveToMS: gift.GetEffectiveToMs(),
|
||||
EffectTypes: gift.GetEffectTypes(),
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
package resource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
"hyapp-admin-server/internal/integration/walletclient"
|
||||
@ -19,19 +21,30 @@ import (
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
wallet walletclient.Client
|
||||
store *repository.Store
|
||||
userDB *sql.DB
|
||||
audit shared.OperationLogger
|
||||
wallet walletclient.Client
|
||||
store *repository.Store
|
||||
userDB *sql.DB
|
||||
requestTimeout time.Duration
|
||||
audit shared.OperationLogger
|
||||
}
|
||||
|
||||
func New(wallet walletclient.Client, store *repository.Store, userDB *sql.DB, audit shared.OperationLogger) *Handler {
|
||||
return &Handler{wallet: wallet, store: store, userDB: userDB, audit: audit}
|
||||
func New(wallet walletclient.Client, store *repository.Store, userDB *sql.DB, requestTimeout time.Duration, audit shared.OperationLogger) *Handler {
|
||||
if requestTimeout <= 0 {
|
||||
requestTimeout = 3 * time.Second
|
||||
}
|
||||
return &Handler{wallet: wallet, store: store, userDB: userDB, requestTimeout: requestTimeout, audit: audit}
|
||||
}
|
||||
|
||||
func (h *Handler) walletRequestContext(c *gin.Context) (context.Context, context.CancelFunc) {
|
||||
// 资源后台在广州 admin-server 跨区访问 Saudi wallet-service;每次 RPC 必须继承 HTTP 取消信号并设置硬上限,避免坏连接拖到前置 504。
|
||||
return context.WithTimeout(c.Request.Context(), h.requestTimeout)
|
||||
}
|
||||
|
||||
func (h *Handler) ListResources(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
resp, err := h.wallet.ListResources(c.Request.Context(), &walletv1.ListResourcesRequest{
|
||||
ctx, cancel := h.walletRequestContext(c)
|
||||
defer cancel()
|
||||
resp, err := h.wallet.ListResources(ctx, &walletv1.ListResourcesRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
ResourceType: strings.TrimSpace(c.Query("resource_type")),
|
||||
@ -94,7 +107,9 @@ func (h *Handler) CreateResource(c *gin.Context) {
|
||||
|
||||
func (h *Handler) ListEmojiPacks(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
resp, err := h.wallet.ListResources(c.Request.Context(), &walletv1.ListResourcesRequest{
|
||||
ctx, cancel := h.walletRequestContext(c)
|
||||
defer cancel()
|
||||
resp, err := h.wallet.ListResources(ctx, &walletv1.ListResourcesRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
ResourceType: resourceTypeEmojiPack,
|
||||
@ -117,8 +132,10 @@ func (h *Handler) ListEmojiPacks(c *gin.Context) {
|
||||
func (h *Handler) ListEmojiPackCategories(c *gin.Context) {
|
||||
const pageSize int32 = 100
|
||||
categories := map[string]struct{}{}
|
||||
ctx, cancel := h.walletRequestContext(c)
|
||||
defer cancel()
|
||||
for page := int32(1); ; page++ {
|
||||
resp, err := h.wallet.ListResources(c.Request.Context(), &walletv1.ListResourcesRequest{
|
||||
resp, err := h.wallet.ListResources(ctx, &walletv1.ListResourcesRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
ResourceType: resourceTypeEmojiPack,
|
||||
@ -207,7 +224,9 @@ func (h *Handler) DisableResource(c *gin.Context) {
|
||||
|
||||
func (h *Handler) ListResourceGroups(c *gin.Context) {
|
||||
options := shared.ListOptions(c)
|
||||
resp, err := h.wallet.ListResourceGroups(c.Request.Context(), &walletv1.ListResourceGroupsRequest{
|
||||
ctx, cancel := h.walletRequestContext(c)
|
||||
defer cancel()
|
||||
resp, err := h.wallet.ListResourceGroups(ctx, &walletv1.ListResourceGroupsRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
Status: options.Status,
|
||||
@ -302,6 +321,7 @@ func (h *Handler) ListGifts(c *gin.Context) {
|
||||
PageSize: int32(options.PageSize),
|
||||
RegionId: regionID,
|
||||
FilterRegion: filterRegion,
|
||||
GiftTypeCode: giftTypeCodeQuery(c),
|
||||
})
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取礼物列表失败")
|
||||
@ -719,6 +739,14 @@ func optionalInt64Query(c *gin.Context, primary string, fallback string) (int64,
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func giftTypeCodeQuery(c *gin.Context) string {
|
||||
value := strings.TrimSpace(c.Query("giftTypeCode"))
|
||||
if value == "" {
|
||||
value = strings.TrimSpace(c.Query("gift_type_code"))
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func optionalInt64QueryWithPresence(c *gin.Context, primary string, fallback string) (int64, bool, bool) {
|
||||
raw := strings.TrimSpace(c.Query(primary))
|
||||
if raw == "" {
|
||||
|
||||
@ -53,7 +53,6 @@ type resourceRequest struct {
|
||||
Amount int64 `json:"amount"`
|
||||
PriceType string `json:"priceType"`
|
||||
CoinPrice int64 `json:"coinPrice"`
|
||||
GiftPointAmount int64 `json:"giftPointAmount"`
|
||||
BadgeForm string `json:"badgeForm"`
|
||||
BadgeKind string `json:"badgeKind"`
|
||||
LevelTrack string `json:"levelTrack"`
|
||||
@ -134,8 +133,7 @@ type giftRequest struct {
|
||||
GiftTypeCode string `json:"giftTypeCode"`
|
||||
ChargeAssetType string `json:"chargeAssetType"`
|
||||
CoinPrice int64 `json:"coinPrice"`
|
||||
GiftPointAmount int64 `json:"giftPointAmount"`
|
||||
HeatValue int64 `json:"heatValue"`
|
||||
HeatValue int64 `json:"heatValue"` // 兼容旧后台请求;新后台不再展示或提交热度。
|
||||
EffectiveAtMS int64 `json:"effectiveAtMs"`
|
||||
EffectiveFromMS int64 `json:"effectiveFromMs"`
|
||||
EffectiveToMS int64 `json:"effectiveToMs"`
|
||||
@ -370,8 +368,8 @@ func (r giftRequest) createProto(c *gin.Context) *walletv1.CreateGiftConfigReque
|
||||
GiftTypeCode: strings.TrimSpace(r.GiftTypeCode),
|
||||
ChargeAssetType: strings.TrimSpace(r.ChargeAssetType),
|
||||
CoinPrice: r.CoinPrice,
|
||||
GiftPointAmount: r.GiftPointAmount,
|
||||
HeatValue: r.HeatValue,
|
||||
GiftPointAmount: 0,
|
||||
HeatValue: r.CoinPrice,
|
||||
EffectiveAtMs: r.EffectiveAtMS,
|
||||
EffectiveFromMs: r.EffectiveFromMS,
|
||||
EffectiveToMs: r.EffectiveToMS,
|
||||
@ -395,8 +393,8 @@ func (r giftRequest) updateProto(c *gin.Context, giftID string) *walletv1.Update
|
||||
GiftTypeCode: strings.TrimSpace(r.GiftTypeCode),
|
||||
ChargeAssetType: strings.TrimSpace(r.ChargeAssetType),
|
||||
CoinPrice: r.CoinPrice,
|
||||
GiftPointAmount: r.GiftPointAmount,
|
||||
HeatValue: r.HeatValue,
|
||||
GiftPointAmount: 0,
|
||||
HeatValue: r.CoinPrice,
|
||||
EffectiveAtMs: r.EffectiveAtMS,
|
||||
EffectiveFromMs: r.EffectiveFromMS,
|
||||
EffectiveToMs: r.EffectiveToMS,
|
||||
@ -502,7 +500,7 @@ func resourcePricing(priceType string, coinPrice int64) (string, int64, int64) {
|
||||
if priceType == resourcePriceTypeFree {
|
||||
return resourcePriceTypeFree, 0, 0
|
||||
}
|
||||
return resourcePriceTypeCoin, coinPrice, coinPrice
|
||||
return resourcePriceTypeCoin, coinPrice, 0
|
||||
}
|
||||
|
||||
func validateResourcePricing(req resourceRequest) error {
|
||||
|
||||
@ -60,7 +60,7 @@ func (h *Handler) CreateRoomPin(c *gin.Context) {
|
||||
writeMutationError(c, err, "新增房间置顶失败")
|
||||
return
|
||||
}
|
||||
writeRoomAuditLog(c, h.audit, "create-room-pin", "room_region_pins", pin.Room.RoomID, "success", "create regional room pin")
|
||||
writeRoomAuditLog(c, h.audit, "create-room-pin", "room_region_pins", pin.Room.RoomID, "success", "create room pin")
|
||||
response.Created(c, pin)
|
||||
}
|
||||
|
||||
@ -74,7 +74,7 @@ func (h *Handler) CancelRoomPin(c *gin.Context) {
|
||||
writeMutationError(c, err, "取消房间置顶失败")
|
||||
return
|
||||
}
|
||||
writeRoomAuditLog(c, h.audit, "cancel-room-pin", "room_region_pins", strconv.FormatInt(pinID, 10), "success", "cancel regional room pin")
|
||||
writeRoomAuditLog(c, h.audit, "cancel-room-pin", "room_region_pins", strconv.FormatInt(pinID, 10), "success", "cancel room pin")
|
||||
response.OK(c, pin)
|
||||
}
|
||||
|
||||
@ -162,6 +162,7 @@ func parseRoomPinListQuery(c *gin.Context) (roomPinListQuery, bool) {
|
||||
Keyword: options.Keyword,
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
PinType: firstQueryValue(c, "pinType", "pin_type"),
|
||||
RegionID: regionID,
|
||||
Status: options.Status,
|
||||
}), true
|
||||
|
||||
@ -15,6 +15,8 @@ const (
|
||||
roomPinStatusCancelled = "cancelled"
|
||||
roomPinStatusExpired = "expired"
|
||||
roomPinStatusAll = "all"
|
||||
roomPinTypeRegion = "region"
|
||||
roomPinTypeGlobal = "global"
|
||||
defaultRoomPinDays = int64(30)
|
||||
maxRoomPinDays = int64(3650)
|
||||
roomPinDayMillis = int64(24 * 60 * 60 * 1000)
|
||||
@ -37,6 +39,7 @@ type RoomPin struct {
|
||||
CreatedByAdminID uint `json:"createdByAdminId"`
|
||||
ExpiresAtMS int64 `json:"expiresAtMs"`
|
||||
ID int64 `json:"id"`
|
||||
PinType string `json:"pinType"`
|
||||
PinnedAtMS int64 `json:"pinnedAtMs"`
|
||||
RemainingMS int64 `json:"remainingMs"`
|
||||
Room RoomPinRoom `json:"room"`
|
||||
@ -57,6 +60,7 @@ func (s *Service) ListRoomPins(ctx context.Context, query roomPinListQuery) ([]R
|
||||
Keyword: query.Keyword,
|
||||
Status: query.Status,
|
||||
RegionID: query.RegionID,
|
||||
PinType: query.PinType,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@ -78,8 +82,11 @@ func (s *Service) CreateRoomPin(ctx context.Context, req createRoomPinRequest, a
|
||||
}
|
||||
pin, err := s.roomClient.CreateRoomPin(ctx, roomclient.CreateRoomPinRequest{
|
||||
RoomID: input.RoomID,
|
||||
PinType: input.PinType,
|
||||
Weight: input.Weight,
|
||||
DurationDays: input.DurationDays,
|
||||
PinnedAtMS: input.PinnedAtMS,
|
||||
ExpiresAtMS: input.ExpiresAtMS,
|
||||
AdminID: uint64(actorID),
|
||||
})
|
||||
if err != nil {
|
||||
@ -124,12 +131,24 @@ func normalizeRoomPinListQuery(query roomPinListQuery) roomPinListQuery {
|
||||
}
|
||||
query.Keyword = strings.TrimSpace(query.Keyword)
|
||||
query.Status = normalizeRoomPinStatus(query.Status)
|
||||
query.PinType = normalizeRoomPinType(query.PinType)
|
||||
if query.RegionID < 0 {
|
||||
query.RegionID = 0
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
func normalizeRoomPinType(pinType string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(pinType)) {
|
||||
case roomPinTypeGlobal:
|
||||
return roomPinTypeGlobal
|
||||
case roomPinTypeRegion:
|
||||
return roomPinTypeRegion
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRoomPinStatus(status string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case "", roomPinStatusActive:
|
||||
@ -153,12 +172,27 @@ func normalizeCreateRoomPinRequest(req createRoomPinRequest) (createRoomPinReque
|
||||
if req.Weight < 0 {
|
||||
return req, fmt.Errorf("%w: 权重不能小于 0", ErrInvalidArgument)
|
||||
}
|
||||
if req.DurationDays == 0 {
|
||||
req.PinType = normalizeRoomPinType(req.PinType)
|
||||
if req.PinType == "" {
|
||||
req.PinType = roomPinTypeRegion
|
||||
}
|
||||
if req.PinnedAtMS <= 0 {
|
||||
req.PinnedAtMS = time.Now().UTC().UnixMilli()
|
||||
}
|
||||
if req.ExpiresAtMS <= 0 && req.DurationDays == 0 {
|
||||
req.DurationDays = defaultRoomPinDays
|
||||
}
|
||||
if req.DurationDays < 0 || req.DurationDays > maxRoomPinDays {
|
||||
if req.ExpiresAtMS <= 0 {
|
||||
req.ExpiresAtMS = req.PinnedAtMS + req.DurationDays*roomPinDayMillis
|
||||
}
|
||||
if req.ExpiresAtMS <= req.PinnedAtMS {
|
||||
return req, fmt.Errorf("%w: 置顶时间区间不正确", ErrInvalidArgument)
|
||||
}
|
||||
durationDays := (req.ExpiresAtMS - req.PinnedAtMS + roomPinDayMillis - 1) / roomPinDayMillis
|
||||
if durationDays < 1 || durationDays > maxRoomPinDays {
|
||||
return req, fmt.Errorf("%w: 置顶时间必须在 1 到 3650 天之间", ErrInvalidArgument)
|
||||
}
|
||||
req.DurationDays = durationDays
|
||||
return req, nil
|
||||
}
|
||||
|
||||
@ -182,6 +216,7 @@ func roomPinFromClient(input roomclient.RoomPin) RoomPin {
|
||||
}
|
||||
return RoomPin{
|
||||
ID: input.ID,
|
||||
PinType: firstNonEmpty(input.PinType, roomPinTypeRegion),
|
||||
Weight: input.Weight,
|
||||
Status: input.Status,
|
||||
PinnedAtMS: input.PinnedAtMS,
|
||||
@ -241,6 +276,15 @@ func scanRoomPin(row roomPinScanner) (RoomPin, error) {
|
||||
return item, err
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Service) fillRoomPinDetails(ctx context.Context, items []RoomPin) error {
|
||||
if s.userDB == nil || len(items) == 0 {
|
||||
return nil
|
||||
|
||||
@ -20,9 +20,28 @@ func TestNormalizeCreateRoomPinRequestDefaultsDuration(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("normalize create pin failed: %v", err)
|
||||
}
|
||||
if req.RoomID != "room-1" || req.DurationDays != defaultRoomPinDays || req.Weight != 10 {
|
||||
if req.RoomID != "room-1" || req.DurationDays != defaultRoomPinDays || req.PinType != roomPinTypeRegion || req.Weight != 10 {
|
||||
t.Fatalf("create pin normalization mismatch: %+v", req)
|
||||
}
|
||||
if req.PinnedAtMS <= 0 || req.ExpiresAtMS <= req.PinnedAtMS {
|
||||
t.Fatalf("create pin interval should be normalized: %+v", req)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeCreateRoomPinRequestAcceptsExplicitIntervalAndType(t *testing.T) {
|
||||
req, err := normalizeCreateRoomPinRequest(createRoomPinRequest{
|
||||
RoomID: "room-1",
|
||||
PinType: roomPinTypeGlobal,
|
||||
Weight: 10,
|
||||
PinnedAtMS: 1000,
|
||||
ExpiresAtMS: 2000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("normalize explicit create pin failed: %v", err)
|
||||
}
|
||||
if req.PinType != roomPinTypeGlobal || req.DurationDays != 1 || req.PinnedAtMS != 1000 || req.ExpiresAtMS != 2000 {
|
||||
t.Fatalf("explicit interval normalization mismatch: %+v", req)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeCreateRoomPinRequestRejectsInvalidInput(t *testing.T) {
|
||||
@ -33,6 +52,7 @@ func TestNormalizeCreateRoomPinRequestRejectsInvalidInput(t *testing.T) {
|
||||
{name: "empty_room", req: createRoomPinRequest{Weight: 1, DurationDays: 30}},
|
||||
{name: "negative_weight", req: createRoomPinRequest{RoomID: "room-1", Weight: -1, DurationDays: 30}},
|
||||
{name: "duration_too_large", req: createRoomPinRequest{RoomID: "room-1", Weight: 1, DurationDays: maxRoomPinDays + 1}},
|
||||
{name: "bad_interval", req: createRoomPinRequest{RoomID: "room-1", Weight: 1, PinnedAtMS: 2000, ExpiresAtMS: 1000}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@ -14,12 +14,16 @@ type roomPinListQuery struct {
|
||||
Keyword string
|
||||
Page int
|
||||
PageSize int
|
||||
PinType string
|
||||
RegionID int64
|
||||
Status string
|
||||
}
|
||||
|
||||
type createRoomPinRequest struct {
|
||||
DurationDays int64 `json:"durationDays"`
|
||||
ExpiresAtMS int64 `json:"expiresAtMs"`
|
||||
PinType string `json:"pinType"`
|
||||
PinnedAtMS int64 `json:"pinnedAtMs"`
|
||||
RoomID string `json:"roomId"`
|
||||
Weight int64 `json:"weight"`
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user