Merge remote-tracking branch 'origin/main' into codex/server-test-main-sync

This commit is contained in:
zhx 2026-06-08 13:01:39 +08:00
commit f79329f6de
338 changed files with 49567 additions and 15528 deletions

5
.gitignore vendored
View File

@ -27,4 +27,7 @@ node_modules/
mysql-data/
redis-data/
.waylog/
.waylog/
.tmp/
/tmp/

View File

@ -2,7 +2,7 @@
## Non-negotiables
0. 任何开发,不需要历史兼容,目前只是在开发阶段
1. 写代码不要猜。先读当前实现、接口、配置和测试,再改。
2. 不写流水账。文档、注释和总结都要解释边界、决策和可执行动作。
3. 前端或客户端改动不要添加无意义描述文案。界面文字必须服务真实操作或状态。
@ -10,6 +10,7 @@
5. 不把连接态塞进 `room-service`。客户端长连接和消息投递属于腾讯云 IM。
6. 代码永远保持高密度注释
7. 写代码的时候碰到为提交修改不用关心,只去做自己的
8. 注意关心代码历史兼容问题
## Architecture Boundaries

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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_idmeta.command_idmeta.actor_user_idmode room_name
// meta.room_idmeta.command_idmeta.actor_user_idmoderoom_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);
}

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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_idtarget_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);

View File

@ -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,

View 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` 或接口返回用户不在麦上时,停止本地麦上心跳。

View File

@ -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 不影响领取接口的后端金额校验 |

View File

@ -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 | 是否允许提现 |
### 可能错误

View File

@ -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`。 |

View File

@ -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` 重试,不生成新命令。 |

View File

@ -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` 被不同请求体复用 |

View File

@ -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` 是当前用户返奖入账后的余额。 |
发送后处理:

View File

@ -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` 重新同步。

View 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。

View File

@ -267,7 +267,7 @@ rocketmq:
1. 钱包余额通知:已按 `walletnotice` 模块落地。
2. 房间踢人私有通知:已按 `roomnotice` 模块落地,只消费 `RoomUserKicked` 的 C2C 通知。
3. 幸运礼物中奖:仍由 `wallet-service` 写余额 outboxnotice 不需要新增特殊账务逻辑,只要客户端按 `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`,独立重试和死信。

View File

@ -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 政策、周期、工资单、关系管理、统计聚合、幂等结果、领域 outboxApp/客户端不能提交区域,后台关系命令的 `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.

View File

@ -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

View File

@ -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 文档。

View File

@ -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 状态、消费幂等和领域状态校验。

View File

@ -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`
## 服务边界

View File

@ -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 包 |

View File

@ -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` |

View File

@ -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 建临时库。
- 该验证不连接线上库,不影响线上数据。

View File

@ -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` | 币商给玩家转金币 |

View File

@ -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

View File

@ -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` 列表桶。
- 房间创建成功后,创建者改国家不自动修改房间区域。

View File

@ -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

View File

@ -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 发布 RocketMQactivity、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 只投 RocketMQactivity/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 logMQ 后续可重试 |
| 到点开箱 | 即使房间未加载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` |

View File

@ -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

View 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`

View File

@ -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`
响应必须包含:

View File

@ -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。
- 提现人工打款前必须冻结余额,打款失败要回到可重新处理状态。
- 钱包事件投递使用 outboxMQ 投递失败不回滚账务事实。

View File

@ -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"
)

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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"),

View File

@ -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"

View 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'
);

View 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';

View 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);

View 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);

View File

@ -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;

View File

@ -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 内网 CLBgRPC 长连接如果长时间空闲,
// 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 {

View File

@ -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"

View File

@ -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` | 后台创建房间 |

View File

@ -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)
}

View File

@ -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,
})
}

View File

@ -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),

View 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)
}
}
}

View File

@ -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"),
})
}

View File

@ -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 {

View File

@ -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, &regionID)
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()
}

View 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)
}
}

View File

@ -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"`

View File

@ -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 == "" {

View File

@ -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"`

View File

@ -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)

View File

@ -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:

View File

@ -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")

View File

@ -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, ",")
}

View File

@ -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)
}

View File

@ -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, "获取统计总览失败")

View File

@ -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))
}

View File

@ -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 {

View File

@ -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"},

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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 ""

View File

@ -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"`
}

View File

@ -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)
}

View File

@ -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
}

View 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)
}
}

View File

@ -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)
}

View File

@ -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{}

View File

@ -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 组装支付后台 handlerwallet 负责账单和商品事实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{

View File

@ -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
}

View File

@ -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(),

View File

@ -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 == "" {

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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