Compare commits

..

No commits in common. "main" and "policy" have entirely different histories.
main ... policy

1685 changed files with 38652 additions and 409632 deletions

View File

@ -1,19 +0,0 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "admin-platform",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["--dir", "/Users/hy/Documents/hy/hyapp-admin-platform", "dev", "--port", "7002"],
"port": 7002,
"autoPort": false
},
{
"name": "admin-server",
"runtimeExecutable": "go",
"runtimeArgs": ["run", "-C", "server/admin", "./cmd/server"],
"port": 13100,
"autoPort": false
}
]
}

5
.gitignore vendored
View File

@ -28,8 +28,3 @@ mysql-data/
redis-data/ redis-data/
.waylog/ .waylog/
.tmp/
/tmp/
.claude/settings.local.json
backfill-last7

View File

@ -2,7 +2,7 @@
## Non-negotiables ## Non-negotiables
0. 任何开发,不需要历史兼容,目前只是在开发阶段
1. 写代码不要猜。先读当前实现、接口、配置和测试,再改。 1. 写代码不要猜。先读当前实现、接口、配置和测试,再改。
2. 不写流水账。文档、注释和总结都要解释边界、决策和可执行动作。 2. 不写流水账。文档、注释和总结都要解释边界、决策和可执行动作。
3. 前端或客户端改动不要添加无意义描述文案。界面文字必须服务真实操作或状态。 3. 前端或客户端改动不要添加无意义描述文案。界面文字必须服务真实操作或状态。
@ -10,7 +10,6 @@
5. 不把连接态塞进 `room-service`。客户端长连接和消息投递属于腾讯云 IM。 5. 不把连接态塞进 `room-service`。客户端长连接和消息投递属于腾讯云 IM。
6. 代码永远保持高密度注释 6. 代码永远保持高密度注释
7. 写代码的时候碰到为提交修改不用关心,只去做自己的 7. 写代码的时候碰到为提交修改不用关心,只去做自己的
8. 注意关心代码历史兼容问题
## Architecture Boundaries ## Architecture Boundaries
@ -19,7 +18,6 @@
- 腾讯云 IM 负责客户端长连接、房间群消息、公屏、单聊、离线/漫游和全球接入; - 腾讯云 IM 负责客户端长连接、房间群消息、公屏、单聊、离线/漫游和全球接入;
- `wallet-service` 负责账务语义。`SendGift` 必须先扣费成功,再落房间表现。 - `wallet-service` 负责账务语义。`SendGift` 必须先扣费成功,再落房间表现。
- `activity-service` 通过 room outbox 事件消费,不侵入 Room Cell 主链路。 - `activity-service` 通过 room outbox 事件消费,不侵入 Room Cell 主链路。
- `activity-service/internal/service/message` 是 App 系统/活动消息 inbox 的唯一业务 owner活动发通知必须走 `CreateSystemNotice``CreateActivityNotice``CreateNoticeFanout`,后台和其他服务不能直接写 `user_inbox_messages` / `message_fanout_jobs`
- `notice-service` 负责从各 owner service 的 outbox/事件源读取事实并投递私有通知、腾讯云 IM 单聊、后续 push/inbox钱包、活动和房间不能直接耦合私有通知投递实现。 - `notice-service` 负责从各 owner service 的 outbox/事件源读取事实并投递私有通知、腾讯云 IM 单聊、后续 push/inbox钱包、活动和房间不能直接耦合私有通知投递实现。
- `user-service` 负责用户主数据,不应该成为房间高频流程的同步中心。 - `user-service` 负责用户主数据,不应该成为房间高频流程的同步中心。
- `cron-service` 只负责后台调度、任务运行记录和通过内部 gRPC 触发 owner service 批处理;不能直接拥有房间、账务、用户或活动业务状态。 - `cron-service` 只负责后台调度、任务运行记录和通过内部 gRPC 触发 owner service 批处理;不能直接拥有房间、账务、用户或活动业务状态。
@ -71,7 +69,6 @@
- 修改 `api/proto` 后必须运行 `make proto`,并提交生成后的 `.pb.go` / `_grpc.pb.go` - 修改 `api/proto` 后必须运行 `make proto`,并提交生成后的 `.pb.go` / `_grpc.pb.go`
- 不要在服务之间直接 import 对方 `internal` 包。跨服务只走 protobuf client 或明确的接口抽象。 - 不要在服务之间直接 import 对方 `internal` 包。跨服务只走 protobuf client 或明确的接口抽象。
- `server/admin` 不能 import `hyapp/services/*``hyapp/pkg/*`;后台专属基础设施放在 `server/admin/internal/platform` - `server/admin` 不能 import `hyapp/services/*``hyapp/pkg/*`;后台专属基础设施放在 `server/admin/internal/platform`
- 系统/活动消息协议只使用后端 inbox 的 `message_type=system|activity``title``summary``body``action_type``action_param``metadata_json` 等字段;后台全服通知必须创建 fanout job不能在 admin HTTP handler 中同步遍历用户发消息。
## Port Rules ## Port Rules
@ -94,12 +91,11 @@
## Coding Rules ## Coding Rules
- 默认使用 Go 标准库和仓库已有小包;不要为了轻量逻辑引入重框架。 - 默认使用 Go 标准库和仓库已有小包;不要为了轻量逻辑引入重框架。
- 手写代码保持高密度有效注释,所有业务逻辑、状态流转、权限判断、支付链路、异步流程都要解释工程语义和失败分支,不复述语法。 - 手写代码保持高密度有效注释,注释解释工程语义和失败分支,不复述语法。
- 新增配置必须同时考虑本地 `config.yaml`、Docker `config.docker.yaml`、以及线上配置示例。 - 新增配置必须同时考虑本地 `config.yaml`、Docker `config.docker.yaml`、以及线上配置示例。
- 所有逻辑功能配置默认开启;密钥、地址、三方账号等敏感值只能由运行环境提供。可提交 YAML/示例配置只能写占位符;真实值只能写入已被 git 忽略的本地运行时 env 文件(例如 `.env.prod`)或部署平台环境变量,不能写入可提交文件。
- 新增服务依赖必须在 `docker-compose.yml` 中体现本地可运行形态。 - 新增服务依赖必须在 `docker-compose.yml` 中体现本地可运行形态。
- 数据库表结构变更必须同步更新对应 owner service 的 `deploy/mysql/initdb` - 数据库表结构变更必须同步更新对应 owner service 的 `deploy/mysql/initdb`
- 不要把线上密钥、云数据库密码、Redis 密码写进 YAML、示例配置、代码、测试或任何会进入 git 的文件;需要落地到文件时,只能写入 `.env.*` 这类已忽略的运行时 env 文件 - 不要把线上密钥、云数据库密码、Redis 密码写进真实配置。只能写占位示例
- 保持 `gofmt`,不要手调 Go import 顺序。 - 保持 `gofmt`,不要手调 Go import 顺序。
## Test Expectations ## Test Expectations

102
Makefile
View File

@ -1,46 +1,13 @@
COMPOSE_SERVICES := mysql redis rocketmq-namesrv rocketmq-broker gateway-service room-service wallet-service user-service activity-service lucky-gift-service luck-gateway cron-service robot-service game-service notice-service statistics-service COMPOSE_SERVICES := mysql redis rocketmq-namesrv rocketmq-broker gateway-service room-service wallet-service user-service activity-service cron-service game-service notice-service statistics-service
GO_RUN_SERVICES := gateway-service room-service wallet-service user-service activity-service lucky-gift-service luck-gateway cron-service robot-service game-service notice-service statistics-service GO_RUN_SERVICES := gateway-service room-service wallet-service user-service activity-service cron-service game-service notice-service statistics-service
SERVICE_ALIASES := gs gateway rs room ws wallet us user as activity lucky lucky-gift lgs luckygift lg cs cron robot robots rb game games ns notice stats statistics mq namesrv broker mysql redis rocketmq-namesrv rocketmq-broker gateway-service room-service wallet-service user-service activity-service lucky-gift-service luck-gateway cron-service robot-service game-service notice-service statistics-service SERVICE_ALIASES := gs gateway rs room ws wallet us user as activity cs cron game games ns notice stats statistics mq namesrv broker mysql redis rocketmq-namesrv rocketmq-broker gateway-service room-service wallet-service user-service activity-service cron-service game-service notice-service statistics-service
SERVICE_TOKEN := $(firstword $(filter $(SERVICE_ALIASES),$(MAKECMDGOALS))) SERVICE_TOKEN := $(firstword $(filter $(SERVICE_ALIASES),$(MAKECMDGOALS)))
SERVICE_ID := $(strip $(or $(SERVICE),$(SERVICE_TOKEN))) SERVICE_ID := $(strip $(or $(SERVICE),$(if $(filter gs gateway,$(SERVICE_TOKEN)),gateway-service,$(if $(filter rs room,$(SERVICE_TOKEN)),room-service,$(if $(filter ws wallet,$(SERVICE_TOKEN)),wallet-service,$(if $(filter us user,$(SERVICE_TOKEN)),user-service,$(if $(filter as activity,$(SERVICE_TOKEN)),activity-service,$(if $(filter cs cron,$(SERVICE_TOKEN)),cron-service,$(if $(filter game games,$(SERVICE_TOKEN)),game-service,$(if $(filter ns notice,$(SERVICE_TOKEN)),notice-service,$(if $(filter stats statistics,$(SERVICE_TOKEN)),statistics-service,$(if $(filter namesrv,$(SERVICE_TOKEN)),rocketmq-namesrv,$(if $(filter mq broker,$(SERVICE_TOKEN)),rocketmq-broker,$(SERVICE_TOKEN))))))))))))))
SERVICE_ID := $(patsubst gs,gateway-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst gateway,gateway-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst rs,room-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst room,room-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst ws,wallet-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst wallet,wallet-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst us,user-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst user,user-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst as,activity-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst activity,activity-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst lucky,lucky-gift-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst lucky-gift,lucky-gift-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst lgs,lucky-gift-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst luckygift,lucky-gift-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst lg,luck-gateway,$(SERVICE_ID))
SERVICE_ID := $(patsubst cs,cron-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst cron,cron-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst robot,robot-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst robots,robot-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst rb,robot-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst game,game-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst games,game-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst ns,notice-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst notice,notice-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst stats,statistics-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst statistics,statistics-service,$(SERVICE_ID))
SERVICE_ID := $(patsubst namesrv,rocketmq-namesrv,$(SERVICE_ID))
SERVICE_ID := $(patsubst mq,rocketmq-broker,$(SERVICE_ID))
SERVICE_ID := $(patsubst broker,rocketmq-broker,$(SERVICE_ID))
ADMIN_CONFIG ?= configs/config.yaml ADMIN_CONFIG ?= configs/config.yaml
DEV_RUN_SERVICES ?= all DEV_RUN_SERVICES ?= all
DEV_RUN_MQ ?= 0 DEV_RUN_SERVICE_LIST := $(strip $(if $(filter mysql redis,$(SERVICE_ID)),,$(if $(SERVICE_ID),$(SERVICE_ID),$(DEV_RUN_SERVICES))))
DEV_RUN_COMPOSE_ONLY := mysql redis rocketmq-namesrv rocketmq-broker
DEV_RUN_SERVICE_LIST := $(strip $(if $(filter $(DEV_RUN_COMPOSE_ONLY),$(SERVICE_ID)),,$(if $(SERVICE_ID),$(SERVICE_ID),$(DEV_RUN_SERVICES))))
ROCKETMQ_BROKER_CONFIG ?= ./tmp/rocketmq/broker.local.conf
export ROCKETMQ_BROKER_CONFIG
.PHONY: proto vet test build run admin admin-up admin-deps admin-bootstrap admin-test admin-build up db-init databi-real-flow-check rebuild restart stop logs ps addr down up-service check-service resolve-compose-conflicts $(SERVICE_ALIASES) .PHONY: proto vet test build run admin admin-up admin-deps admin-bootstrap admin-test admin-build up db-init rebuild restart stop logs ps addr down up-service check-service resolve-compose-conflicts $(SERVICE_ALIASES)
# `proto` 负责把独立 api module 内的 .proto 重新生成成 Go 代码。 # `proto` 负责把独立 api module 内的 .proto 重新生成成 Go 代码。
proto: proto:
@ -54,12 +21,9 @@ proto:
proto/user/v1/auth.proto \ proto/user/v1/auth.proto \
proto/user/v1/host.proto \ proto/user/v1/host.proto \
proto/activity/v1/activity.proto \ proto/activity/v1/activity.proto \
proto/luckygift/v1/luckygift.proto \
proto/room/v1/room.proto \ proto/room/v1/room.proto \
proto/game/v1/game.proto \ proto/game/v1/game.proto \
proto/robot/v1/robot.proto \
proto/wallet/v1/wallet.proto \ proto/wallet/v1/wallet.proto \
proto/events/luckygift/v1/events.proto \
proto/events/room/v1/events.proto proto/events/room/v1/events.proto
# `vet` 使用当前 Go 工具链检查 workspace 内所有后端模块,升级 Go 版本时先暴露静态风险。 # `vet` 使用当前 Go 工具链检查 workspace 内所有后端模块,升级 Go 版本时先暴露静态风险。
@ -67,46 +31,27 @@ vet:
go vet ./... go vet ./...
go vet ./api/... go vet ./api/...
go vet ./server/admin/... go vet ./server/admin/...
go vet ./server/luck-gateway/...
# `test` 统一跑 workspace 内的 app 后端、api 契约和后台管理服务测试。 # `test` 统一跑 workspace 内的 app 后端、api 契约和后台管理服务测试。
test: vet test: vet
go test ./... go test ./...
go test ./api/... go test ./api/...
go test ./server/admin/... go test ./server/admin/...
go test ./server/luck-gateway/...
# `build` 验证 app 后端和后台管理服务命令入口都能编译。 # `build` 验证 app 后端和后台管理服务命令入口都能编译。
build: build:
go build ./... go build ./...
go build ./server/admin/... go build ./server/admin/...
go build ./server/luck-gateway/...
# `run` 默认只拉 MySQL/Redis 并让 go run 服务读取内置默认配置,避免本地开发被 MQ topic/consumer 初始化拖慢。 # `run` 拉起 MySQL/Redis 后在本机用 go run 启动所有 Go 服务,不做源码监听或热重启。
# 需要验证 MQ fanout 时使用 `DEV_RUN_MQ=1 make run`,该模式会恢复 RocketMQ 配置生成和 topic 初始化。
# 可用 `make run rs`、`make run SERVICE=room-service` 或 `make run DEV_RUN_SERVICES=user-service,gateway-service` 只跑子集。 # 可用 `make run rs`、`make run SERVICE=room-service` 或 `make run DEV_RUN_SERVICES=user-service,gateway-service` 只跑子集。
run: run:
./scripts/resolve-compose-container-conflicts.sh ./scripts/resolve-compose-container-conflicts.sh
./scripts/prepare-local-mysql-initdb.sh docker compose up -d mysql redis
@if [ "$(DEV_RUN_MQ)" = "1" ]; then \
./scripts/prepare-local-rocketmq-config.sh; \
docker compose up -d mysql redis rocketmq-namesrv rocketmq-broker; \
else \
docker compose up -d mysql redis; \
fi
./scripts/apply-local-mysql-initdb.sh ./scripts/apply-local-mysql-initdb.sh
@if [ "$(DEV_RUN_MQ)" = "1" ]; then \
./scripts/apply-local-rocketmq-topics.sh; \
else \
echo "DEV_RUN_MQ=0: skip RocketMQ startup and topic initialization"; \
fi
docker compose stop $(GO_RUN_SERVICES) >/dev/null 2>&1 || true docker compose stop $(GO_RUN_SERVICES) >/dev/null 2>&1 || true
DEV_RUN_MQ="$(DEV_RUN_MQ)" ./scripts/print-compose-addresses.sh ./scripts/print-compose-addresses.sh
@if [ -n "$(DEV_RUN_SERVICE_LIST)" ]; then \ TZ=UTC go run ./cmd/dev-run -services "$(DEV_RUN_SERVICE_LIST)"
DEV_RUN_MQ="$(DEV_RUN_MQ)" TZ=UTC go run ./cmd/dev-run -services "$(DEV_RUN_SERVICE_LIST)"; \
else \
echo "compose dependencies are running; no Go service selected"; \
fi
# `admin` 在本机直接启动后台管理后端;依赖默认从 server/admin/configs/config.yaml 读取。 # `admin` 在本机直接启动后台管理后端;依赖默认从 server/admin/configs/config.yaml 读取。
# 启动前自动杀掉占用 13100 端口的旧进程,避免 bind: address already in use。 # 启动前自动杀掉占用 13100 端口的旧进程,避免 bind: address already in use。
@ -121,18 +66,10 @@ admin-up: admin-deps admin
# `admin-deps` 拉起后台本地运行所需的 MySQL、Redis 和 user-service。 # `admin-deps` 拉起后台本地运行所需的 MySQL、Redis 和 user-service。
admin-deps: admin-deps:
./scripts/resolve-compose-container-conflicts.sh ./scripts/resolve-compose-container-conflicts.sh
./scripts/prepare-local-mysql-initdb.sh
docker compose up -d mysql redis docker compose up -d mysql redis
./scripts/apply-local-mysql-initdb.sh ./scripts/apply-local-mysql-initdb.sh
@if [ "$(DEV_RUN_MQ)" = "1" ]; then \
./scripts/prepare-local-rocketmq-config.sh; \
docker compose up -d rocketmq-namesrv rocketmq-broker; \
./scripts/apply-local-rocketmq-topics.sh; \
else \
echo "DEV_RUN_MQ=0: skip RocketMQ startup and topic initialization"; \
fi
docker compose up --build -d user-service docker compose up --build -d user-service
DEV_RUN_MQ="$(DEV_RUN_MQ)" ./scripts/print-compose-addresses.sh ./scripts/print-compose-addresses.sh
# `admin-bootstrap` 显式执行后台初始化种子后启动服务。 # `admin-bootstrap` 显式执行后台初始化种子后启动服务。
admin-bootstrap: admin-bootstrap:
@ -151,17 +88,14 @@ admin-build:
# 完整 up 结束后自动启动 admin 前台进程。 # 完整 up 结束后自动启动 admin 前台进程。
up: up:
./scripts/resolve-compose-container-conflicts.sh ./scripts/resolve-compose-container-conflicts.sh
./scripts/prepare-local-mysql-initdb.sh
@if [ -z "$(SERVICE_ID)" ]; then \ @if [ -z "$(SERVICE_ID)" ]; then \
docker compose up -d mysql redis; \ docker compose up -d mysql redis; \
./scripts/apply-local-mysql-initdb.sh; \ ./scripts/apply-local-mysql-initdb.sh; \
./scripts/apply-local-rocketmq-topics.sh; \ docker compose up --build -d gateway-service room-service wallet-service user-service activity-service cron-service game-service notice-service statistics-service; \
docker compose up --build -d gateway-service room-service wallet-service user-service activity-service lucky-gift-service luck-gateway cron-service robot-service game-service notice-service statistics-service; \
elif [ "$(SERVICE_ID)" = "mysql" ] || [ "$(SERVICE_ID)" = "redis" ]; then \ elif [ "$(SERVICE_ID)" = "mysql" ] || [ "$(SERVICE_ID)" = "redis" ]; then \
docker compose up -d $(SERVICE_ID); \ docker compose up -d $(SERVICE_ID); \
else \ else \
./scripts/apply-local-mysql-initdb.sh; \ ./scripts/apply-local-mysql-initdb.sh; \
./scripts/apply-local-rocketmq-topics.sh; \
docker compose up -d $(SERVICE_ID); \ docker compose up -d $(SERVICE_ID); \
fi fi
./scripts/print-compose-addresses.sh ./scripts/print-compose-addresses.sh
@ -174,17 +108,11 @@ db-init:
./scripts/resolve-compose-container-conflicts.sh mysql ./scripts/resolve-compose-container-conflicts.sh mysql
./scripts/apply-local-mysql-initdb.sh ./scripts/apply-local-mysql-initdb.sh
# `databi-real-flow-check` 用真实 owner outbox 重放并审计 Databi 口径;充值允许走本地模拟回调,其余必须先通过真实业务流程产生事实。
databi-real-flow-check:
./scripts/databi-real-flow-check.sh
# `rebuild` 重新构建并启动单个本地服务。 # `rebuild` 重新构建并启动单个本地服务。
rebuild: check-service rebuild: check-service
./scripts/resolve-compose-container-conflicts.sh ./scripts/resolve-compose-container-conflicts.sh
./scripts/prepare-local-mysql-initdb.sh
@if [ "$(SERVICE_ID)" != "mysql" ] && [ "$(SERVICE_ID)" != "redis" ]; then \ @if [ "$(SERVICE_ID)" != "mysql" ] && [ "$(SERVICE_ID)" != "redis" ]; then \
./scripts/apply-local-mysql-initdb.sh; \ ./scripts/apply-local-mysql-initdb.sh; \
./scripts/apply-local-rocketmq-topics.sh; \
fi fi
docker compose up -d --build $(SERVICE_ID) docker compose up -d --build $(SERVICE_ID)
./scripts/print-compose-addresses.sh ./scripts/print-compose-addresses.sh
@ -225,13 +153,13 @@ down:
check-service: check-service:
@if [ -z "$(SERVICE_ID)" ]; then \ @if [ -z "$(SERVICE_ID)" ]; then \
echo "Service is required. Example: make restart rs"; \ echo "Service is required. Example: make restart rs"; \
echo "Aliases: gs/gateway rs/room ws/wallet us/user as/activity lucky/lgs lg/luck-gateway cs/cron game/games ns/notice mysql redis"; \ echo "Aliases: gs/gateway rs/room ws/wallet us/user as/activity cs/cron game/games ns/notice mysql redis"; \
echo "Services: $(COMPOSE_SERVICES)"; \ echo "Services: $(COMPOSE_SERVICES)"; \
exit 1; \ exit 1; \
fi fi
@if ! printf '%s\n' $(COMPOSE_SERVICES) | grep -qx "$(SERVICE_ID)"; then \ @if ! printf '%s\n' $(COMPOSE_SERVICES) | grep -qx "$(SERVICE_ID)"; then \
echo "Unknown service: $(SERVICE_ID)"; \ echo "Unknown service: $(SERVICE_ID)"; \
echo "Aliases: gs/gateway rs/room ws/wallet us/user as/activity lucky/lgs lg/luck-gateway cs/cron game/games ns/notice mysql redis"; \ echo "Aliases: gs/gateway rs/room ws/wallet us/user as/activity cs/cron game/games ns/notice mysql redis"; \
echo "Services: $(COMPOSE_SERVICES)"; \ echo "Services: $(COMPOSE_SERVICES)"; \
exit 1; \ exit 1; \
fi fi
@ -243,10 +171,8 @@ resolve-compose-conflicts:
# `up-service` 是 `up SERVICE=...` 的内部实现。 # `up-service` 是 `up SERVICE=...` 的内部实现。
up-service: check-service up-service: check-service
./scripts/resolve-compose-container-conflicts.sh ./scripts/resolve-compose-container-conflicts.sh
./scripts/prepare-local-mysql-initdb.sh
@if [ "$(SERVICE_ID)" != "mysql" ] && [ "$(SERVICE_ID)" != "redis" ]; then \ @if [ "$(SERVICE_ID)" != "mysql" ] && [ "$(SERVICE_ID)" != "redis" ]; then \
./scripts/apply-local-mysql-initdb.sh; \ ./scripts/apply-local-mysql-initdb.sh; \
./scripts/apply-local-rocketmq-topics.sh; \
fi fi
docker compose up -d $(SERVICE_ID) docker compose up -d $(SERVICE_ID)
./scripts/print-compose-addresses.sh ./scripts/print-compose-addresses.sh

View File

@ -6,8 +6,10 @@ HY Voice Room 是语音房后端 monorepo。当前架构不再自研 IM 长连
核心边界: 核心边界:
任何开发,不需要历史兼容,目前只是在开发阶段
- `gateway-service` 是客户端 HTTP JSON 入口,负责鉴权、协议转换、签发腾讯云 IM UserSig以及调用内部 gRPC 服务。 - `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` 同步调用它。 - `wallet-service` 当前提供 `DebitGift` 送礼扣费语义,余额、流水和幂等记录落 MySQL`room-service``SendGift` 同步调用它。
- `user-service` 当前承接登录、三方注册、密码、refresh session、用户主数据、默认短号和临时靓号基础能力。 - `user-service` 当前承接登录、三方注册、密码、refresh session、用户主数据、默认短号和临时靓号基础能力。
- `activity-service` 当前承接每日任务、活动/系统消息基础能力和房间事件 consumer可通过 room outbox direct gRPC 或 RocketMQ `hyapp_room_outbox` 独立 consumer group 消费房间事实。 - `activity-service` 当前承接每日任务、活动/系统消息基础能力和房间事件 consumer可通过 room outbox direct gRPC 或 RocketMQ `hyapp_room_outbox` 独立 consumer group 消费房间事实。
@ -27,24 +29,13 @@ HY Voice Room 是语音房后端 monorepo。当前架构不再自研 IM 长连
- 下游消费 owner outbox 必须使用本服务独立 RocketMQ consumer group并在本服务库内用幂等表或唯一键记录已消费 `event_id`;消费位点不能写回或复用 owner service 的 outbox 状态。 - 下游消费 owner outbox 必须使用本服务独立 RocketMQ consumer group并在本服务库内用幂等表或唯一键记录已消费 `event_id`;消费位点不能写回或复用 owner service 的 outbox 状态。
- 后台统计查询面:`server/admin``statistics-service` 内部 HTTP API`gateway-service` 是 App 入口,除非有明确 App 产品需求,否则不暴露后台数据大屏统计。 - 后台统计查询面:`server/admin``statistics-service` 内部 HTTP API`gateway-service` 是 App 入口,除非有明确 App 产品需求,否则不暴露后台数据大屏统计。
App 系统/活动消息:
- Flutter 消息页固定展示腾讯云 IM 用户消息、后端系统消息和后端活动消息三个入口;系统/活动页通过 `gateway-service` 读取 `activity-service` 的 inbox只按当前客户端能力展示标题、摘要/正文和时间。
- `activity-service/internal/service/message` 是系统/活动消息 owner。单人消息使用 `CreateSystemNotice` / `CreateActivityNotice`,全服或范围通知使用 `CreateNoticeFanout` 创建 `message_fanout_jobs`,由 cron 触发 fanout worker 分批写入 `user_inbox_messages`
- 活动业务发通知时传 `producer_event_id``command_id` 作为幂等键,`message_type` 只允许 `system``activity`;跳转参数使用已有 `action_type` 白名单,例如 `activity_detail``room_detail``user_profile``app_h5`
- `server/admin` 的 APP配置/系统消息推送只通过 `MessageInboxService.CreateFanoutJob` 创建后台 fanout 命令,不直接写 `hyapp_activity` 表,也不在 HTTP 请求里同步遍历用户。
## Time Model ## Time Model
项目全局时间模型固定为 UTC + epoch milliseconds。除客户端展示外App 后端所有按天、按周、按月归档或结算的业务口径都按 UTC不按中国时区、服务器本地时区、用户设备时区或 IP 推断时区切分。 项目全局时间模型固定为 UTC + epoch milliseconds
- 跨服务、数据库和 HTTP/gRPC 响应里的业务时间统一使用 Unix epoch milliseconds字段名保持 `*_ms`,例如 `created_at_ms``expire_at_ms``start_at_ms``end_at_ms``next_refresh_at_ms` - 跨服务、数据库和 HTTP/gRPC 响应里的业务时间统一使用 Unix epoch milliseconds字段名保持 `*_ms`,例如 `created_at_ms``expire_at_ms``start_at_ms``end_at_ms``next_refresh_at_ms`
- 后端业务自然日统一按 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 的毫秒时间戳。 - 后端业务自然日统一按 UTC 切日。每日任务的 `task_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 毫秒范围,再交给后端查询。 - 统计和账务时间范围统一使用 `[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`,避免依赖服务器所在地或容器本地时区。 - MySQL DSN 必须使用 `loc=UTC`。Docker 环境设置 `TZ=UTC`MySQL 使用 `--default-time-zone=+00:00`,避免依赖服务器所在地或容器本地时区。
- 用户所在时区、设备时区或 IP 推断时区只能用于展示、风控或资料字段;不能作为结算、统计、任务刷新和幂等周期的业务切日来源。 - 用户所在时区、设备时区或 IP 推断时区只能用于展示、风控或资料字段;不能作为结算、统计、任务刷新和幂等周期的业务切日来源。
@ -150,7 +141,7 @@ Authorization: Bearer <access_token>
- `gateway-service` 创建房间时从 access token 取当前 `user_id` 写入 `RequestMeta.actor_user_id``room-service` 由该 actor 收敛 owner外部 CreateRoom 请求体不接收 `owner_user_id` - `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` 由前端按用户动作生成,用于服务端幂等。 - `X-Request-ID`/`request_id` 由后端生成,只做链路追踪;写操作的 `command_id` 由前端按用户动作生成,用于服务端幂等。
- `room-service``CreateRoom` 成功后写 `room_outbox`,由 outbox worker 异步创建或补偿同名腾讯云 IM 群组。 - `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 单聊。 - `notice-service` 消费钱包余额变更 outbox 和房间踢人 outbox向单个用户发送 `wallet_notice``room_notice` 私有自定义消息owner service 不直接调用腾讯云 IM 单聊。
- `gateway-service` 已提供 `/api/v1/tencent-im/callback`,支持腾讯云 IM 入群前和发言前回调,并分别回查 `VerifyRoomPresence``CheckSpeakPermission` - `gateway-service` 已提供 `/api/v1/tencent-im/callback`,支持腾讯云 IM 入群前和发言前回调,并分别回查 `VerifyRoomPresence``CheckSpeakPermission`
@ -180,7 +171,7 @@ docs/flutter对接/Flutter App 通知与钱包余额对接.md
规则: 规则:
- `room_outbox` 只由 `room-service` 读取和发布;`wallet_outbox` 只由 `wallet-service` 读取和发布;`user_outbox` 只由 `user-service` 读取和发布;`game_outbox` 只由 `game-service` 读取和发布`lucky_gift_outbox` 只由 `lucky-gift-service` 读取和发布 - `room_outbox` 只由 `room-service` 读取和发布;`wallet_outbox` 只由 `wallet-service` 读取和发布;`user_outbox` 只由 `user-service` 读取和发布;`game_outbox` 只由 `game-service` 读取和发布。
- `statistics-service``activity-service``notice-service``cron-service` 等非 owner service 不能配置 owner 数据库 DSN 或扫描其他服务 outbox 表。 - `statistics-service``activity-service``notice-service``cron-service` 等非 owner service 不能配置 owner 数据库 DSN 或扫描其他服务 outbox 表。
- 每个业务消费者使用独立 consumer group。即使消费同一个 topic不同业务也不能共用 group否则会互相抢消息。 - 每个业务消费者使用独立 consumer group。即使消费同一个 topic不同业务也不能共用 group否则会互相抢消息。
- 每个消费者在自己的库内维护幂等事实,例如按 `app_code + event_id` 或业务唯一键建唯一约束RocketMQ 的消费位点只解决投递进度,不替代业务幂等。 - 每个消费者在自己的库内维护幂等事实,例如按 `app_code + event_id` 或业务唯一键建唯一约束RocketMQ 的消费位点只解决投递进度,不替代业务幂等。
@ -190,7 +181,7 @@ docs/flutter对接/Flutter App 通知与钱包余额对接.md
| Topic | Producer | Consumer Group | 用途 | | Topic | Producer | Consumer Group | 用途 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-activity-room-outbox` | activity-service 消费 `RoomGiftSent``RoomRocketIgnited` 等房间事实 | | `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-activity-room-outbox` | activity-service 消费 `RoomGiftSent``RoomTreasureCountdownStarted` 等房间事实 |
| `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-notice-room-outbox` | notice-service 消费 `RoomUserKicked` 等私有通知事实 | | `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-room-im-bridge` | room-service IM bridge 消费房间群系统消息和群成员控制事实 |
| `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-user-mictime-room-outbox` | user-service 消费 `RoomMicChanged` 并更新用户麦时读模型 | | `hyapp_room_outbox` | `hyapp-room-outbox-producer` | `hyapp-user-mictime-room-outbox` | user-service 消费 `RoomMicChanged` 并更新用户麦时读模型 |
@ -201,14 +192,10 @@ docs/flutter对接/Flutter App 通知与钱包余额对接.md
| `hyapp_wallet_outbox` | `hyapp-wallet-outbox-producer` | `hyapp-activity-red-packet-wallet-outbox` | activity-service 消费 `WalletRedPacketCreated` 并生成区域飘屏 outbox | | `hyapp_wallet_outbox` | `hyapp-wallet-outbox-producer` | `hyapp-activity-red-packet-wallet-outbox` | activity-service 消费 `WalletRedPacketCreated` 并生成区域飘屏 outbox |
| `hyapp_wallet_outbox` | `hyapp-wallet-outbox-producer` | `hyapp-notice-wallet-outbox` | notice-service 消费余额、账务等私有通知事实 | | `hyapp_wallet_outbox` | `hyapp-wallet-outbox-producer` | `hyapp-notice-wallet-outbox` | notice-service 消费余额、账务等私有通知事实 |
| `hyapp_wallet_outbox` | `hyapp-wallet-outbox-producer` | `hyapp-statistics-wallet-outbox` | statistics-service 消费 `WalletRechargeRecorded` 并更新充值聚合 | | `hyapp_wallet_outbox` | `hyapp-wallet-outbox-producer` | `hyapp-statistics-wallet-outbox` | statistics-service 消费 `WalletRechargeRecorded` 并更新充值聚合 |
| `hyapp_lucky_gift_outbox` | `hyapp-lucky-gift-outbox-producer` | `hyapp-room-lucky-gift-outbox` | room-service 消费已到账的 `LuckyGiftDrawn`,以本地 `room_outbox` 幂等投递房间顶部飘屏 |
| `hyapp_lucky_gift_outbox` | `hyapp-lucky-gift-outbox-producer` | `hyapp-activity-lucky-gift-outbox` | activity-service 消费已到账的 `LuckyGiftDrawn`,以本地 `im_broadcast_outbox` 幂等投递区域大赢家飘屏 |
| `hyapp_game_outbox` | `hyapp-game-outbox-producer` | `hyapp-statistics-game-outbox` | statistics-service 消费 `GameOrderSettled` 并更新游戏聚合 | | `hyapp_game_outbox` | `hyapp-game-outbox-producer` | `hyapp-statistics-game-outbox` | statistics-service 消费 `GameOrderSettled` 并更新游戏聚合 |
| `hyapp_room_rocket_launch` | `hyapp-room-rocket-launch-producer` | `hyapp-room-rocket-launch` | 火箭点火后到点唤醒 room-service 发射命令 | | `hyapp_room_treasure_open` | `hyapp-room-treasure-open-producer` | `hyapp-room-treasure-open` | 宝箱倒计时到点唤醒 room-service 开箱命令 |
RocketMQ 不拥有业务状态。`room_outbox``wallet_outbox``user_outbox``game_outbox``lucky_gift_outbox` 仍是各 owner service MySQL 内的可靠事实源MQ 发布失败时 outbox worker 退避重试,到达重试上限后进入死信。火箭延迟消息只负责唤醒,到点后 room-service 会重新校验 `rocket_id`、等级、`launch_at_ms`、状态和 UTC 重置边界。 RocketMQ 不拥有业务状态。`room_outbox``wallet_outbox``user_outbox``game_outbox` 仍是各 owner service MySQL 内的可靠事实源MQ 发布失败时 outbox worker 退避重试,到达重试上限后进入死信。宝箱延迟消息只负责唤醒,到点后 room-service 会重新校验 `box_id`、等级、`open_at_ms`、状态和 UTC 重置边界。
`hyapp_lucky_gift_outbox` 的两个消费者保持实时副作用所需的 latest offset。首次上线必须先创建 topic/group并先部署、确认 room/activity 两个 group 已在线订阅,再开启 lucky-gift producer不能用 `ConsumeFromFirst` 扫历史大奖生成过期飘屏。
## Storage Model ## Storage Model

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,465 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v5.29.2
// source: proto/events/luckygift/v1/events.proto
package luckygifteventsv1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// EventEnvelope 是 lucky-gift-service 发布 owner outbox 事实的稳定信封。
// occurred_at_ms 固定为抽奖事实创建时的 Unix epoch millisecondsMQ 实际发送时间不改变业务发生时间。
type EventEnvelope struct {
state protoimpl.MessageState `protogen:"open.v1"`
EventId string `protobuf:"bytes,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"`
EventType string `protobuf:"bytes,2,opt,name=event_type,json=eventType,proto3" json:"event_type,omitempty"`
AppCode string `protobuf:"bytes,3,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
// draw_id 是本次送礼命令的聚合中奖标识;批量子抽明细仍只保存在 lucky owner 数据库,不能放大 MQ 负载。
DrawId string `protobuf:"bytes,4,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"`
OccurredAtMs int64 `protobuf:"varint,5,opt,name=occurred_at_ms,json=occurredAtMs,proto3" json:"occurred_at_ms,omitempty"`
Body []byte `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EventEnvelope) Reset() {
*x = EventEnvelope{}
mi := &file_proto_events_luckygift_v1_events_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EventEnvelope) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EventEnvelope) ProtoMessage() {}
func (x *EventEnvelope) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_luckygift_v1_events_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EventEnvelope.ProtoReflect.Descriptor instead.
func (*EventEnvelope) Descriptor() ([]byte, []int) {
return file_proto_events_luckygift_v1_events_proto_rawDescGZIP(), []int{0}
}
func (x *EventEnvelope) GetEventId() string {
if x != nil {
return x.EventId
}
return ""
}
func (x *EventEnvelope) GetEventType() string {
if x != nil {
return x.EventType
}
return ""
}
func (x *EventEnvelope) GetAppCode() string {
if x != nil {
return x.AppCode
}
return ""
}
func (x *EventEnvelope) GetDrawId() string {
if x != nil {
return x.DrawId
}
return ""
}
func (x *EventEnvelope) GetOccurredAtMs() int64 {
if x != nil {
return x.OccurredAtMs
}
return 0
}
func (x *EventEnvelope) GetBody() []byte {
if x != nil {
return x.Body
}
return nil
}
// LuckyGiftDrawn 表达钱包返奖已经 granted 的幸运礼物中奖事实。
// lucky-gift-service 必须先完成 wallet-service 入账和本地 granted 收敛,再发布本事件;
// room-service 与 activity-service 只从该事实派生房内 IM 和区域飘屏,不能反向改变抽奖或账务状态。
type LuckyGiftDrawn struct {
state protoimpl.MessageState `protogen:"open.v1"`
DrawId string `protobuf:"bytes,1,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"`
CommandId string `protobuf:"bytes,2,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
RoomId string `protobuf:"bytes,3,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"`
PoolId string `protobuf:"bytes,4,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
GiftId string `protobuf:"bytes,5,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
GiftCount int32 `protobuf:"varint,6,opt,name=gift_count,json=giftCount,proto3" json:"gift_count,omitempty"`
SenderUserId int64 `protobuf:"varint,7,opt,name=sender_user_id,json=senderUserId,proto3" json:"sender_user_id,omitempty"`
TargetUserId int64 `protobuf:"varint,8,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
// 展示资料是送礼入口固化的快照;消费者不应为了飘屏高频同步反查 user-service。
SenderName string `protobuf:"bytes,9,opt,name=sender_name,json=senderName,proto3" json:"sender_name,omitempty"`
SenderAvatar string `protobuf:"bytes,10,opt,name=sender_avatar,json=senderAvatar,proto3" json:"sender_avatar,omitempty"`
SenderDisplayUserId string `protobuf:"bytes,11,opt,name=sender_display_user_id,json=senderDisplayUserId,proto3" json:"sender_display_user_id,omitempty"`
SenderPrettyDisplayUserId string `protobuf:"bytes,12,opt,name=sender_pretty_display_user_id,json=senderPrettyDisplayUserId,proto3" json:"sender_pretty_display_user_id,omitempty"`
// visible_region_id 是房间可见区域,区域播报不能用客户端 IP 或服务所在地推断。
VisibleRegionId int64 `protobuf:"varint,13,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"`
CountryId int64 `protobuf:"varint,14,opt,name=country_id,json=countryId,proto3" json:"country_id,omitempty"`
CoinSpent int64 `protobuf:"varint,15,opt,name=coin_spent,json=coinSpent,proto3" json:"coin_spent,omitempty"`
RuleVersion int64 `protobuf:"varint,16,opt,name=rule_version,json=ruleVersion,proto3" json:"rule_version,omitempty"`
ExperiencePool string `protobuf:"bytes,17,opt,name=experience_pool,json=experiencePool,proto3" json:"experience_pool,omitempty"`
SelectedTierId string `protobuf:"bytes,18,opt,name=selected_tier_id,json=selectedTierId,proto3" json:"selected_tier_id,omitempty"`
MultiplierPpm int64 `protobuf:"varint,19,opt,name=multiplier_ppm,json=multiplierPpm,proto3" json:"multiplier_ppm,omitempty"`
BaseRewardCoins int64 `protobuf:"varint,20,opt,name=base_reward_coins,json=baseRewardCoins,proto3" json:"base_reward_coins,omitempty"`
EffectiveRewardCoins int64 `protobuf:"varint,21,opt,name=effective_reward_coins,json=effectiveRewardCoins,proto3" json:"effective_reward_coins,omitempty"`
StageFeedback bool `protobuf:"varint,22,opt,name=stage_feedback,json=stageFeedback,proto3" json:"stage_feedback,omitempty"`
HighMultiplier bool `protobuf:"varint,23,opt,name=high_multiplier,json=highMultiplier,proto3" json:"high_multiplier,omitempty"`
// reward_status 在本事件中只能是 granted保留字段是为了下游 JSON 与现有 Flutter 协议同名映射。
RewardStatus string `protobuf:"bytes,24,opt,name=reward_status,json=rewardStatus,proto3" json:"reward_status,omitempty"`
WalletTransactionId string `protobuf:"bytes,25,opt,name=wallet_transaction_id,json=walletTransactionId,proto3" json:"wallet_transaction_id,omitempty"`
// 两个时间均为 UTC Unix epoch milliseconds抽奖发生时间与钱包返奖确认时间不能混用。
DrawCreatedAtMs int64 `protobuf:"varint,26,opt,name=draw_created_at_ms,json=drawCreatedAtMs,proto3" json:"draw_created_at_ms,omitempty"`
RewardGrantedAtMs int64 `protobuf:"varint,27,opt,name=reward_granted_at_ms,json=rewardGrantedAtMs,proto3" json:"reward_granted_at_ms,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LuckyGiftDrawn) Reset() {
*x = LuckyGiftDrawn{}
mi := &file_proto_events_luckygift_v1_events_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LuckyGiftDrawn) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LuckyGiftDrawn) ProtoMessage() {}
func (x *LuckyGiftDrawn) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_luckygift_v1_events_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LuckyGiftDrawn.ProtoReflect.Descriptor instead.
func (*LuckyGiftDrawn) Descriptor() ([]byte, []int) {
return file_proto_events_luckygift_v1_events_proto_rawDescGZIP(), []int{1}
}
func (x *LuckyGiftDrawn) GetDrawId() string {
if x != nil {
return x.DrawId
}
return ""
}
func (x *LuckyGiftDrawn) GetCommandId() string {
if x != nil {
return x.CommandId
}
return ""
}
func (x *LuckyGiftDrawn) GetRoomId() string {
if x != nil {
return x.RoomId
}
return ""
}
func (x *LuckyGiftDrawn) GetPoolId() string {
if x != nil {
return x.PoolId
}
return ""
}
func (x *LuckyGiftDrawn) GetGiftId() string {
if x != nil {
return x.GiftId
}
return ""
}
func (x *LuckyGiftDrawn) GetGiftCount() int32 {
if x != nil {
return x.GiftCount
}
return 0
}
func (x *LuckyGiftDrawn) GetSenderUserId() int64 {
if x != nil {
return x.SenderUserId
}
return 0
}
func (x *LuckyGiftDrawn) GetTargetUserId() int64 {
if x != nil {
return x.TargetUserId
}
return 0
}
func (x *LuckyGiftDrawn) GetSenderName() string {
if x != nil {
return x.SenderName
}
return ""
}
func (x *LuckyGiftDrawn) GetSenderAvatar() string {
if x != nil {
return x.SenderAvatar
}
return ""
}
func (x *LuckyGiftDrawn) GetSenderDisplayUserId() string {
if x != nil {
return x.SenderDisplayUserId
}
return ""
}
func (x *LuckyGiftDrawn) GetSenderPrettyDisplayUserId() string {
if x != nil {
return x.SenderPrettyDisplayUserId
}
return ""
}
func (x *LuckyGiftDrawn) GetVisibleRegionId() int64 {
if x != nil {
return x.VisibleRegionId
}
return 0
}
func (x *LuckyGiftDrawn) GetCountryId() int64 {
if x != nil {
return x.CountryId
}
return 0
}
func (x *LuckyGiftDrawn) GetCoinSpent() int64 {
if x != nil {
return x.CoinSpent
}
return 0
}
func (x *LuckyGiftDrawn) GetRuleVersion() int64 {
if x != nil {
return x.RuleVersion
}
return 0
}
func (x *LuckyGiftDrawn) GetExperiencePool() string {
if x != nil {
return x.ExperiencePool
}
return ""
}
func (x *LuckyGiftDrawn) GetSelectedTierId() string {
if x != nil {
return x.SelectedTierId
}
return ""
}
func (x *LuckyGiftDrawn) GetMultiplierPpm() int64 {
if x != nil {
return x.MultiplierPpm
}
return 0
}
func (x *LuckyGiftDrawn) GetBaseRewardCoins() int64 {
if x != nil {
return x.BaseRewardCoins
}
return 0
}
func (x *LuckyGiftDrawn) GetEffectiveRewardCoins() int64 {
if x != nil {
return x.EffectiveRewardCoins
}
return 0
}
func (x *LuckyGiftDrawn) GetStageFeedback() bool {
if x != nil {
return x.StageFeedback
}
return false
}
func (x *LuckyGiftDrawn) GetHighMultiplier() bool {
if x != nil {
return x.HighMultiplier
}
return false
}
func (x *LuckyGiftDrawn) GetRewardStatus() string {
if x != nil {
return x.RewardStatus
}
return ""
}
func (x *LuckyGiftDrawn) GetWalletTransactionId() string {
if x != nil {
return x.WalletTransactionId
}
return ""
}
func (x *LuckyGiftDrawn) GetDrawCreatedAtMs() int64 {
if x != nil {
return x.DrawCreatedAtMs
}
return 0
}
func (x *LuckyGiftDrawn) GetRewardGrantedAtMs() int64 {
if x != nil {
return x.RewardGrantedAtMs
}
return 0
}
var File_proto_events_luckygift_v1_events_proto protoreflect.FileDescriptor
const file_proto_events_luckygift_v1_events_proto_rawDesc = "" +
"\n" +
"&proto/events/luckygift/v1/events.proto\x12\x19hyapp.events.luckygift.v1\"\xb7\x01\n" +
"\rEventEnvelope\x12\x19\n" +
"\bevent_id\x18\x01 \x01(\tR\aeventId\x12\x1d\n" +
"\n" +
"event_type\x18\x02 \x01(\tR\teventType\x12\x19\n" +
"\bapp_code\x18\x03 \x01(\tR\aappCode\x12\x17\n" +
"\adraw_id\x18\x04 \x01(\tR\x06drawId\x12$\n" +
"\x0eoccurred_at_ms\x18\x05 \x01(\x03R\foccurredAtMs\x12\x12\n" +
"\x04body\x18\x06 \x01(\fR\x04body\"\xab\b\n" +
"\x0eLuckyGiftDrawn\x12\x17\n" +
"\adraw_id\x18\x01 \x01(\tR\x06drawId\x12\x1d\n" +
"\n" +
"command_id\x18\x02 \x01(\tR\tcommandId\x12\x17\n" +
"\aroom_id\x18\x03 \x01(\tR\x06roomId\x12\x17\n" +
"\apool_id\x18\x04 \x01(\tR\x06poolId\x12\x17\n" +
"\agift_id\x18\x05 \x01(\tR\x06giftId\x12\x1d\n" +
"\n" +
"gift_count\x18\x06 \x01(\x05R\tgiftCount\x12$\n" +
"\x0esender_user_id\x18\a \x01(\x03R\fsenderUserId\x12$\n" +
"\x0etarget_user_id\x18\b \x01(\x03R\ftargetUserId\x12\x1f\n" +
"\vsender_name\x18\t \x01(\tR\n" +
"senderName\x12#\n" +
"\rsender_avatar\x18\n" +
" \x01(\tR\fsenderAvatar\x123\n" +
"\x16sender_display_user_id\x18\v \x01(\tR\x13senderDisplayUserId\x12@\n" +
"\x1dsender_pretty_display_user_id\x18\f \x01(\tR\x19senderPrettyDisplayUserId\x12*\n" +
"\x11visible_region_id\x18\r \x01(\x03R\x0fvisibleRegionId\x12\x1d\n" +
"\n" +
"country_id\x18\x0e \x01(\x03R\tcountryId\x12\x1d\n" +
"\n" +
"coin_spent\x18\x0f \x01(\x03R\tcoinSpent\x12!\n" +
"\frule_version\x18\x10 \x01(\x03R\vruleVersion\x12'\n" +
"\x0fexperience_pool\x18\x11 \x01(\tR\x0eexperiencePool\x12(\n" +
"\x10selected_tier_id\x18\x12 \x01(\tR\x0eselectedTierId\x12%\n" +
"\x0emultiplier_ppm\x18\x13 \x01(\x03R\rmultiplierPpm\x12*\n" +
"\x11base_reward_coins\x18\x14 \x01(\x03R\x0fbaseRewardCoins\x124\n" +
"\x16effective_reward_coins\x18\x15 \x01(\x03R\x14effectiveRewardCoins\x12%\n" +
"\x0estage_feedback\x18\x16 \x01(\bR\rstageFeedback\x12'\n" +
"\x0fhigh_multiplier\x18\x17 \x01(\bR\x0ehighMultiplier\x12#\n" +
"\rreward_status\x18\x18 \x01(\tR\frewardStatus\x122\n" +
"\x15wallet_transaction_id\x18\x19 \x01(\tR\x13walletTransactionId\x12+\n" +
"\x12draw_created_at_ms\x18\x1a \x01(\x03R\x0fdrawCreatedAtMs\x12/\n" +
"\x14reward_granted_at_ms\x18\x1b \x01(\x03R\x11rewardGrantedAtMsB=Z;hyapp.local/api/proto/events/luckygift/v1;luckygifteventsv1b\x06proto3"
var (
file_proto_events_luckygift_v1_events_proto_rawDescOnce sync.Once
file_proto_events_luckygift_v1_events_proto_rawDescData []byte
)
func file_proto_events_luckygift_v1_events_proto_rawDescGZIP() []byte {
file_proto_events_luckygift_v1_events_proto_rawDescOnce.Do(func() {
file_proto_events_luckygift_v1_events_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_events_luckygift_v1_events_proto_rawDesc), len(file_proto_events_luckygift_v1_events_proto_rawDesc)))
})
return file_proto_events_luckygift_v1_events_proto_rawDescData
}
var file_proto_events_luckygift_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proto_events_luckygift_v1_events_proto_goTypes = []any{
(*EventEnvelope)(nil), // 0: hyapp.events.luckygift.v1.EventEnvelope
(*LuckyGiftDrawn)(nil), // 1: hyapp.events.luckygift.v1.LuckyGiftDrawn
}
var file_proto_events_luckygift_v1_events_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_proto_events_luckygift_v1_events_proto_init() }
func file_proto_events_luckygift_v1_events_proto_init() {
if File_proto_events_luckygift_v1_events_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_events_luckygift_v1_events_proto_rawDesc), len(file_proto_events_luckygift_v1_events_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_proto_events_luckygift_v1_events_proto_goTypes,
DependencyIndexes: file_proto_events_luckygift_v1_events_proto_depIdxs,
MessageInfos: file_proto_events_luckygift_v1_events_proto_msgTypes,
}.Build()
File_proto_events_luckygift_v1_events_proto = out.File
file_proto_events_luckygift_v1_events_proto_goTypes = nil
file_proto_events_luckygift_v1_events_proto_depIdxs = nil
}

View File

@ -1,54 +0,0 @@
syntax = "proto3";
package hyapp.events.luckygift.v1;
option go_package = "hyapp.local/api/proto/events/luckygift/v1;luckygifteventsv1";
// EventEnvelope lucky-gift-service owner outbox
// occurred_at_ms Unix epoch millisecondsMQ
message EventEnvelope {
string event_id = 1;
string event_type = 2;
string app_code = 3;
// draw_id lucky owner MQ
string draw_id = 4;
int64 occurred_at_ms = 5;
bytes body = 6;
}
// LuckyGiftDrawn granted
// lucky-gift-service wallet-service granted
// room-service activity-service IM
message LuckyGiftDrawn {
string draw_id = 1;
string command_id = 2;
string room_id = 3;
string pool_id = 4;
string gift_id = 5;
int32 gift_count = 6;
int64 sender_user_id = 7;
int64 target_user_id = 8;
// user-service
string sender_name = 9;
string sender_avatar = 10;
string sender_display_user_id = 11;
string sender_pretty_display_user_id = 12;
// visible_region_id IP
int64 visible_region_id = 13;
int64 country_id = 14;
int64 coin_spent = 15;
int64 rule_version = 16;
string experience_pool = 17;
string selected_tier_id = 18;
int64 multiplier_ppm = 19;
int64 base_reward_coins = 20;
int64 effective_reward_coins = 21;
bool stage_feedback = 22;
bool high_multiplier = 23;
// reward_status granted JSON Flutter
string reward_status = 24;
string wallet_transaction_id = 25;
// UTC Unix epoch milliseconds
int64 draw_created_at_ms = 26;
int64 reward_granted_at_ms = 27;
}

File diff suppressed because it is too large Load Diff

View File

@ -45,38 +45,11 @@ message RoomBackgroundChanged {
} }
// RoomUserJoined // RoomUserJoined
message RoomEntryVehicleSnapshot {
int64 resource_id = 1;
string resource_code = 2;
string name = 3;
string asset_url = 4;
string preview_url = 5;
string animation_url = 6;
string metadata_json = 7;
string entitlement_id = 8;
int64 expires_at_ms = 9;
}
message RoomUserJoined { message RoomUserJoined {
int64 user_id = 1; int64 user_id = 1;
string role = 2; string role = 2;
// visible_region_id / room-service // visible_region_id / room-service
int64 visible_region_id = 3; int64 visible_region_id = 3;
RoomEntryVehicleSnapshot entry_vehicle = 4;
int64 country_id = 5;
int64 region_id = 6;
// is_robot
bool is_robot = 7;
// nickname/avatar/display_user_id IM user-service
string nickname = 8;
string avatar = 9;
string display_user_id = 10;
string pretty_display_user_id = 11;
// VIP GetMyVip effective state outbox direct IM
string vip_program_type = 12;
int32 effective_vip_level = 13;
string effective_vip_name = 14;
bool room_entry_notice_enabled = 15;
} }
// RoomUserLeft // RoomUserLeft
@ -107,12 +80,6 @@ message RoomMicChanged {
int64 publish_event_time_ms = 10; int64 publish_event_time_ms = 10;
bool mic_muted = 11; bool mic_muted = 11;
string seat_status = 12; string seat_status = 12;
int64 target_gift_value = 13;
// target_* IM/UI
string target_nickname = 14;
string target_avatar = 15;
string target_display_user_id = 16;
string target_pretty_display_user_id = 17;
} }
// RoomMicSeatLocked // RoomMicSeatLocked
@ -132,8 +99,6 @@ message RoomChatEnabledChanged {
message RoomPasswordChanged { message RoomPasswordChanged {
int64 actor_user_id = 1; int64 actor_user_id = 1;
bool locked = 2; bool locked = 2;
// visible_region_id activity-service
int64 visible_region_id = 3;
} }
// RoomAdminChanged // RoomAdminChanged
@ -173,149 +138,6 @@ message RoomGiftSent {
int64 visible_region_id = 7; int64 visible_region_id = 7;
string command_id = 8; string command_id = 8;
string pool_id = 9; string pool_id = 9;
int64 coin_spent = 10;
int64 country_id = 11;
int64 region_id = 12;
string gift_type_code = 13;
string cp_relation_type = 14;
string gift_name = 15;
string gift_icon_url = 16;
string gift_animation_url = 17;
int64 target_gift_value = 18;
repeated string gift_effect_types = 19;
// is_robot_gift
bool is_robot_gift = 20;
// synthetic_lucky_gift
bool synthetic_lucky_gift = 21;
// real_room_robot_gift Databi
bool real_room_robot_gift = 22;
// sender_name IM
string sender_name = 23;
string sender_avatar = 24;
string sender_display_user_id = 25;
string sender_pretty_display_user_id = 26;
// receiver_nickname Flutter
string receiver_nickname = 27;
string receiver_avatar = 28;
string receiver_display_user_id = 29;
string receiver_pretty_display_user_id = 30;
// display_mode=batch activityCP RoomGiftBatchSent
string display_mode = 31;
}
// RoomGiftBatchLuckyResult
// IM activity
message RoomGiftBatchLuckyResult {
bool enabled = 1;
string draw_id = 2;
string command_id = 3;
string pool_id = 4;
string gift_id = 5;
int64 rule_version = 6;
string experience_pool = 7;
string selected_tier_id = 8;
int64 multiplier_ppm = 9;
int64 base_reward_coins = 10;
int64 effective_reward_coins = 11;
string reward_status = 12;
bool stage_feedback = 13;
bool high_multiplier = 14;
int64 created_at_ms = 15;
int64 target_user_id = 16;
}
// RoomGiftBatchTarget
// gateway user-service IM bridge
message RoomGiftBatchTarget {
int64 target_user_id = 1;
string receiver_nickname = 2;
string receiver_avatar = 3;
string receiver_display_user_id = 4;
string receiver_pretty_display_user_id = 5;
int64 gift_value = 6;
int64 target_gift_value = 7;
int64 coin_spent = 8;
string billing_receipt_id = 9;
string command_id = 10;
RoomGiftBatchLuckyResult lucky_gift = 11;
}
// RoomGiftBatchSent Flutter
// RoomGiftSent durable outbox CP
message RoomGiftBatchSent {
int64 sender_user_id = 1;
string sender_name = 2;
string sender_avatar = 3;
string sender_display_user_id = 4;
string sender_pretty_display_user_id = 5;
string gift_id = 6;
int32 gift_count = 7;
int64 total_gift_value = 8;
int64 total_coin_spent = 9;
string billing_receipt_id = 10;
int64 room_heat = 11;
string pool_id = 12;
string gift_type_code = 13;
string cp_relation_type = 14;
string gift_name = 15;
string gift_icon_url = 16;
string gift_animation_url = 17;
repeated string gift_effect_types = 18;
repeated int64 target_user_ids = 19;
repeated RoomGiftBatchTarget targets = 20;
string command_id = 21;
int64 visible_region_id = 22;
}
// RoomLuckyGiftDrawn room-service lucky-gift-service owner
// IM Room Cell room_version
message RoomLuckyGiftDrawn {
string draw_id = 1;
string command_id = 2;
string pool_id = 3;
string gift_id = 4;
int32 gift_count = 5;
int64 sender_user_id = 6;
int64 target_user_id = 7;
string sender_name = 8;
string sender_avatar = 9;
string sender_display_user_id = 10;
string sender_pretty_display_user_id = 11;
int64 visible_region_id = 12;
int64 country_id = 13;
int64 coin_spent = 14;
int64 rule_version = 15;
string experience_pool = 16;
string selected_tier_id = 17;
int64 multiplier_ppm = 18;
int64 base_reward_coins = 19;
int64 effective_reward_coins = 20;
bool stage_feedback = 21;
bool high_multiplier = 22;
string reward_status = 23;
string wallet_transaction_id = 24;
// 沿 lucky owner UTC Unix epoch milliseconds room-service
int64 draw_created_at_ms = 25;
int64 reward_granted_at_ms = 26;
}
// RoomRobotLuckyGiftDrawn
// lucky_gift_drawn
message RoomRobotLuckyGiftDrawn {
string draw_id = 1;
string command_id = 2;
int64 sender_user_id = 3;
int64 target_user_id = 4;
string gift_id = 5;
int32 gift_count = 6;
string pool_id = 7;
int64 multiplier_ppm = 8;
int64 base_reward_coins = 9;
int64 effective_reward_coins = 10;
int64 created_at_ms = 11;
int64 visible_region_id = 12;
bool is_robot = 13;
bool synthetic = 14;
} }
// RoomHeatChanged // RoomHeatChanged
@ -331,7 +153,7 @@ message RoomRankChanged {
int64 gift_value = 3; int64 gift_value = 3;
} }
message RoomRocketRewardGrant { message RoomTreasureRewardGrant {
string reward_role = 1; string reward_role = 1;
int64 user_id = 2; int64 user_id = 2;
string reward_item_id = 3; string reward_item_id = 3;
@ -342,15 +164,15 @@ message RoomRocketRewardGrant {
string status = 8; string status = 8;
} }
// RoomRocketFuelChanged // RoomTreasureProgressChanged
message RoomRocketFuelChanged { message RoomTreasureProgressChanged {
string rocket_id = 1; string box_id = 1;
int32 level = 2; int32 level = 2;
int64 added_fuel = 3; int64 added_energy = 3;
int64 effective_added_fuel = 4; int64 effective_added_energy = 4;
int64 overflow_fuel = 5; int64 overflow_energy = 5;
int64 current_fuel = 6; int64 current_progress = 6;
int64 fuel_threshold = 7; int64 energy_threshold = 7;
string status = 8; string status = 8;
int64 reset_at_ms = 9; int64 reset_at_ms = 9;
int64 sender_user_id = 10; int64 sender_user_id = 10;
@ -360,40 +182,37 @@ message RoomRocketFuelChanged {
int64 visible_region_id = 14; int64 visible_region_id = 14;
} }
// RoomRocketIgnited / // RoomTreasureCountdownStarted /
message RoomRocketIgnited { message RoomTreasureCountdownStarted {
string rocket_id = 1; string box_id = 1;
int32 level = 2; int32 level = 2;
int64 current_fuel = 3; int64 current_progress = 3;
int64 fuel_threshold = 4; int64 energy_threshold = 4;
int64 ignited_at_ms = 5; int64 countdown_started_at_ms = 5;
int64 launch_at_ms = 6; int64 open_at_ms = 6;
string broadcast_scope = 7; string broadcast_scope = 7;
int64 visible_region_id = 8; int64 visible_region_id = 8;
int64 top1_user_id = 9; int64 top1_user_id = 9;
int64 igniter_user_id = 10; int64 igniter_user_id = 10;
string command_id = 11; string command_id = 11;
string room_short_id = 12;
string rocket_cover_url = 13;
int64 reset_at_ms = 14;
} }
// RoomRocketLaunched 线 Room Cell presence // RoomTreasureOpened 线 Room Cell presence
message RoomRocketLaunched { message RoomTreasureOpened {
string rocket_id = 1; string box_id = 1;
int32 level = 2; int32 level = 2;
int32 next_level = 3; int32 next_level = 3;
int64 launched_at_ms = 4; int64 opened_at_ms = 4;
int64 reset_at_ms = 5; int64 reset_at_ms = 5;
int64 top1_user_id = 6; int64 top1_user_id = 6;
int64 igniter_user_id = 7; int64 igniter_user_id = 7;
repeated int64 in_room_user_ids = 8; repeated int64 in_room_user_ids = 8;
repeated RoomRocketRewardGrant rewards = 9; repeated RoomTreasureRewardGrant rewards = 9;
} }
// RoomRocketRewardGranted // RoomTreasureRewardGranted
message RoomRocketRewardGranted { message RoomTreasureRewardGranted {
string rocket_id = 1; string box_id = 1;
int32 level = 2; int32 level = 2;
repeated RoomRocketRewardGrant rewards = 3; repeated RoomTreasureRewardGrant rewards = 3;
} }

File diff suppressed because it is too large Load Diff

View File

@ -48,9 +48,6 @@ message GameCatalogItem {
repeated string tags = 14; repeated string tags = 14;
int64 created_at_ms = 15; int64 created_at_ms = 15;
int64 updated_at_ms = 16; int64 updated_at_ms = 16;
int32 safe_height = 17;
// whitelist_enabled game_user_whitelist
bool whitelist_enabled = 18;
} }
message AppGame { message AppGame {
@ -67,7 +64,6 @@ message AppGame {
bool enabled = 11; bool enabled = 11;
bool maintenance = 12; bool maintenance = 12;
int32 sort_order = 13; int32 sort_order = 13;
int32 safe_height = 14;
} }
message ListGamesRequest { message ListGamesRequest {
@ -85,53 +81,6 @@ message ListGamesResponse {
int64 server_time_ms = 2; int64 server_time_ms = 2;
} }
message ListRecentGamesRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string scene = 3;
string room_id = 4;
int64 region_id = 5;
string language = 6;
string client_platform = 7;
int32 page_size = 8;
}
message ListExploreWinnersRequest {
RequestMeta meta = 1;
int32 page_size = 2;
}
message ExploreWinner {
string win_id = 1;
string game_code = 2;
string game_id = 3;
int64 user_id = 4;
string display_name = 5;
string avatar_url = 6;
int64 coin_amount = 7;
int64 won_at_ms = 8;
}
message ListExploreWinnersResponse {
repeated ExploreWinner winners = 1;
int64 server_time_ms = 2;
}
message GetBridgeScriptRequest {
RequestMeta meta = 1;
}
message GameBridgeScriptConfig {
string bridge_script_url = 1;
string bridge_script_version = 2;
string bridge_script_sha256 = 3;
}
message GetBridgeScriptResponse {
GameBridgeScriptConfig config = 1;
int64 server_time_ms = 2;
}
message LaunchGameRequest { message LaunchGameRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
int64 user_id = 2; int64 user_id = 2;
@ -142,8 +91,6 @@ message LaunchGameRequest {
// access_token is the same bearer token used by the App request. // access_token is the same bearer token used by the App request.
// Provider adapters pass it through as the game token instead of minting a separate game token. // Provider adapters pass it through as the game token instead of minting a separate game token.
string access_token = 7; string access_token = 7;
// country_id gateway provider
int64 country_id = 8;
} }
message LaunchGameResponse { message LaunchGameResponse {
@ -152,273 +99,6 @@ message LaunchGameResponse {
int64 expires_at_ms = 3; int64 expires_at_ms = 3;
string orientation = 4; string orientation = 4;
int64 server_time_ms = 5; int64 server_time_ms = 5;
int32 safe_height = 6;
}
message DiceParticipant {
int64 user_id = 1;
int32 seat_no = 2;
string status = 3;
int64 stake_coin = 4;
repeated int32 dice_points = 5;
string result = 6;
int64 payout_coin = 7;
int64 balance_after = 8;
int64 joined_at_ms = 9;
int64 updated_at_ms = 10;
string participant_type = 11;
bool is_robot = 12;
string rps_gesture = 13;
}
message DiceMatch {
string app_code = 1;
string match_id = 2;
string game_id = 3;
string room_id = 4;
int64 region_id = 5;
int32 min_players = 6;
int32 max_players = 7;
int32 current_players = 8;
int64 stake_coin = 9;
int32 round_no = 10;
string status = 11;
string result = 12;
repeated DiceParticipant participants = 13;
int64 join_deadline_ms = 14;
int64 created_at_ms = 15;
int64 updated_at_ms = 16;
int64 settled_at_ms = 17;
string phase = 18;
int64 phase_deadline_ms = 19;
int32 fee_bps = 20;
int32 pool_bps = 21;
string match_mode = 22;
string forced_result = 23;
int64 ready_at_ms = 24;
int64 canceled_at_ms = 25;
int64 pool_delta_coin = 26;
// country_id
int64 country_id = 27;
}
message DiceStakeOption {
int64 stake_coin = 1;
bool enabled = 2;
int32 sort_order = 3;
}
message DiceConfig {
reserved 14;
string app_code = 1;
string game_id = 2;
string status = 3;
repeated DiceStakeOption stake_options = 4;
int32 fee_bps = 5;
int32 pool_bps = 6;
int32 min_players = 7;
int32 max_players = 8;
bool robot_enabled = 9;
int64 robot_match_wait_ms = 10;
int64 pool_balance_coin = 11;
int64 created_at_ms = 12;
int64 updated_at_ms = 13;
}
message GetDiceConfigRequest {
RequestMeta meta = 1;
string game_id = 2;
}
message DiceConfigResponse {
DiceConfig config = 1;
int64 server_time_ms = 2;
}
message RoomRPSStakeGift {
int64 gift_id = 1;
string gift_name = 2;
string gift_icon_url = 3;
int64 gift_price_coin = 4;
bool enabled = 5;
int32 sort_order = 6;
string gift_id_text = 7;
string gift_animation_url = 8;
}
message RoomRPSConfig {
string app_code = 1;
string game_id = 2;
string status = 3;
int64 challenge_timeout_ms = 4;
int64 reveal_countdown_ms = 5;
repeated RoomRPSStakeGift stake_gifts = 6;
int64 created_at_ms = 7;
int64 updated_at_ms = 8;
}
message RoomRPSPlayer {
int64 user_id = 1;
string gesture = 2;
string result = 3;
int64 balance_after = 4;
int64 joined_at_ms = 5;
}
message RoomRPSChallenge {
string app_code = 1;
string challenge_id = 2;
string room_id = 3;
int64 region_id = 4;
string status = 5;
int64 stake_gift_id = 6;
int64 stake_coin = 7;
RoomRPSPlayer initiator = 8;
RoomRPSPlayer challenger = 9;
int64 winner_user_id = 10;
string settlement_status = 11;
string failure_reason = 12;
int64 timeout_at_ms = 13;
int64 reveal_at_ms = 14;
int64 created_at_ms = 15;
int64 matched_at_ms = 16;
int64 settled_at_ms = 17;
int64 updated_at_ms = 18;
string stake_gift_id_text = 19;
}
message GetRoomRPSConfigRequest {
RequestMeta meta = 1;
}
message RoomRPSConfigResponse {
RoomRPSConfig config = 1;
int64 server_time_ms = 2;
}
message CreateRoomRPSChallengeRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string room_id = 3;
int64 region_id = 4;
int64 gift_id = 5;
string gesture = 6;
string gift_id_text = 7;
}
message AcceptRoomRPSChallengeRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string challenge_id = 3;
string gesture = 4;
}
message GetRoomRPSChallengeRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string challenge_id = 3;
}
message ListRoomRPSChallengesRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string room_id = 3;
string status = 4;
int64 initiator_user_id = 5;
int64 challenger_user_id = 6;
int64 start_time_ms = 7;
int64 end_time_ms = 8;
int32 page_size = 9;
string cursor = 10;
}
message RoomRPSChallengeResponse {
RoomRPSChallenge challenge = 1;
int64 server_time_ms = 2;
}
message ListRoomRPSChallengesResponse {
repeated RoomRPSChallenge challenges = 1;
string next_cursor = 2;
int32 page_size = 3;
int64 server_time_ms = 4;
}
message UpdateRoomRPSConfigRequest {
RequestMeta meta = 1;
RoomRPSConfig config = 2;
}
message RetryRoomRPSSettlementRequest {
RequestMeta meta = 1;
string challenge_id = 2;
}
message ExpireRoomRPSChallengeRequest {
RequestMeta meta = 1;
string challenge_id = 2;
}
message CreateDiceMatchRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string game_id = 3;
string room_id = 4;
int64 region_id = 5;
int64 stake_coin = 6;
int32 min_players = 7;
int32 max_players = 8;
string rps_gesture = 9;
// country_id gateway
int64 country_id = 10;
}
message JoinDiceMatchRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string match_id = 3;
string game_id = 4;
string rps_gesture = 5;
}
message GetDiceMatchRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string match_id = 3;
string game_id = 4;
}
message RollDiceMatchRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string match_id = 3;
string game_id = 4;
string rps_gesture = 5;
}
message MatchDiceRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string game_id = 3;
string room_id = 4;
int64 region_id = 5;
int64 stake_coin = 6;
string rps_gesture = 7;
// country_id gateway
int64 country_id = 8;
}
message CancelDiceMatchRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string match_id = 3;
string game_id = 4;
}
message DiceMatchResponse {
DiceMatch match = 1;
int64 server_time_ms = 2;
} }
message CallbackRequest { message CallbackRequest {
@ -511,244 +191,9 @@ message DeleteCatalogResponse {
int64 server_time_ms = 1; int64 server_time_ms = 1;
} }
message ListSelfGamesRequest {
RequestMeta meta = 1;
}
message ListSelfGamesResponse {
repeated DiceConfig games = 1;
int64 server_time_ms = 2;
}
message UpdateDiceConfigRequest {
RequestMeta meta = 1;
DiceConfig config = 2;
}
message SelfGameNewUserPolicy {
string app_code = 1;
string game_id = 2;
bool enabled = 3;
int32 protection_rounds = 4;
int32 protection_hours = 5;
int64 max_stake_coin = 6;
int64 lifetime_subsidy_quota_coin = 7;
int64 day1_subsidy_quota_coin = 8;
int64 single_round_subsidy_cap_coin = 9;
string strategy_level = 10;
bool lose_streak_protection_enabled = 11;
int32 lose_streak_trigger = 12;
int64 created_at_ms = 13;
int64 updated_at_ms = 14;
int32 normal_phase_target_win_rate_percent = 15;
bool black_pool_robot_force_win_enabled = 16;
}
message GetSelfGameNewUserPolicyRequest {
RequestMeta meta = 1;
string game_id = 2;
}
message UpdateSelfGameNewUserPolicyRequest {
RequestMeta meta = 1;
SelfGameNewUserPolicy policy = 2;
}
message SelfGameNewUserPolicyResponse {
SelfGameNewUserPolicy policy = 1;
int64 server_time_ms = 2;
}
message SelfGameStakePool {
string app_code = 1;
string game_id = 2;
int64 stake_coin = 3;
int64 balance_coin = 4;
int64 reserved_coin = 5;
int64 available_coin = 6;
int64 target_balance_coin = 7;
int64 safe_balance_coin = 8;
int64 warning_balance_coin = 9;
int64 danger_balance_coin = 10;
string status = 11;
string pool_level = 12;
int64 required_pool_coin = 13;
int64 human_win_capacity = 14;
bool bot_match_enabled = 15;
string black_reason = 16;
int64 created_at_ms = 17;
int64 updated_at_ms = 18;
}
message ListSelfGameStakePoolsRequest {
RequestMeta meta = 1;
string game_id = 2;
}
message UpdateSelfGameStakePoolRequest {
RequestMeta meta = 1;
SelfGameStakePool pool = 2;
}
message SelfGameStakePoolResponse {
SelfGameStakePool pool = 1;
int64 server_time_ms = 2;
}
message ListSelfGameStakePoolsResponse {
repeated SelfGameStakePool pools = 1;
int64 server_time_ms = 2;
}
message DicePoolAdjustment {
string adjustment_id = 1;
string game_id = 2;
int64 amount_coin = 3;
string direction = 4;
string reason = 5;
int64 balance_after = 6;
int64 created_at_ms = 7;
int64 stake_coin = 8;
}
message AdjustSelfGameStakePoolRequest {
RequestMeta meta = 1;
string game_id = 2;
int64 amount_coin = 3;
string direction = 4;
string reason = 5;
int64 stake_coin = 6;
}
message AdjustSelfGameStakePoolResponse {
DicePoolAdjustment adjustment = 1;
DiceConfig config = 2;
int64 server_time_ms = 3;
}
message DiceRobot {
string app_code = 1;
string game_id = 2;
int64 user_id = 3;
string status = 4;
int64 created_by_admin_id = 5;
int64 created_at_ms = 6;
int64 updated_at_ms = 7;
}
message ListDiceRobotsRequest {
RequestMeta meta = 1;
string game_id = 2;
string status = 3;
int32 page_size = 4;
string cursor = 5;
}
message ListDiceRobotsResponse {
repeated DiceRobot robots = 1;
string next_cursor = 2;
int64 server_time_ms = 3;
}
message RegisterDiceRobotsRequest {
RequestMeta meta = 1;
string game_id = 2;
repeated int64 user_ids = 3;
}
message RegisterDiceRobotsResponse {
repeated DiceRobot robots = 1;
int64 server_time_ms = 2;
}
message SetDiceRobotStatusRequest {
RequestMeta meta = 1;
string game_id = 2;
int64 user_id = 3;
string status = 4;
}
message DeleteDiceRobotRequest {
RequestMeta meta = 1;
string game_id = 2;
int64 user_id = 3;
}
message DeleteDiceRobotResponse {
bool deleted = 1;
int64 server_time_ms = 2;
}
message DiceRobotResponse {
DiceRobot robot = 1;
int64 server_time_ms = 2;
}
// GameWhitelistUser game-service 访 admin-server user-service
message GameWhitelistUser {
string app_code = 1;
string game_id = 2;
int64 user_id = 3;
int64 created_by_admin_id = 4;
int64 created_at_ms = 5;
}
message SetGameWhitelistEnabledRequest {
RequestMeta meta = 1;
string game_id = 2;
bool enabled = 3;
}
message ListGameWhitelistUsersRequest {
RequestMeta meta = 1;
string game_id = 2;
int32 page_size = 3;
}
message ListGameWhitelistUsersResponse {
repeated GameWhitelistUser users = 1;
int64 server_time_ms = 2;
}
message AddGameWhitelistUserRequest {
RequestMeta meta = 1;
string game_id = 2;
int64 user_id = 3;
}
message GameWhitelistUserResponse {
GameWhitelistUser user = 1;
int64 server_time_ms = 2;
}
message DeleteGameWhitelistUserRequest {
RequestMeta meta = 1;
string game_id = 2;
int64 user_id = 3;
}
message DeleteGameWhitelistUserResponse {
int64 server_time_ms = 1;
}
service GameAppService { service GameAppService {
rpc ListGames(ListGamesRequest) returns (ListGamesResponse); rpc ListGames(ListGamesRequest) returns (ListGamesResponse);
rpc ListRecentGames(ListRecentGamesRequest) returns (ListGamesResponse);
rpc ListExploreWinners(ListExploreWinnersRequest) returns (ListExploreWinnersResponse);
rpc GetBridgeScript(GetBridgeScriptRequest) returns (GetBridgeScriptResponse);
rpc LaunchGame(LaunchGameRequest) returns (LaunchGameResponse); rpc LaunchGame(LaunchGameRequest) returns (LaunchGameResponse);
rpc GetDiceConfig(GetDiceConfigRequest) returns (DiceConfigResponse);
rpc MatchDice(MatchDiceRequest) returns (DiceMatchResponse);
rpc CreateDiceMatch(CreateDiceMatchRequest) returns (DiceMatchResponse);
rpc JoinDiceMatch(JoinDiceMatchRequest) returns (DiceMatchResponse);
rpc GetDiceMatch(GetDiceMatchRequest) returns (DiceMatchResponse);
rpc RollDiceMatch(RollDiceMatchRequest) returns (DiceMatchResponse);
rpc CancelDiceMatch(CancelDiceMatchRequest) returns (DiceMatchResponse);
rpc GetRoomRPSConfig(GetRoomRPSConfigRequest) returns (RoomRPSConfigResponse);
rpc ListRoomRPSChallenges(ListRoomRPSChallengesRequest) returns (ListRoomRPSChallengesResponse);
rpc CreateRoomRPSChallenge(CreateRoomRPSChallengeRequest) returns (RoomRPSChallengeResponse);
rpc AcceptRoomRPSChallenge(AcceptRoomRPSChallengeRequest) returns (RoomRPSChallengeResponse);
rpc GetRoomRPSChallenge(GetRoomRPSChallengeRequest) returns (RoomRPSChallengeResponse);
} }
service GameCallbackService { service GameCallbackService {
@ -767,25 +212,4 @@ service GameAdminService {
rpc UpsertCatalog(UpsertCatalogRequest) returns (CatalogResponse); rpc UpsertCatalog(UpsertCatalogRequest) returns (CatalogResponse);
rpc SetGameStatus(SetGameStatusRequest) returns (CatalogResponse); rpc SetGameStatus(SetGameStatusRequest) returns (CatalogResponse);
rpc DeleteCatalog(DeleteCatalogRequest) returns (DeleteCatalogResponse); rpc DeleteCatalog(DeleteCatalogRequest) returns (DeleteCatalogResponse);
rpc SetGameWhitelistEnabled(SetGameWhitelistEnabledRequest) returns (CatalogResponse);
rpc ListGameWhitelistUsers(ListGameWhitelistUsersRequest) returns (ListGameWhitelistUsersResponse);
rpc AddGameWhitelistUser(AddGameWhitelistUserRequest) returns (GameWhitelistUserResponse);
rpc DeleteGameWhitelistUser(DeleteGameWhitelistUserRequest) returns (DeleteGameWhitelistUserResponse);
rpc ListSelfGames(ListSelfGamesRequest) returns (ListSelfGamesResponse);
rpc UpdateDiceConfig(UpdateDiceConfigRequest) returns (DiceConfigResponse);
rpc GetSelfGameNewUserPolicy(GetSelfGameNewUserPolicyRequest) returns (SelfGameNewUserPolicyResponse);
rpc UpdateSelfGameNewUserPolicy(UpdateSelfGameNewUserPolicyRequest) returns (SelfGameNewUserPolicyResponse);
rpc ListSelfGameStakePools(ListSelfGameStakePoolsRequest) returns (ListSelfGameStakePoolsResponse);
rpc UpdateSelfGameStakePool(UpdateSelfGameStakePoolRequest) returns (SelfGameStakePoolResponse);
rpc AdjustSelfGameStakePool(AdjustSelfGameStakePoolRequest) returns (AdjustSelfGameStakePoolResponse);
rpc ListDiceRobots(ListDiceRobotsRequest) returns (ListDiceRobotsResponse);
rpc RegisterDiceRobots(RegisterDiceRobotsRequest) returns (RegisterDiceRobotsResponse);
rpc SetDiceRobotStatus(SetDiceRobotStatusRequest) returns (DiceRobotResponse);
rpc DeleteDiceRobot(DeleteDiceRobotRequest) returns (DeleteDiceRobotResponse);
rpc GetRoomRPSConfig(GetRoomRPSConfigRequest) returns (RoomRPSConfigResponse);
rpc UpdateRoomRPSConfig(UpdateRoomRPSConfigRequest) returns (RoomRPSConfigResponse);
rpc ListRoomRPSChallenges(ListRoomRPSChallengesRequest) returns (ListRoomRPSChallengesResponse);
rpc GetRoomRPSChallenge(GetRoomRPSChallengeRequest) returns (RoomRPSChallengeResponse);
rpc RetryRoomRPSSettlement(RetryRoomRPSSettlementRequest) returns (RoomRPSChallengeResponse);
rpc ExpireRoomRPSChallenge(ExpireRoomRPSChallengeRequest) returns (RoomRPSChallengeResponse);
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,285 +0,0 @@
syntax = "proto3";
package hyapp.luckygift.v1;
option go_package = "hyapp.local/api/proto/luckygift/v1;luckygiftv1";
// RequestMeta lucky-gift-service
// request_id 使
message RequestMeta {
string request_id = 1;
string caller = 2;
string gateway_node_id = 3;
int64 sent_at_ms = 4;
string app_code = 5;
}
// LuckyGiftMeta HyApp
// command_id wallet-service receipt payload
message LuckyGiftMeta {
RequestMeta meta = 1;
string command_id = 2;
int64 user_id = 3;
string device_id = 4;
string room_id = 5;
string anchor_id = 6;
string gift_id = 7;
int64 coin_spent = 8;
int64 paid_at_ms = 9;
string pool_id = 10;
int64 target_user_id = 11;
int32 gift_count = 12;
int64 visible_region_id = 13;
int64 country_id = 14;
// sender_* room-service IM
string sender_name = 15;
string sender_avatar = 16;
string sender_display_user_id = 17;
string sender_pretty_display_user_id = 18;
}
message LuckyGiftRuleTier {
string stage = 1;
string tier_id = 2;
int64 multiplier_ppm = 3;
int64 base_weight_ppm = 4;
bool high_water_only = 5;
bool enabled = 6;
}
message LuckyGiftRuleStage {
string stage = 1;
repeated LuckyGiftRuleTier tiers = 2;
}
message LuckyGiftRuleConfig {
string app_code = 1;
string pool_id = 2;
int64 rule_version = 3;
bool enabled = 4;
int64 target_rtp_ppm = 5;
int64 pool_rate_ppm = 6;
int64 settlement_window_wager = 7;
int64 control_band_ppm = 8;
int64 gift_price_reference = 9;
int64 novice_max_equivalent_draws = 10;
int64 normal_max_equivalent_draws = 11;
int64 effective_from_ms = 12;
int64 created_by_admin_id = 13;
int64 created_at_ms = 14;
repeated LuckyGiftRuleStage stages = 15;
}
message CheckLuckyGiftRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string room_id = 3;
string gift_id = 4;
string pool_id = 5;
}
message CheckLuckyGiftResponse {
bool enabled = 1;
string reason = 2;
string gift_id = 3;
int64 gift_price = 4;
int64 rule_version = 5;
int64 target_rtp_ppm = 6;
string experience_pool = 7;
string pool_id = 8;
}
message LuckyGiftDrawResult {
string draw_id = 1;
string command_id = 2;
string gift_id = 3;
int64 rule_version = 4;
string experience_pool = 5;
string selected_tier_id = 6;
int64 base_reward_coins = 7;
int64 effective_reward_coins = 8;
string reward_status = 9;
int64 rtp_window_index = 10;
int64 gift_rtp_window_index = 11;
int64 global_base_rtp_ppm = 12;
int64 gift_base_rtp_ppm = 13;
bool stage_feedback = 14;
bool high_multiplier = 15;
int64 created_at_ms = 16;
string pool_id = 17;
int64 multiplier_ppm = 18;
string wallet_transaction_id = 19;
int64 coin_balance_after = 20;
int64 user_id = 21;
string external_user_id = 22;
string app_code = 23;
repeated LuckyGiftHit hits = 24;
}
message ExecuteLuckyGiftDrawRequest {
LuckyGiftMeta lucky_gift = 1;
}
message ExecuteLuckyGiftDrawResponse {
LuckyGiftDrawResult result = 1;
}
message BatchExecuteLuckyGiftDrawRequest {
repeated LuckyGiftMeta lucky_gifts = 1;
}
message BatchExecuteLuckyGiftDrawResponse {
repeated LuckyGiftDrawResult results = 1;
}
// ExternalGiftDrawRequest App
// App lucky-gift-service
message ExternalGiftDrawRequest {
RequestMeta meta = 1;
string app_code = 3;
string external_user_id = 4;
string request_id = 5;
int64 gift_count = 6;
int64 unit_amount = 7;
int64 total_amount = 8;
string currency = 9;
int64 paid_at_ms = 10;
string metadata_json = 11;
string pool_id = 12;
}
message ExternalGiftDrawResponse {
string draw_id = 1;
string request_id = 2;
string app_code = 4;
string external_user_id = 5;
int64 gift_count = 6;
int64 unit_amount = 7;
int64 total_amount = 8;
int64 reward_amount = 9;
int64 multiplier_ppm = 10;
string reward_status = 11;
int64 rule_version = 12;
int64 created_at_ms = 13;
}
message GetLuckyGiftConfigRequest {
RequestMeta meta = 1;
string pool_id = 2;
}
message GetLuckyGiftConfigResponse {
LuckyGiftRuleConfig config = 1;
}
message UpsertLuckyGiftConfigRequest {
RequestMeta meta = 1;
LuckyGiftRuleConfig config = 2;
int64 operator_admin_id = 3;
}
message UpsertLuckyGiftConfigResponse {
LuckyGiftRuleConfig config = 1;
}
message ListLuckyGiftConfigsRequest {
RequestMeta meta = 1;
}
message ListLuckyGiftConfigsResponse {
repeated LuckyGiftRuleConfig configs = 1;
}
message ListLuckyGiftDrawsRequest {
RequestMeta meta = 1;
string gift_id = 2;
int64 user_id = 3;
string room_id = 4;
string status = 5;
int32 page = 6;
int32 page_size = 7;
string pool_id = 8;
string external_user_id = 10;
bool external_only = 11;
}
message ListLuckyGiftDrawsResponse {
repeated LuckyGiftDrawResult draws = 1;
int64 total = 2;
}
message LuckyGiftDrawSummary {
string pool_id = 1;
int64 total_draws = 2;
int64 unique_users = 3;
int64 unique_rooms = 4;
int64 total_spent_coins = 5;
int64 total_reward_coins = 6;
int64 base_reward_coins = 7;
int64 actual_rtp_ppm = 8;
int64 pending_draws = 9;
int64 granted_draws = 10;
int64 failed_draws = 11;
}
message LuckyGiftPoolBalance {
string app_code = 1;
string pool_id = 2;
int64 balance = 3;
int64 reserve_floor = 4;
int64 available_balance = 5;
int64 total_in = 6;
int64 total_out = 7;
bool materialized = 8;
int64 updated_at_ms = 9;
}
message GetLuckyGiftDrawSummaryRequest {
RequestMeta meta = 1;
string gift_id = 2;
int64 user_id = 3;
string room_id = 4;
string status = 5;
string pool_id = 6;
string external_user_id = 8;
bool external_only = 9;
}
message GetLuckyGiftDrawSummaryResponse {
LuckyGiftDrawSummary summary = 1;
}
message ListLuckyGiftPoolBalancesRequest {
RequestMeta meta = 1;
string pool_id = 2;
}
message ListLuckyGiftPoolBalancesResponse {
repeated LuckyGiftPoolBalance pools = 1;
}
service LuckyGiftService {
rpc CheckLuckyGift(CheckLuckyGiftRequest) returns (CheckLuckyGiftResponse);
rpc ExecuteLuckyGiftDraw(ExecuteLuckyGiftDrawRequest) returns (ExecuteLuckyGiftDrawResponse);
rpc BatchExecuteLuckyGiftDraw(BatchExecuteLuckyGiftDrawRequest) returns (BatchExecuteLuckyGiftDrawResponse);
rpc ExecuteExternalGiftDraw(ExternalGiftDrawRequest) returns (ExternalGiftDrawResponse);
}
service AdminLuckyGiftService {
rpc GetLuckyGiftConfig(GetLuckyGiftConfigRequest) returns (GetLuckyGiftConfigResponse);
rpc UpsertLuckyGiftConfig(UpsertLuckyGiftConfigRequest) returns (UpsertLuckyGiftConfigResponse);
rpc ListLuckyGiftConfigs(ListLuckyGiftConfigsRequest) returns (ListLuckyGiftConfigsResponse);
rpc ListLuckyGiftDraws(ListLuckyGiftDrawsRequest) returns (ListLuckyGiftDrawsResponse);
rpc GetLuckyGiftDrawSummary(GetLuckyGiftDrawSummaryRequest) returns (GetLuckyGiftDrawSummaryResponse);
rpc ListLuckyGiftPoolBalances(ListLuckyGiftPoolBalancesRequest) returns (ListLuckyGiftPoolBalancesResponse);
}
// LuckyGiftHit
// protobuf message index
message LuckyGiftHit {
int32 gift_index = 1;
string draw_id = 2;
string selected_tier_id = 3;
int64 multiplier_ppm = 4;
int64 reward_coins = 5;
}

View File

@ -1,527 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc v5.29.2
// source: proto/luckygift/v1/luckygift.proto
package luckygiftv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
LuckyGiftService_CheckLuckyGift_FullMethodName = "/hyapp.luckygift.v1.LuckyGiftService/CheckLuckyGift"
LuckyGiftService_ExecuteLuckyGiftDraw_FullMethodName = "/hyapp.luckygift.v1.LuckyGiftService/ExecuteLuckyGiftDraw"
LuckyGiftService_BatchExecuteLuckyGiftDraw_FullMethodName = "/hyapp.luckygift.v1.LuckyGiftService/BatchExecuteLuckyGiftDraw"
LuckyGiftService_ExecuteExternalGiftDraw_FullMethodName = "/hyapp.luckygift.v1.LuckyGiftService/ExecuteExternalGiftDraw"
)
// LuckyGiftServiceClient is the client API for LuckyGiftService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type LuckyGiftServiceClient interface {
CheckLuckyGift(ctx context.Context, in *CheckLuckyGiftRequest, opts ...grpc.CallOption) (*CheckLuckyGiftResponse, error)
ExecuteLuckyGiftDraw(ctx context.Context, in *ExecuteLuckyGiftDrawRequest, opts ...grpc.CallOption) (*ExecuteLuckyGiftDrawResponse, error)
BatchExecuteLuckyGiftDraw(ctx context.Context, in *BatchExecuteLuckyGiftDrawRequest, opts ...grpc.CallOption) (*BatchExecuteLuckyGiftDrawResponse, error)
ExecuteExternalGiftDraw(ctx context.Context, in *ExternalGiftDrawRequest, opts ...grpc.CallOption) (*ExternalGiftDrawResponse, error)
}
type luckyGiftServiceClient struct {
cc grpc.ClientConnInterface
}
func NewLuckyGiftServiceClient(cc grpc.ClientConnInterface) LuckyGiftServiceClient {
return &luckyGiftServiceClient{cc}
}
func (c *luckyGiftServiceClient) CheckLuckyGift(ctx context.Context, in *CheckLuckyGiftRequest, opts ...grpc.CallOption) (*CheckLuckyGiftResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CheckLuckyGiftResponse)
err := c.cc.Invoke(ctx, LuckyGiftService_CheckLuckyGift_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *luckyGiftServiceClient) ExecuteLuckyGiftDraw(ctx context.Context, in *ExecuteLuckyGiftDrawRequest, opts ...grpc.CallOption) (*ExecuteLuckyGiftDrawResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ExecuteLuckyGiftDrawResponse)
err := c.cc.Invoke(ctx, LuckyGiftService_ExecuteLuckyGiftDraw_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *luckyGiftServiceClient) BatchExecuteLuckyGiftDraw(ctx context.Context, in *BatchExecuteLuckyGiftDrawRequest, opts ...grpc.CallOption) (*BatchExecuteLuckyGiftDrawResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(BatchExecuteLuckyGiftDrawResponse)
err := c.cc.Invoke(ctx, LuckyGiftService_BatchExecuteLuckyGiftDraw_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *luckyGiftServiceClient) ExecuteExternalGiftDraw(ctx context.Context, in *ExternalGiftDrawRequest, opts ...grpc.CallOption) (*ExternalGiftDrawResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ExternalGiftDrawResponse)
err := c.cc.Invoke(ctx, LuckyGiftService_ExecuteExternalGiftDraw_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// LuckyGiftServiceServer is the server API for LuckyGiftService service.
// All implementations must embed UnimplementedLuckyGiftServiceServer
// for forward compatibility.
type LuckyGiftServiceServer interface {
CheckLuckyGift(context.Context, *CheckLuckyGiftRequest) (*CheckLuckyGiftResponse, error)
ExecuteLuckyGiftDraw(context.Context, *ExecuteLuckyGiftDrawRequest) (*ExecuteLuckyGiftDrawResponse, error)
BatchExecuteLuckyGiftDraw(context.Context, *BatchExecuteLuckyGiftDrawRequest) (*BatchExecuteLuckyGiftDrawResponse, error)
ExecuteExternalGiftDraw(context.Context, *ExternalGiftDrawRequest) (*ExternalGiftDrawResponse, error)
mustEmbedUnimplementedLuckyGiftServiceServer()
}
// UnimplementedLuckyGiftServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedLuckyGiftServiceServer struct{}
func (UnimplementedLuckyGiftServiceServer) CheckLuckyGift(context.Context, *CheckLuckyGiftRequest) (*CheckLuckyGiftResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CheckLuckyGift not implemented")
}
func (UnimplementedLuckyGiftServiceServer) ExecuteLuckyGiftDraw(context.Context, *ExecuteLuckyGiftDrawRequest) (*ExecuteLuckyGiftDrawResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ExecuteLuckyGiftDraw not implemented")
}
func (UnimplementedLuckyGiftServiceServer) BatchExecuteLuckyGiftDraw(context.Context, *BatchExecuteLuckyGiftDrawRequest) (*BatchExecuteLuckyGiftDrawResponse, error) {
return nil, status.Error(codes.Unimplemented, "method BatchExecuteLuckyGiftDraw not implemented")
}
func (UnimplementedLuckyGiftServiceServer) ExecuteExternalGiftDraw(context.Context, *ExternalGiftDrawRequest) (*ExternalGiftDrawResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ExecuteExternalGiftDraw not implemented")
}
func (UnimplementedLuckyGiftServiceServer) mustEmbedUnimplementedLuckyGiftServiceServer() {}
func (UnimplementedLuckyGiftServiceServer) testEmbeddedByValue() {}
// UnsafeLuckyGiftServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to LuckyGiftServiceServer will
// result in compilation errors.
type UnsafeLuckyGiftServiceServer interface {
mustEmbedUnimplementedLuckyGiftServiceServer()
}
func RegisterLuckyGiftServiceServer(s grpc.ServiceRegistrar, srv LuckyGiftServiceServer) {
// If the following call panics, it indicates UnimplementedLuckyGiftServiceServer 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.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&LuckyGiftService_ServiceDesc, srv)
}
func _LuckyGiftService_CheckLuckyGift_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CheckLuckyGiftRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(LuckyGiftServiceServer).CheckLuckyGift(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: LuckyGiftService_CheckLuckyGift_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(LuckyGiftServiceServer).CheckLuckyGift(ctx, req.(*CheckLuckyGiftRequest))
}
return interceptor(ctx, in, info, handler)
}
func _LuckyGiftService_ExecuteLuckyGiftDraw_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ExecuteLuckyGiftDrawRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(LuckyGiftServiceServer).ExecuteLuckyGiftDraw(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: LuckyGiftService_ExecuteLuckyGiftDraw_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(LuckyGiftServiceServer).ExecuteLuckyGiftDraw(ctx, req.(*ExecuteLuckyGiftDrawRequest))
}
return interceptor(ctx, in, info, handler)
}
func _LuckyGiftService_BatchExecuteLuckyGiftDraw_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BatchExecuteLuckyGiftDrawRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(LuckyGiftServiceServer).BatchExecuteLuckyGiftDraw(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: LuckyGiftService_BatchExecuteLuckyGiftDraw_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(LuckyGiftServiceServer).BatchExecuteLuckyGiftDraw(ctx, req.(*BatchExecuteLuckyGiftDrawRequest))
}
return interceptor(ctx, in, info, handler)
}
func _LuckyGiftService_ExecuteExternalGiftDraw_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ExternalGiftDrawRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(LuckyGiftServiceServer).ExecuteExternalGiftDraw(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: LuckyGiftService_ExecuteExternalGiftDraw_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(LuckyGiftServiceServer).ExecuteExternalGiftDraw(ctx, req.(*ExternalGiftDrawRequest))
}
return interceptor(ctx, in, info, handler)
}
// LuckyGiftService_ServiceDesc is the grpc.ServiceDesc for LuckyGiftService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var LuckyGiftService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "hyapp.luckygift.v1.LuckyGiftService",
HandlerType: (*LuckyGiftServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CheckLuckyGift",
Handler: _LuckyGiftService_CheckLuckyGift_Handler,
},
{
MethodName: "ExecuteLuckyGiftDraw",
Handler: _LuckyGiftService_ExecuteLuckyGiftDraw_Handler,
},
{
MethodName: "BatchExecuteLuckyGiftDraw",
Handler: _LuckyGiftService_BatchExecuteLuckyGiftDraw_Handler,
},
{
MethodName: "ExecuteExternalGiftDraw",
Handler: _LuckyGiftService_ExecuteExternalGiftDraw_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/luckygift/v1/luckygift.proto",
}
const (
AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftConfig"
AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/UpsertLuckyGiftConfig"
AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftConfigs"
AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftDraws"
AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftDrawSummary"
AdminLuckyGiftService_ListLuckyGiftPoolBalances_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftPoolBalances"
)
// AdminLuckyGiftServiceClient is the client API for AdminLuckyGiftService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type AdminLuckyGiftServiceClient interface {
GetLuckyGiftConfig(ctx context.Context, in *GetLuckyGiftConfigRequest, opts ...grpc.CallOption) (*GetLuckyGiftConfigResponse, error)
UpsertLuckyGiftConfig(ctx context.Context, in *UpsertLuckyGiftConfigRequest, opts ...grpc.CallOption) (*UpsertLuckyGiftConfigResponse, error)
ListLuckyGiftConfigs(ctx context.Context, in *ListLuckyGiftConfigsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigsResponse, error)
ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error)
GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error)
ListLuckyGiftPoolBalances(ctx context.Context, in *ListLuckyGiftPoolBalancesRequest, opts ...grpc.CallOption) (*ListLuckyGiftPoolBalancesResponse, error)
}
type adminLuckyGiftServiceClient struct {
cc grpc.ClientConnInterface
}
func NewAdminLuckyGiftServiceClient(cc grpc.ClientConnInterface) AdminLuckyGiftServiceClient {
return &adminLuckyGiftServiceClient{cc}
}
func (c *adminLuckyGiftServiceClient) GetLuckyGiftConfig(ctx context.Context, in *GetLuckyGiftConfigRequest, opts ...grpc.CallOption) (*GetLuckyGiftConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetLuckyGiftConfigResponse)
err := c.cc.Invoke(ctx, AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *adminLuckyGiftServiceClient) UpsertLuckyGiftConfig(ctx context.Context, in *UpsertLuckyGiftConfigRequest, opts ...grpc.CallOption) (*UpsertLuckyGiftConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpsertLuckyGiftConfigResponse)
err := c.cc.Invoke(ctx, AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *adminLuckyGiftServiceClient) ListLuckyGiftConfigs(ctx context.Context, in *ListLuckyGiftConfigsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListLuckyGiftConfigsResponse)
err := c.cc.Invoke(ctx, AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *adminLuckyGiftServiceClient) ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListLuckyGiftDrawsResponse)
err := c.cc.Invoke(ctx, AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *adminLuckyGiftServiceClient) GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetLuckyGiftDrawSummaryResponse)
err := c.cc.Invoke(ctx, AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *adminLuckyGiftServiceClient) ListLuckyGiftPoolBalances(ctx context.Context, in *ListLuckyGiftPoolBalancesRequest, opts ...grpc.CallOption) (*ListLuckyGiftPoolBalancesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListLuckyGiftPoolBalancesResponse)
err := c.cc.Invoke(ctx, AdminLuckyGiftService_ListLuckyGiftPoolBalances_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AdminLuckyGiftServiceServer is the server API for AdminLuckyGiftService service.
// All implementations must embed UnimplementedAdminLuckyGiftServiceServer
// for forward compatibility.
type AdminLuckyGiftServiceServer interface {
GetLuckyGiftConfig(context.Context, *GetLuckyGiftConfigRequest) (*GetLuckyGiftConfigResponse, error)
UpsertLuckyGiftConfig(context.Context, *UpsertLuckyGiftConfigRequest) (*UpsertLuckyGiftConfigResponse, error)
ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error)
ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error)
GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error)
ListLuckyGiftPoolBalances(context.Context, *ListLuckyGiftPoolBalancesRequest) (*ListLuckyGiftPoolBalancesResponse, error)
mustEmbedUnimplementedAdminLuckyGiftServiceServer()
}
// UnimplementedAdminLuckyGiftServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedAdminLuckyGiftServiceServer struct{}
func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftConfig(context.Context, *GetLuckyGiftConfigRequest) (*GetLuckyGiftConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetLuckyGiftConfig not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) UpsertLuckyGiftConfig(context.Context, *UpsertLuckyGiftConfigRequest) (*UpsertLuckyGiftConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpsertLuckyGiftConfig not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftConfigs not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftDraws not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetLuckyGiftDrawSummary not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftPoolBalances(context.Context, *ListLuckyGiftPoolBalancesRequest) (*ListLuckyGiftPoolBalancesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftPoolBalances not implemented")
}
func (UnimplementedAdminLuckyGiftServiceServer) mustEmbedUnimplementedAdminLuckyGiftServiceServer() {}
func (UnimplementedAdminLuckyGiftServiceServer) testEmbeddedByValue() {}
// UnsafeAdminLuckyGiftServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AdminLuckyGiftServiceServer will
// result in compilation errors.
type UnsafeAdminLuckyGiftServiceServer interface {
mustEmbedUnimplementedAdminLuckyGiftServiceServer()
}
func RegisterAdminLuckyGiftServiceServer(s grpc.ServiceRegistrar, srv AdminLuckyGiftServiceServer) {
// If the following call panics, it indicates UnimplementedAdminLuckyGiftServiceServer 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.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&AdminLuckyGiftService_ServiceDesc, srv)
}
func _AdminLuckyGiftService_GetLuckyGiftConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetLuckyGiftConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftConfig(ctx, req.(*GetLuckyGiftConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AdminLuckyGiftService_UpsertLuckyGiftConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpsertLuckyGiftConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdminLuckyGiftServiceServer).UpsertLuckyGiftConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdminLuckyGiftServiceServer).UpsertLuckyGiftConfig(ctx, req.(*UpsertLuckyGiftConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AdminLuckyGiftService_ListLuckyGiftConfigs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListLuckyGiftConfigsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftConfigs(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftConfigs(ctx, req.(*ListLuckyGiftConfigsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AdminLuckyGiftService_ListLuckyGiftDraws_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListLuckyGiftDrawsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftDraws(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftDraws(ctx, req.(*ListLuckyGiftDrawsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AdminLuckyGiftService_GetLuckyGiftDrawSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetLuckyGiftDrawSummaryRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftDrawSummary(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftDrawSummary(ctx, req.(*GetLuckyGiftDrawSummaryRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AdminLuckyGiftService_ListLuckyGiftPoolBalances_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListLuckyGiftPoolBalancesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftPoolBalances(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AdminLuckyGiftService_ListLuckyGiftPoolBalances_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftPoolBalances(ctx, req.(*ListLuckyGiftPoolBalancesRequest))
}
return interceptor(ctx, in, info, handler)
}
// AdminLuckyGiftService_ServiceDesc is the grpc.ServiceDesc for AdminLuckyGiftService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var AdminLuckyGiftService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "hyapp.luckygift.v1.AdminLuckyGiftService",
HandlerType: (*AdminLuckyGiftServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetLuckyGiftConfig",
Handler: _AdminLuckyGiftService_GetLuckyGiftConfig_Handler,
},
{
MethodName: "UpsertLuckyGiftConfig",
Handler: _AdminLuckyGiftService_UpsertLuckyGiftConfig_Handler,
},
{
MethodName: "ListLuckyGiftConfigs",
Handler: _AdminLuckyGiftService_ListLuckyGiftConfigs_Handler,
},
{
MethodName: "ListLuckyGiftDraws",
Handler: _AdminLuckyGiftService_ListLuckyGiftDraws_Handler,
},
{
MethodName: "GetLuckyGiftDrawSummary",
Handler: _AdminLuckyGiftService_GetLuckyGiftDrawSummary_Handler,
},
{
MethodName: "ListLuckyGiftPoolBalances",
Handler: _AdminLuckyGiftService_ListLuckyGiftPoolBalances_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/luckygift/v1/luckygift.proto",
}

File diff suppressed because it is too large Load Diff

View File

@ -1,155 +0,0 @@
syntax = "proto3";
package hyapp.robot.v1;
option go_package = "hyapp.local/api/proto/robot/v1;robotv1";
// RequestMeta robot-service RPC app_code
message RequestMeta {
string request_id = 1;
string caller = 2;
int64 actor_user_id = 3;
int64 sent_at_ms = 4;
string app_code = 5;
}
message Robot {
string app_code = 1;
string robot_scope = 2;
string game_id = 3;
string room_scene = 4;
int64 user_id = 5;
string status = 6;
int64 created_by_admin_id = 7;
int64 created_at_ms = 8;
int64 updated_at_ms = 9;
int64 last_used_at_ms = 10;
int64 used_count = 11;
}
message ListGameRobotsRequest {
RequestMeta meta = 1;
string game_id = 2;
string status = 3;
int32 page_size = 4;
string cursor = 5;
}
message ListGameRobotsResponse {
repeated Robot robots = 1;
string next_cursor = 2;
int64 server_time_ms = 3;
}
message RegisterGameRobotsRequest {
RequestMeta meta = 1;
string game_id = 2;
repeated int64 user_ids = 3;
}
message RegisterGameRobotsResponse {
repeated Robot robots = 1;
int64 server_time_ms = 2;
}
message PickGameRobotRequest {
RequestMeta meta = 1;
string game_id = 2;
string match_id = 3;
int64 selected_at_ms = 4;
repeated int64 exclude_user_ids = 5;
}
message GameRobotResponse {
Robot robot = 1;
int64 server_time_ms = 2;
}
message SetGameRobotStatusRequest {
RequestMeta meta = 1;
string game_id = 2;
int64 user_id = 3;
string status = 4;
}
message DeleteGameRobotRequest {
RequestMeta meta = 1;
string game_id = 2;
int64 user_id = 3;
}
message DeleteGameRobotResponse {
bool deleted = 1;
int64 server_time_ms = 2;
}
message ListRoomRobotsRequest {
RequestMeta meta = 1;
string room_scene = 2;
string status = 3;
int32 page_size = 4;
string cursor = 5;
}
message ListRoomRobotsResponse {
repeated Robot robots = 1;
string next_cursor = 2;
int64 server_time_ms = 3;
}
message RegisterRoomRobotsRequest {
RequestMeta meta = 1;
string room_scene = 2;
repeated int64 user_ids = 3;
}
message RegisterRoomRobotsResponse {
repeated Robot robots = 1;
int64 server_time_ms = 2;
}
message SetRoomRobotStatusRequest {
RequestMeta meta = 1;
string room_scene = 2;
int64 user_id = 3;
string status = 4;
}
message DeleteRoomRobotRequest {
RequestMeta meta = 1;
string room_scene = 2;
int64 user_id = 3;
}
message DeleteRoomRobotResponse {
bool deleted = 1;
int64 server_time_ms = 2;
}
message PickRoomRobotRequest {
RequestMeta meta = 1;
string room_scene = 2;
string room_id = 3;
int64 selected_at_ms = 4;
}
message RoomRobotResponse {
Robot robot = 1;
int64 server_time_ms = 2;
}
service GameRobotService {
rpc ListGameRobots(ListGameRobotsRequest) returns (ListGameRobotsResponse);
rpc RegisterGameRobots(RegisterGameRobotsRequest) returns (RegisterGameRobotsResponse);
rpc PickGameRobot(PickGameRobotRequest) returns (GameRobotResponse);
rpc SetGameRobotStatus(SetGameRobotStatusRequest) returns (GameRobotResponse);
rpc DeleteGameRobot(DeleteGameRobotRequest) returns (DeleteGameRobotResponse);
}
service RoomRobotService {
rpc ListRoomRobots(ListRoomRobotsRequest) returns (ListRoomRobotsResponse);
rpc RegisterRoomRobots(RegisterRoomRobotsRequest) returns (RegisterRoomRobotsResponse);
rpc PickRoomRobot(PickRoomRobotRequest) returns (RoomRobotResponse);
rpc SetRoomRobotStatus(SetRoomRobotStatusRequest) returns (RoomRobotResponse);
rpc DeleteRoomRobot(DeleteRoomRobotRequest) returns (DeleteRoomRobotResponse);
}

View File

@ -1,527 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.2
// - protoc v5.29.2
// source: proto/robot/v1/robot.proto
package robotv1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
GameRobotService_ListGameRobots_FullMethodName = "/hyapp.robot.v1.GameRobotService/ListGameRobots"
GameRobotService_RegisterGameRobots_FullMethodName = "/hyapp.robot.v1.GameRobotService/RegisterGameRobots"
GameRobotService_PickGameRobot_FullMethodName = "/hyapp.robot.v1.GameRobotService/PickGameRobot"
GameRobotService_SetGameRobotStatus_FullMethodName = "/hyapp.robot.v1.GameRobotService/SetGameRobotStatus"
GameRobotService_DeleteGameRobot_FullMethodName = "/hyapp.robot.v1.GameRobotService/DeleteGameRobot"
)
// GameRobotServiceClient is the client API for GameRobotService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type GameRobotServiceClient interface {
ListGameRobots(ctx context.Context, in *ListGameRobotsRequest, opts ...grpc.CallOption) (*ListGameRobotsResponse, error)
RegisterGameRobots(ctx context.Context, in *RegisterGameRobotsRequest, opts ...grpc.CallOption) (*RegisterGameRobotsResponse, error)
PickGameRobot(ctx context.Context, in *PickGameRobotRequest, opts ...grpc.CallOption) (*GameRobotResponse, error)
SetGameRobotStatus(ctx context.Context, in *SetGameRobotStatusRequest, opts ...grpc.CallOption) (*GameRobotResponse, error)
DeleteGameRobot(ctx context.Context, in *DeleteGameRobotRequest, opts ...grpc.CallOption) (*DeleteGameRobotResponse, error)
}
type gameRobotServiceClient struct {
cc grpc.ClientConnInterface
}
func NewGameRobotServiceClient(cc grpc.ClientConnInterface) GameRobotServiceClient {
return &gameRobotServiceClient{cc}
}
func (c *gameRobotServiceClient) ListGameRobots(ctx context.Context, in *ListGameRobotsRequest, opts ...grpc.CallOption) (*ListGameRobotsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListGameRobotsResponse)
err := c.cc.Invoke(ctx, GameRobotService_ListGameRobots_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *gameRobotServiceClient) RegisterGameRobots(ctx context.Context, in *RegisterGameRobotsRequest, opts ...grpc.CallOption) (*RegisterGameRobotsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RegisterGameRobotsResponse)
err := c.cc.Invoke(ctx, GameRobotService_RegisterGameRobots_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *gameRobotServiceClient) PickGameRobot(ctx context.Context, in *PickGameRobotRequest, opts ...grpc.CallOption) (*GameRobotResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GameRobotResponse)
err := c.cc.Invoke(ctx, GameRobotService_PickGameRobot_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *gameRobotServiceClient) SetGameRobotStatus(ctx context.Context, in *SetGameRobotStatusRequest, opts ...grpc.CallOption) (*GameRobotResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GameRobotResponse)
err := c.cc.Invoke(ctx, GameRobotService_SetGameRobotStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *gameRobotServiceClient) DeleteGameRobot(ctx context.Context, in *DeleteGameRobotRequest, opts ...grpc.CallOption) (*DeleteGameRobotResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DeleteGameRobotResponse)
err := c.cc.Invoke(ctx, GameRobotService_DeleteGameRobot_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// GameRobotServiceServer is the server API for GameRobotService service.
// All implementations must embed UnimplementedGameRobotServiceServer
// for forward compatibility.
type GameRobotServiceServer interface {
ListGameRobots(context.Context, *ListGameRobotsRequest) (*ListGameRobotsResponse, error)
RegisterGameRobots(context.Context, *RegisterGameRobotsRequest) (*RegisterGameRobotsResponse, error)
PickGameRobot(context.Context, *PickGameRobotRequest) (*GameRobotResponse, error)
SetGameRobotStatus(context.Context, *SetGameRobotStatusRequest) (*GameRobotResponse, error)
DeleteGameRobot(context.Context, *DeleteGameRobotRequest) (*DeleteGameRobotResponse, error)
mustEmbedUnimplementedGameRobotServiceServer()
}
// UnimplementedGameRobotServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedGameRobotServiceServer struct{}
func (UnimplementedGameRobotServiceServer) ListGameRobots(context.Context, *ListGameRobotsRequest) (*ListGameRobotsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListGameRobots not implemented")
}
func (UnimplementedGameRobotServiceServer) RegisterGameRobots(context.Context, *RegisterGameRobotsRequest) (*RegisterGameRobotsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RegisterGameRobots not implemented")
}
func (UnimplementedGameRobotServiceServer) PickGameRobot(context.Context, *PickGameRobotRequest) (*GameRobotResponse, error) {
return nil, status.Error(codes.Unimplemented, "method PickGameRobot not implemented")
}
func (UnimplementedGameRobotServiceServer) SetGameRobotStatus(context.Context, *SetGameRobotStatusRequest) (*GameRobotResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetGameRobotStatus not implemented")
}
func (UnimplementedGameRobotServiceServer) DeleteGameRobot(context.Context, *DeleteGameRobotRequest) (*DeleteGameRobotResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteGameRobot not implemented")
}
func (UnimplementedGameRobotServiceServer) mustEmbedUnimplementedGameRobotServiceServer() {}
func (UnimplementedGameRobotServiceServer) testEmbeddedByValue() {}
// UnsafeGameRobotServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to GameRobotServiceServer will
// result in compilation errors.
type UnsafeGameRobotServiceServer interface {
mustEmbedUnimplementedGameRobotServiceServer()
}
func RegisterGameRobotServiceServer(s grpc.ServiceRegistrar, srv GameRobotServiceServer) {
// If the following call panics, it indicates UnimplementedGameRobotServiceServer 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.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&GameRobotService_ServiceDesc, srv)
}
func _GameRobotService_ListGameRobots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListGameRobotsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GameRobotServiceServer).ListGameRobots(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GameRobotService_ListGameRobots_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GameRobotServiceServer).ListGameRobots(ctx, req.(*ListGameRobotsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GameRobotService_RegisterGameRobots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RegisterGameRobotsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GameRobotServiceServer).RegisterGameRobots(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GameRobotService_RegisterGameRobots_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GameRobotServiceServer).RegisterGameRobots(ctx, req.(*RegisterGameRobotsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GameRobotService_PickGameRobot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PickGameRobotRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GameRobotServiceServer).PickGameRobot(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GameRobotService_PickGameRobot_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GameRobotServiceServer).PickGameRobot(ctx, req.(*PickGameRobotRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GameRobotService_SetGameRobotStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetGameRobotStatusRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GameRobotServiceServer).SetGameRobotStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GameRobotService_SetGameRobotStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GameRobotServiceServer).SetGameRobotStatus(ctx, req.(*SetGameRobotStatusRequest))
}
return interceptor(ctx, in, info, handler)
}
func _GameRobotService_DeleteGameRobot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteGameRobotRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GameRobotServiceServer).DeleteGameRobot(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: GameRobotService_DeleteGameRobot_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GameRobotServiceServer).DeleteGameRobot(ctx, req.(*DeleteGameRobotRequest))
}
return interceptor(ctx, in, info, handler)
}
// GameRobotService_ServiceDesc is the grpc.ServiceDesc for GameRobotService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var GameRobotService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "hyapp.robot.v1.GameRobotService",
HandlerType: (*GameRobotServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ListGameRobots",
Handler: _GameRobotService_ListGameRobots_Handler,
},
{
MethodName: "RegisterGameRobots",
Handler: _GameRobotService_RegisterGameRobots_Handler,
},
{
MethodName: "PickGameRobot",
Handler: _GameRobotService_PickGameRobot_Handler,
},
{
MethodName: "SetGameRobotStatus",
Handler: _GameRobotService_SetGameRobotStatus_Handler,
},
{
MethodName: "DeleteGameRobot",
Handler: _GameRobotService_DeleteGameRobot_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/robot/v1/robot.proto",
}
const (
RoomRobotService_ListRoomRobots_FullMethodName = "/hyapp.robot.v1.RoomRobotService/ListRoomRobots"
RoomRobotService_RegisterRoomRobots_FullMethodName = "/hyapp.robot.v1.RoomRobotService/RegisterRoomRobots"
RoomRobotService_PickRoomRobot_FullMethodName = "/hyapp.robot.v1.RoomRobotService/PickRoomRobot"
RoomRobotService_SetRoomRobotStatus_FullMethodName = "/hyapp.robot.v1.RoomRobotService/SetRoomRobotStatus"
RoomRobotService_DeleteRoomRobot_FullMethodName = "/hyapp.robot.v1.RoomRobotService/DeleteRoomRobot"
)
// RoomRobotServiceClient is the client API for RoomRobotService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type RoomRobotServiceClient interface {
ListRoomRobots(ctx context.Context, in *ListRoomRobotsRequest, opts ...grpc.CallOption) (*ListRoomRobotsResponse, error)
RegisterRoomRobots(ctx context.Context, in *RegisterRoomRobotsRequest, opts ...grpc.CallOption) (*RegisterRoomRobotsResponse, error)
PickRoomRobot(ctx context.Context, in *PickRoomRobotRequest, opts ...grpc.CallOption) (*RoomRobotResponse, error)
SetRoomRobotStatus(ctx context.Context, in *SetRoomRobotStatusRequest, opts ...grpc.CallOption) (*RoomRobotResponse, error)
DeleteRoomRobot(ctx context.Context, in *DeleteRoomRobotRequest, opts ...grpc.CallOption) (*DeleteRoomRobotResponse, error)
}
type roomRobotServiceClient struct {
cc grpc.ClientConnInterface
}
func NewRoomRobotServiceClient(cc grpc.ClientConnInterface) RoomRobotServiceClient {
return &roomRobotServiceClient{cc}
}
func (c *roomRobotServiceClient) ListRoomRobots(ctx context.Context, in *ListRoomRobotsRequest, opts ...grpc.CallOption) (*ListRoomRobotsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListRoomRobotsResponse)
err := c.cc.Invoke(ctx, RoomRobotService_ListRoomRobots_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomRobotServiceClient) RegisterRoomRobots(ctx context.Context, in *RegisterRoomRobotsRequest, opts ...grpc.CallOption) (*RegisterRoomRobotsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RegisterRoomRobotsResponse)
err := c.cc.Invoke(ctx, RoomRobotService_RegisterRoomRobots_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomRobotServiceClient) PickRoomRobot(ctx context.Context, in *PickRoomRobotRequest, opts ...grpc.CallOption) (*RoomRobotResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RoomRobotResponse)
err := c.cc.Invoke(ctx, RoomRobotService_PickRoomRobot_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomRobotServiceClient) SetRoomRobotStatus(ctx context.Context, in *SetRoomRobotStatusRequest, opts ...grpc.CallOption) (*RoomRobotResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RoomRobotResponse)
err := c.cc.Invoke(ctx, RoomRobotService_SetRoomRobotStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomRobotServiceClient) DeleteRoomRobot(ctx context.Context, in *DeleteRoomRobotRequest, opts ...grpc.CallOption) (*DeleteRoomRobotResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DeleteRoomRobotResponse)
err := c.cc.Invoke(ctx, RoomRobotService_DeleteRoomRobot_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// RoomRobotServiceServer is the server API for RoomRobotService service.
// All implementations must embed UnimplementedRoomRobotServiceServer
// for forward compatibility.
type RoomRobotServiceServer interface {
ListRoomRobots(context.Context, *ListRoomRobotsRequest) (*ListRoomRobotsResponse, error)
RegisterRoomRobots(context.Context, *RegisterRoomRobotsRequest) (*RegisterRoomRobotsResponse, error)
PickRoomRobot(context.Context, *PickRoomRobotRequest) (*RoomRobotResponse, error)
SetRoomRobotStatus(context.Context, *SetRoomRobotStatusRequest) (*RoomRobotResponse, error)
DeleteRoomRobot(context.Context, *DeleteRoomRobotRequest) (*DeleteRoomRobotResponse, error)
mustEmbedUnimplementedRoomRobotServiceServer()
}
// UnimplementedRoomRobotServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedRoomRobotServiceServer struct{}
func (UnimplementedRoomRobotServiceServer) ListRoomRobots(context.Context, *ListRoomRobotsRequest) (*ListRoomRobotsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomRobots not implemented")
}
func (UnimplementedRoomRobotServiceServer) RegisterRoomRobots(context.Context, *RegisterRoomRobotsRequest) (*RegisterRoomRobotsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RegisterRoomRobots not implemented")
}
func (UnimplementedRoomRobotServiceServer) PickRoomRobot(context.Context, *PickRoomRobotRequest) (*RoomRobotResponse, error) {
return nil, status.Error(codes.Unimplemented, "method PickRoomRobot not implemented")
}
func (UnimplementedRoomRobotServiceServer) SetRoomRobotStatus(context.Context, *SetRoomRobotStatusRequest) (*RoomRobotResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetRoomRobotStatus not implemented")
}
func (UnimplementedRoomRobotServiceServer) DeleteRoomRobot(context.Context, *DeleteRoomRobotRequest) (*DeleteRoomRobotResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteRoomRobot not implemented")
}
func (UnimplementedRoomRobotServiceServer) mustEmbedUnimplementedRoomRobotServiceServer() {}
func (UnimplementedRoomRobotServiceServer) testEmbeddedByValue() {}
// UnsafeRoomRobotServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to RoomRobotServiceServer will
// result in compilation errors.
type UnsafeRoomRobotServiceServer interface {
mustEmbedUnimplementedRoomRobotServiceServer()
}
func RegisterRoomRobotServiceServer(s grpc.ServiceRegistrar, srv RoomRobotServiceServer) {
// If the following call panics, it indicates UnimplementedRoomRobotServiceServer 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.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&RoomRobotService_ServiceDesc, srv)
}
func _RoomRobotService_ListRoomRobots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRoomRobotsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomRobotServiceServer).ListRoomRobots(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomRobotService_ListRoomRobots_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomRobotServiceServer).ListRoomRobots(ctx, req.(*ListRoomRobotsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomRobotService_RegisterRoomRobots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RegisterRoomRobotsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomRobotServiceServer).RegisterRoomRobots(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomRobotService_RegisterRoomRobots_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomRobotServiceServer).RegisterRoomRobots(ctx, req.(*RegisterRoomRobotsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomRobotService_PickRoomRobot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PickRoomRobotRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomRobotServiceServer).PickRoomRobot(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomRobotService_PickRoomRobot_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomRobotServiceServer).PickRoomRobot(ctx, req.(*PickRoomRobotRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomRobotService_SetRoomRobotStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetRoomRobotStatusRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomRobotServiceServer).SetRoomRobotStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomRobotService_SetRoomRobotStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomRobotServiceServer).SetRoomRobotStatus(ctx, req.(*SetRoomRobotStatusRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomRobotService_DeleteRoomRobot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteRoomRobotRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomRobotServiceServer).DeleteRoomRobot(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomRobotService_DeleteRoomRobot_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomRobotServiceServer).DeleteRoomRobot(ctx, req.(*DeleteRoomRobotRequest))
}
return interceptor(ctx, in, info, handler)
}
// RoomRobotService_ServiceDesc is the grpc.ServiceDesc for RoomRobotService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var RoomRobotService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "hyapp.robot.v1.RoomRobotService",
HandlerType: (*RoomRobotServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ListRoomRobots",
Handler: _RoomRobotService_ListRoomRobots_Handler,
},
{
MethodName: "RegisterRoomRobots",
Handler: _RoomRobotService_RegisterRoomRobots_Handler,
},
{
MethodName: "PickRoomRobot",
Handler: _RoomRobotService_PickRoomRobot_Handler,
},
{
MethodName: "SetRoomRobotStatus",
Handler: _RoomRobotService_SetRoomRobotStatus_Handler,
},
{
MethodName: "DeleteRoomRobot",
Handler: _RoomRobotService_DeleteRoomRobot_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/robot/v1/robot.proto",
}

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,6 @@ message RoomUser {
string role = 2; string role = 2;
int64 joined_at_ms = 3; int64 joined_at_ms = 3;
int64 last_seen_at_ms = 4; int64 last_seen_at_ms = 4;
int64 gift_value = 5;
} }
// RoomOnlineUser 线 // RoomOnlineUser 线
@ -66,10 +65,6 @@ message SeatState {
bool mic_muted = 9; bool mic_muted = 9;
// seat_status empty/locked/occupied/publishing/muted // seat_status empty/locked/occupied/publishing/muted
string seat_status = 10; string seat_status = 10;
// mic_heartbeat_at_ms mic_session UTC epoch ms
int64 mic_heartbeat_at_ms = 11;
// gift_value
int64 gift_value = 12;
} }
// RankItem // RankItem
@ -80,15 +75,6 @@ message RankItem {
int64 updated_at_ms = 4; int64 updated_at_ms = 4;
} }
// RoomContributionRankItem
message RoomContributionRankItem {
int64 rank = 1;
int64 user_id = 2;
int64 score = 3;
int64 gift_value = 4;
int64 updated_at_ms = 5;
}
// LuckyGiftDrawResult SendGift // LuckyGiftDrawResult SendGift
message LuckyGiftDrawResult { message LuckyGiftDrawResult {
bool enabled = 1; bool enabled = 1;
@ -101,45 +87,39 @@ message LuckyGiftDrawResult {
string selected_tier_id = 8; string selected_tier_id = 8;
int64 multiplier_ppm = 9; int64 multiplier_ppm = 9;
int64 base_reward_coins = 10; int64 base_reward_coins = 10;
int64 effective_reward_coins = 11; int64 room_atmosphere_reward_coins = 11;
string reward_status = 12; int64 activity_subsidy_coins = 12;
bool stage_feedback = 13; int64 effective_reward_coins = 13;
bool high_multiplier = 14; string reward_status = 14;
int64 created_at_ms = 15; bool stage_feedback = 15;
string wallet_transaction_id = 16; bool high_multiplier = 16;
int64 coin_balance_after = 17; int64 created_at_ms = 17;
// target_user_id
int64 target_user_id = 18;
// hits gift_count
repeated SendGiftLuckyHit hits = 19;
} }
// RoomRocketRewardItem // RoomTreasureRewardItem
message RoomRocketRewardItem { message RoomTreasureRewardItem {
string reward_item_id = 1; string reward_item_id = 1;
int64 resource_group_id = 2; int64 resource_group_id = 2;
int64 weight = 3; int64 weight = 3;
string display_name = 4; string display_name = 4;
string icon_url = 5; string icon_url = 5;
// quantity 0 1
int64 quantity = 6;
} }
// RoomRocketLevel // RoomTreasureLevel
message RoomRocketLevel { message RoomTreasureLevel {
int32 level = 1; int32 level = 1;
int64 fuel_threshold = 2; int64 energy_threshold = 2;
string cover_url = 3; string cover_url = 3;
string animation_url = 4; string animation_url = 4;
string launch_animation_url = 5; string opening_animation_url = 5;
string launched_image_url = 6; string opened_image_url = 6;
repeated RoomRocketRewardItem in_room_rewards = 7; repeated RoomTreasureRewardItem in_room_rewards = 7;
repeated RoomRocketRewardItem top1_rewards = 8; repeated RoomTreasureRewardItem top1_rewards = 8;
repeated RoomRocketRewardItem igniter_rewards = 9; repeated RoomTreasureRewardItem igniter_rewards = 9;
} }
// RoomRocketRewardGrant // RoomTreasureRewardGrant
message RoomRocketRewardGrant { message RoomTreasureRewardGrant {
string reward_role = 1; string reward_role = 1;
int64 user_id = 2; int64 user_id = 2;
string reward_item_id = 3; string reward_item_id = 3;
@ -150,103 +130,80 @@ message RoomRocketRewardGrant {
string status = 8; string status = 8;
} }
// RoomRocketContribution // RoomTreasureState Room Cell
message RoomRocketContribution { message RoomTreasureState {
int64 user_id = 1;
int64 fuel = 2;
int64 updated_at_ms = 3;
}
// 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; int32 current_level = 1;
int64 current_fuel = 2; int64 current_progress = 2;
int64 fuel_threshold = 3; int64 energy_threshold = 3;
string status = 4; string status = 4;
int64 ignited_at_ms = 5; int64 countdown_started_at_ms = 5;
int64 launch_at_ms = 6; int64 open_at_ms = 6;
int64 launched_at_ms = 7; int64 opened_at_ms = 7;
int64 reset_at_ms = 8; int64 reset_at_ms = 8;
int64 top1_user_id = 9; int64 top1_user_id = 9;
int64 igniter_user_id = 10; int64 igniter_user_id = 10;
string rocket_id = 11; string box_id = 11;
int64 config_version = 12; int64 config_version = 12;
repeated RoomRocketRewardGrant last_rewards = 13; repeated RoomTreasureRewardGrant last_rewards = 13;
repeated RoomRocketPendingLaunch pending_launches = 14;
repeated RoomRocketContribution current_contributions = 15;
} }
// RoomRocketInfo App UI // RoomTreasureInfo App UI
message RoomRocketInfo { message RoomTreasureInfo {
bool enabled = 1; bool enabled = 1;
repeated RoomRocketLevel levels = 2; repeated RoomTreasureLevel levels = 2;
RoomRocketState state = 3; RoomTreasureState state = 3;
int64 server_time_ms = 4; int64 server_time_ms = 4;
string broadcast_scope = 5; string broadcast_scope = 5;
int64 launch_delay_ms = 6; int64 open_delay_ms = 6;
int64 broadcast_delay_ms = 7; int64 broadcast_delay_ms = 7;
string reward_stack_policy = 8; string reward_stack_policy = 8;
} }
// RoomRocketGiftFuelRule // RoomTreasureGiftEnergyRule
message RoomRocketGiftFuelRule { message RoomTreasureGiftEnergyRule {
string rule_id = 1; string rule_id = 1;
string gift_id = 2; string gift_id = 2;
string gift_type_code = 3; string gift_type_code = 3;
int64 multiplier_ppm = 4; int64 multiplier_ppm = 4;
int64 fixed_fuel = 5; int64 fixed_energy = 5;
bool excluded = 6; bool excluded = 6;
} }
// AdminRoomRocketConfig // AdminRoomTreasureConfig
message AdminRoomRocketConfig { message AdminRoomTreasureConfig {
string app_code = 1; string app_code = 1;
bool enabled = 2; bool enabled = 2;
int64 config_version = 3; int64 config_version = 3;
string fuel_source = 4; string energy_source = 4;
int64 launch_delay_ms = 5; int64 open_delay_ms = 5;
bool broadcast_enabled = 6; bool broadcast_enabled = 6;
string broadcast_scope = 7; string broadcast_scope = 7;
int64 broadcast_delay_ms = 8; int64 broadcast_delay_ms = 8;
string reward_stack_policy = 9; string reward_stack_policy = 9;
repeated RoomRocketLevel levels = 10; repeated RoomTreasureLevel levels = 10;
repeated RoomRocketGiftFuelRule gift_fuel_rules = 11; repeated RoomTreasureGiftEnergyRule gift_energy_rules = 11;
int64 updated_by_admin_id = 12; int64 updated_by_admin_id = 12;
int64 created_at_ms = 13; int64 created_at_ms = 13;
int64 updated_at_ms = 14; int64 updated_at_ms = 14;
} }
message AdminGetRoomRocketConfigRequest { message AdminGetRoomTreasureConfigRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
} }
message AdminGetRoomRocketConfigResponse { message AdminGetRoomTreasureConfigResponse {
AdminRoomRocketConfig config = 1; AdminRoomTreasureConfig config = 1;
int64 server_time_ms = 2; int64 server_time_ms = 2;
} }
message AdminUpdateRoomRocketConfigRequest { message AdminUpdateRoomTreasureConfigRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
AdminRoomRocketConfig config = 2; AdminRoomTreasureConfig config = 2;
int64 admin_id = 3; int64 admin_id = 3;
} }
message AdminUpdateRoomRocketConfigResponse { message AdminUpdateRoomTreasureConfigResponse {
AdminRoomRocketConfig config = 1; AdminRoomTreasureConfig config = 1;
int64 server_time_ms = 2; int64 server_time_ms = 2;
} }
@ -277,72 +234,6 @@ message AdminUpdateRoomSeatConfigResponse {
int64 server_time_ms = 2; int64 server_time_ms = 2;
} }
message AdminHumanRoomRobotCountryPool {
string country_code = 1;
repeated int64 robot_user_ids = 2;
}
message AdminHumanRoomRobotCountryRule {
string country_code = 1;
int32 max_room_count = 2;
// allowed_owner_ids / owner_user_id
repeated string allowed_owner_ids = 3;
}
message AdminHumanRoomRobotConfig {
string app_code = 1;
bool enabled = 2;
int32 candidate_room_max_online = 3;
int32 room_full_stop_online = 4;
int64 robot_stay_min_ms = 5;
int64 robot_stay_max_ms = 6;
int64 robot_replace_min_ms = 7;
int64 robot_replace_max_ms = 8;
int64 normal_gift_interval_ms = 9;
repeated string gift_ids = 10;
repeated string lucky_gift_ids = 11;
repeated string super_lucky_gift_ids = 12;
int64 lucky_combo_min = 13;
int64 lucky_combo_max = 14;
int64 lucky_pause_min_ms = 15;
int64 lucky_pause_max_ms = 16;
int64 max_gift_senders = 17;
repeated AdminHumanRoomRobotCountryPool country_pools = 18;
uint64 updated_by_admin_id = 19;
int64 created_at_ms = 20;
int64 updated_at_ms = 21;
int64 normal_gift_interval_min_ms = 22;
int64 normal_gift_interval_max_ms = 23;
int32 room_target_min_online = 24;
int32 room_target_max_online = 25;
// allowed_owner_ids / owner_user_id
repeated string allowed_owner_ids = 26;
// country_limit_enabled country_rules
bool country_limit_enabled = 27;
// country_rules
repeated AdminHumanRoomRobotCountryRule country_rules = 28;
}
message AdminGetHumanRoomRobotConfigRequest {
RequestMeta meta = 1;
}
message AdminGetHumanRoomRobotConfigResponse {
AdminHumanRoomRobotConfig config = 1;
int64 server_time_ms = 2;
}
message AdminUpdateHumanRoomRobotConfigRequest {
RequestMeta meta = 1;
AdminHumanRoomRobotConfig config = 2;
uint64 admin_id = 3;
}
message AdminUpdateHumanRoomRobotConfigResponse {
AdminHumanRoomRobotConfig config = 1;
int64 server_time_ms = 2;
}
message AdminRoomPinRoom { message AdminRoomPinRoom {
string room_id = 1; string room_id = 1;
string room_short_id = 2; string room_short_id = 2;
@ -367,7 +258,6 @@ message AdminRoomPin {
int64 created_at_ms = 11; int64 created_at_ms = 11;
int64 updated_at_ms = 12; int64 updated_at_ms = 12;
AdminRoomPinRoom room = 13; AdminRoomPinRoom room = 13;
string pin_type = 14;
} }
message AdminListRoomPinsRequest { message AdminListRoomPinsRequest {
@ -377,7 +267,6 @@ message AdminListRoomPinsRequest {
string keyword = 4; string keyword = 4;
string status = 5; string status = 5;
int64 visible_region_id = 6; int64 visible_region_id = 6;
string pin_type = 7;
} }
message AdminListRoomPinsResponse { message AdminListRoomPinsResponse {
@ -392,9 +281,6 @@ message AdminCreateRoomPinRequest {
int64 weight = 3; int64 weight = 3;
int64 duration_days = 4; int64 duration_days = 4;
uint64 admin_id = 5; uint64 admin_id = 5;
int64 pinned_at_ms = 6;
int64 expires_at_ms = 7;
string pin_type = 8;
} }
message AdminCreateRoomPinResponse { message AdminCreateRoomPinResponse {
@ -413,107 +299,6 @@ message AdminCancelRoomPinResponse {
int64 server_time_ms = 2; int64 server_time_ms = 2;
} }
message AdminRobotRoomGiftRule {
repeated string gift_ids = 1;
repeated string lucky_gift_ids = 2;
int64 normal_gift_interval_ms = 3;
int64 lucky_combo_min = 4;
int64 lucky_combo_max = 5;
int64 lucky_pause_min_ms = 6;
int64 lucky_pause_max_ms = 7;
int64 robot_stay_min_ms = 8;
int64 robot_stay_max_ms = 9;
int64 robot_replace_min_ms = 10;
int64 robot_replace_max_ms = 11;
int64 max_gift_senders = 12;
}
message AdminRobotRoom {
string app_code = 1;
string room_id = 2;
string room_short_id = 3;
string title = 4;
string cover_url = 5;
int64 visible_region_id = 6;
string status = 7;
int64 owner_robot_user_id = 8;
repeated int64 robot_user_ids = 9;
AdminRobotRoomGiftRule gift_rule = 10;
uint64 created_by_admin_id = 11;
int64 created_at_ms = 12;
int64 updated_at_ms = 13;
int32 active_robot_count = 14;
int32 seat_count = 15;
}
message AdminListRobotRoomsRequest {
RequestMeta meta = 1;
int32 page = 2;
int32 page_size = 3;
string status = 4;
}
message AdminListRobotRoomsResponse {
repeated AdminRobotRoom rooms = 1;
int64 total = 2;
int64 server_time_ms = 3;
}
message AdminCreateRobotRoomRequest {
RequestMeta meta = 1;
int64 owner_robot_user_id = 2;
repeated int64 candidate_robot_user_ids = 3;
int32 min_robot_count = 4;
int32 max_robot_count = 5;
string room_name = 6;
string room_avatar = 7;
int64 visible_region_id = 8;
AdminRobotRoomGiftRule gift_rule = 9;
uint64 admin_id = 10;
string owner_country_code = 11;
int32 seat_count = 12;
}
message AdminCreateRobotRoomResponse {
AdminRobotRoom room = 1;
int64 server_time_ms = 2;
}
message AdminSetRobotRoomStatusRequest {
RequestMeta meta = 1;
string status = 2;
uint64 admin_id = 3;
}
message AdminSetRobotRoomStatusResponse {
AdminRobotRoom room = 1;
int64 server_time_ms = 2;
}
message AdminRenameOwnerCountryCodeRequest {
RequestMeta meta = 1;
string old_country_code = 2;
string new_country_code = 3;
uint64 admin_id = 4;
}
message AdminRenameOwnerCountryCodeResponse {
int64 room_list_entries = 1;
int64 room_snapshots = 2;
int64 server_time_ms = 3;
}
message AdminFilterAvailableRoomRobotsRequest {
RequestMeta meta = 1;
repeated int64 user_ids = 2;
}
message AdminFilterAvailableRoomRobotsResponse {
repeated int64 available_user_ids = 1;
repeated int64 occupied_user_ids = 2;
int64 server_time_ms = 3;
}
// RoomBanState ban // RoomBanState ban
message RoomBanState { message RoomBanState {
int64 user_id = 1; int64 user_id = 1;
@ -544,12 +329,10 @@ message RoomSnapshot {
int64 visible_region_id = 18; int64 visible_region_id = 18;
// locked HTTP JSON // locked HTTP JSON
bool locked = 19; bool locked = 19;
// rocket GetRoomRocket // treasure GetRoomTreasure
RoomRocketState rocket = 20; RoomTreasureState treasure = 20;
// ban_states ban ban_user_ids // ban_states ban ban_user_ids
repeated RoomBanState ban_states = 21; repeated RoomBanState ban_states = 21;
// online_count lite online_users 线
int32 online_count = 22;
} }
// RoomBackgroundImage // RoomBackgroundImage
@ -598,7 +381,7 @@ message SetRoomBackgroundResponse {
} }
// CreateRoomRequest // CreateRoomRequest
// meta.room_idmeta.command_idmeta.actor_user_idmoderoom_name room_avatar // meta.room_idmeta.command_idmeta.actor_user_idmode room_name
// meta.actor_user_id owner Conflict // meta.actor_user_id owner Conflict
message CreateRoomRequest { message CreateRoomRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
@ -610,18 +393,12 @@ message CreateRoomRequest {
int64 visible_region_id = 4; int64 visible_region_id = 4;
// room_name room-service RoomSnapshot.room_ext["title"] // room_name room-service RoomSnapshot.room_ext["title"]
string room_name = 5; string room_name = 5;
// room_avatar room-service RoomSnapshot.room_ext["cover_url"] // room_avatar room-service
string room_avatar = 6; string room_avatar = 6;
// room_description Room Cell // room_description Room Cell
string room_description = 7; string room_description = 7;
// room_short_id ID default/current display_user_id // room_short_id ID default/current display_user_id
string room_short_id = 8; string room_short_id = 8;
// owner_country_code gateway user-service
string owner_country_code = 9;
// robot_room gateway
bool robot_room = 10;
// robot_user_ids
repeated int64 robot_user_ids = 11;
} }
// CreateRoomResponse // CreateRoomResponse
@ -647,53 +424,11 @@ message UpdateRoomProfileResponse {
} }
// JoinRoomRequest presence // JoinRoomRequest presence
message RoomEntryVehicleSnapshot {
int64 resource_id = 1;
string resource_code = 2;
string name = 3;
string asset_url = 4;
string preview_url = 5;
string animation_url = 6;
string metadata_json = 7;
string entitlement_id = 8;
int64 expires_at_ms = 9;
}
// RoomUserDisplayProfile gateway IM/UI
message RoomUserDisplayProfile {
int64 user_id = 1;
string nickname = 2;
string avatar = 3;
string display_user_id = 4;
string pretty_display_user_id = 5;
}
message JoinRoomRequest { message JoinRoomRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
string role = 2; string role = 2;
// password room-service command log // password room-service command log
string password = 3; string password = 3;
RoomEntryVehicleSnapshot entry_vehicle = 4;
int64 actor_country_id = 5;
int64 actor_region_id = 6;
// actor_is_robot room-service gateway false
bool actor_is_robot = 7;
// actor_display_profile room-service IM/outbox RoomState
RoomUserDisplayProfile actor_display_profile = 8;
// response_mode=lite Room Cell outbox IM
string response_mode = 9;
// presence_recovery presence
// Room Cell 线 RoomUserJoined IM/outbox
bool presence_recovery = 10;
}
// RoomEffectiveVipSnapshot room-service wallet-service VIP
// effective_benefits
message RoomEffectiveVipSnapshot {
string program_type = 1;
int32 level = 2;
string name = 3;
bool room_entry_notice_enabled = 4;
} }
// JoinRoomResponse // JoinRoomResponse
@ -701,7 +436,6 @@ message JoinRoomResponse {
CommandResult result = 1; CommandResult result = 1;
RoomUser user = 2; RoomUser user = 2;
RoomSnapshot room = 3; RoomSnapshot room = 3;
RoomEffectiveVipSnapshot effective_vip = 4;
} }
// RoomHeartbeatRequest presence // RoomHeartbeatRequest presence
@ -772,7 +506,6 @@ message AdminListRoomsRequest {
int64 visible_region_id = 6; int64 visible_region_id = 6;
string sort_by = 7; string sort_by = 7;
string sort_direction = 8; string sort_direction = 8;
int64 owner_user_id = 9;
} }
message AdminListRoomsResponse { message AdminListRoomsResponse {
@ -802,8 +535,6 @@ message AdminUpdateRoomRequest {
string close_reason = 8; string close_reason = 8;
uint64 admin_id = 9; uint64 admin_id = 9;
string admin_name = 10; string admin_name = 10;
// owner_country_code / Room Cell owner
optional string owner_country_code = 11;
} }
message AdminUpdateRoomResponse { message AdminUpdateRoomResponse {
@ -826,8 +557,6 @@ message AdminDeleteRoomResponse {
message MicUpRequest { message MicUpRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
int32 seat_no = 2; int32 seat_no = 2;
// target_display_profile room-service IM/outbox
RoomUserDisplayProfile target_display_profile = 3;
} }
// MicUpResponse // MicUpResponse
@ -845,8 +574,6 @@ message MicDownRequest {
int64 target_user_id = 2; int64 target_user_id = 2;
// reason publish_timeout // reason publish_timeout
string reason = 3; string reason = 3;
// target_display_profile IM
RoomUserDisplayProfile target_display_profile = 4;
} }
// MicDownResponse // MicDownResponse
@ -861,8 +588,6 @@ message ChangeMicSeatRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
int64 target_user_id = 2; int64 target_user_id = 2;
int32 seat_no = 3; int32 seat_no = 3;
// target_display_profile 线
RoomUserDisplayProfile target_display_profile = 4;
} }
// ChangeMicSeatResponse // ChangeMicSeatResponse
@ -881,8 +606,6 @@ message ConfirmMicPublishingRequest {
int64 room_version = 4; int64 room_version = 4;
int64 event_time_ms = 5; int64 event_time_ms = 5;
string source = 6; string source = 6;
// target_display_profile
RoomUserDisplayProfile target_display_profile = 7;
} }
// ConfirmMicPublishingResponse // ConfirmMicPublishingResponse
@ -892,31 +615,12 @@ message ConfirmMicPublishingResponse {
RoomSnapshot room = 3; 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 // SetMicMuteRequest
message SetMicMuteRequest { message SetMicMuteRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
// target_user_id actor_user_id // target_user_id actor_user_id
int64 target_user_id = 2; int64 target_user_id = 2;
bool muted = 3; bool muted = 3;
// target_display_profile IM/UI
RoomUserDisplayProfile target_display_profile = 4;
} }
// SetMicMuteResponse // SetMicMuteResponse
@ -1061,70 +765,6 @@ message SystemEvictUserResponse {
string rtc_kick_error = 6; string rtc_kick_error = 6;
} }
// SendGiftTargetHostScope gateway
message SendGiftTargetHostScope {
int64 target_user_id = 1;
// target_is_host gateway user-service active host profile
bool target_is_host = 2;
// target_host_region_id Host & Agency
int64 target_host_region_id = 3;
// target_agency_owner_user_id
int64 target_agency_owner_user_id = 4;
}
// SendGiftDisplayProfile gateway
// room-service IM Room Cell
message SendGiftDisplayProfile {
int64 user_id = 1;
string username = 2;
string avatar = 3;
string display_user_id = 4;
string pretty_display_user_id = 5;
}
// SendGiftBatchTarget
// RoomGiftSent
message SendGiftBatchTarget {
int64 target_user_id = 1;
string receiver_nickname = 2;
string receiver_avatar = 3;
string receiver_display_user_id = 4;
string receiver_pretty_display_user_id = 5;
int64 gift_value = 6;
int64 target_gift_value = 7;
int64 coin_spent = 8;
string billing_receipt_id = 9;
string command_id = 10;
LuckyGiftDrawResult lucky_gift = 11;
}
// SendGiftBatchDisplay
// 广使 RoomGiftBatchSent IM coin_balance_after
message SendGiftBatchDisplay {
string event_id = 1;
int64 sender_user_id = 2;
string sender_name = 3;
string sender_avatar = 4;
string sender_display_user_id = 5;
string sender_pretty_display_user_id = 6;
string gift_id = 7;
int32 gift_count = 8;
int64 total_gift_value = 9;
int64 total_coin_spent = 10;
string billing_receipt_id = 11;
int64 room_heat = 12;
string pool_id = 13;
string gift_type_code = 14;
string cp_relation_type = 15;
string gift_name = 16;
string gift_icon_url = 17;
string gift_animation_url = 18;
repeated string gift_effect_types = 19;
repeated int64 target_user_ids = 20;
repeated SendGiftBatchTarget targets = 21;
string command_id = 22;
}
// SendGiftRequest // SendGiftRequest
message SendGiftRequest { message SendGiftRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
@ -1142,25 +782,6 @@ message SendGiftRequest {
int64 target_host_region_id = 9; int64 target_host_region_id = 9;
// target_agency_owner_user_id // target_agency_owner_user_id
int64 target_agency_owner_user_id = 10; 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;
// entitlement_id Bag
string entitlement_id = 13;
// source bag
string source = 14;
// target_host_scopes target
repeated SendGiftTargetHostScope target_host_scopes = 15;
// sender_display_profile gateway user-service IM
SendGiftDisplayProfile sender_display_profile = 16;
// target_display_profiles IM target_user_id
repeated SendGiftDisplayProfile target_display_profiles = 17;
// display_mode=batch batch-send HTTP IM send
string display_mode = 18;
// combo_session_id + batch_seq Flutter command_id
string combo_session_id = 19;
int64 batch_seq = 20;
} }
// SendGiftResponse // SendGiftResponse
@ -1170,19 +791,8 @@ message SendGiftResponse {
int64 room_heat = 3; int64 room_heat = 3;
repeated RankItem gift_rank = 4; repeated RankItem gift_rank = 4;
RoomSnapshot room = 5; RoomSnapshot room = 5;
RoomRocketState rocket = 6; RoomTreasureState treasure = 6;
// lucky_gift
LuckyGiftDrawResult lucky_gift = 7; LuckyGiftDrawResult lucky_gift = 7;
// lucky_gifts target_user_id
repeated LuckyGiftDrawResult lucky_gifts = 8;
// coin_balance_after COIN
int64 coin_balance_after = 9;
// gift_income_balance_after COIN 0
int64 gift_income_balance_after = 10;
// batch_display batch-send send
SendGiftBatchDisplay batch_display = 11;
// v2 Flutter V1
SendGiftResultV2 v2 = 12;
} }
// CheckSpeakPermissionRequest IM gateway // CheckSpeakPermissionRequest IM gateway
@ -1215,20 +825,8 @@ message VerifyRoomPresenceResponse {
int64 room_version = 3; int64 room_version = 3;
} }
// ResolveRoomAppCodeRequest room_id // ListRoomsRequest
// app_code URL Fami Lalu // visible_region_id gateway user-service GetUser
message ResolveRoomAppCodeRequest {
string request_id = 1;
string room_id = 2;
}
message ResolveRoomAppCodeResponse {
string app_code = 1;
}
// ListRoomsRequest
// visible_region_id gateway viewer filter_region_id
// gateway filter_region_id
message ListRoomsRequest { message ListRoomsRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
int64 viewer_user_id = 2; int64 viewer_user_id = 2;
@ -1237,26 +835,6 @@ message ListRoomsRequest {
string cursor = 5; string cursor = 5;
int32 limit = 6; int32 limit = 6;
string query = 7; string query = 7;
// country_code gateway active
string country_code = 8;
// viewer_country_code All/
string viewer_country_code = 9;
// all_visible_regions gateway App true visible_region_id
bool all_visible_regions = 10;
// filter_region_id gateway active all_visible_regions/country_code
int64 filter_region_id = 11;
}
// ListRoomsByOwnersRequest active
// owner_user_ids gateway
message ListRoomsByOwnersRequest {
RequestMeta meta = 1;
int64 viewer_user_id = 2;
repeated int64 owner_user_ids = 3;
string tab = 4;
string cursor = 5;
int32 limit = 6;
string query = 7;
} }
// ListRoomFeedsRequest Mine visited/friend/following/followed // ListRoomFeedsRequest Mine visited/friend/following/followed
@ -1303,7 +881,6 @@ message RoomListItem {
string app_code = 13; string app_code = 13;
string room_short_id = 14; string room_short_id = 14;
bool locked = 15; bool locked = 15;
string country_code = 16;
} }
// ListRoomsResponse cursor // ListRoomsResponse cursor
@ -1330,26 +907,6 @@ message ListRoomGiftLeaderboardResponse {
int64 server_time_ms = 6; int64 server_time_ms = 6;
} }
// ListRoomContributionRankRequest
message ListRoomContributionRankRequest {
RequestMeta meta = 1;
string room_id = 2;
string period = 3;
int32 limit = 4;
int64 viewer_user_id = 5;
}
// ListRoomContributionRankResponse
message ListRoomContributionRankResponse {
string room_id = 1;
string period = 2;
int64 start_at_ms = 3;
int64 end_at_ms = 4;
repeated RoomContributionRankItem items = 5;
int64 total = 6;
int64 server_time_ms = 7;
}
// GetMyRoomRequest // GetMyRoomRequest
// gateway owner_user_id ID // gateway owner_user_id ID
message GetMyRoomRequest { message GetMyRoomRequest {
@ -1402,16 +959,16 @@ message GetRoomSnapshotResponse {
bool is_followed = 3; bool is_followed = 3;
} }
// GetRoomRocketRequest // GetRoomTreasureRequest
// gateway viewer_user_id viewer_user_id // gateway viewer_user_id viewer_user_id
message GetRoomRocketRequest { message GetRoomTreasureRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
string room_id = 2; string room_id = 2;
int64 viewer_user_id = 3; int64 viewer_user_id = 3;
} }
message GetRoomRocketResponse { message GetRoomTreasureResponse {
RoomRocketInfo rocket = 1; RoomTreasureInfo treasure = 1;
int64 server_time_ms = 2; int64 server_time_ms = 2;
} }
@ -1502,19 +1059,14 @@ service RoomCommandService {
rpc CloseRoom(CloseRoomRequest) returns (CloseRoomResponse); rpc CloseRoom(CloseRoomRequest) returns (CloseRoomResponse);
rpc AdminUpdateRoom(AdminUpdateRoomRequest) returns (AdminUpdateRoomResponse); rpc AdminUpdateRoom(AdminUpdateRoomRequest) returns (AdminUpdateRoomResponse);
rpc AdminDeleteRoom(AdminDeleteRoomRequest) returns (AdminDeleteRoomResponse); rpc AdminDeleteRoom(AdminDeleteRoomRequest) returns (AdminDeleteRoomResponse);
rpc AdminUpdateRoomRocketConfig(AdminUpdateRoomRocketConfigRequest) returns (AdminUpdateRoomRocketConfigResponse); rpc AdminUpdateRoomTreasureConfig(AdminUpdateRoomTreasureConfigRequest) returns (AdminUpdateRoomTreasureConfigResponse);
rpc AdminUpdateRoomSeatConfig(AdminUpdateRoomSeatConfigRequest) returns (AdminUpdateRoomSeatConfigResponse); rpc AdminUpdateRoomSeatConfig(AdminUpdateRoomSeatConfigRequest) returns (AdminUpdateRoomSeatConfigResponse);
rpc AdminUpdateHumanRoomRobotConfig(AdminUpdateHumanRoomRobotConfigRequest) returns (AdminUpdateHumanRoomRobotConfigResponse);
rpc AdminCreateRoomPin(AdminCreateRoomPinRequest) returns (AdminCreateRoomPinResponse); rpc AdminCreateRoomPin(AdminCreateRoomPinRequest) returns (AdminCreateRoomPinResponse);
rpc AdminCancelRoomPin(AdminCancelRoomPinRequest) returns (AdminCancelRoomPinResponse); rpc AdminCancelRoomPin(AdminCancelRoomPinRequest) returns (AdminCancelRoomPinResponse);
rpc AdminCreateRobotRoom(AdminCreateRobotRoomRequest) returns (AdminCreateRobotRoomResponse);
rpc AdminSetRobotRoomStatus(AdminSetRobotRoomStatusRequest) returns (AdminSetRobotRoomStatusResponse);
rpc AdminRenameOwnerCountryCode(AdminRenameOwnerCountryCodeRequest) returns (AdminRenameOwnerCountryCodeResponse);
rpc MicUp(MicUpRequest) returns (MicUpResponse); rpc MicUp(MicUpRequest) returns (MicUpResponse);
rpc MicDown(MicDownRequest) returns (MicDownResponse); rpc MicDown(MicDownRequest) returns (MicDownResponse);
rpc ChangeMicSeat(ChangeMicSeatRequest) returns (ChangeMicSeatResponse); rpc ChangeMicSeat(ChangeMicSeatRequest) returns (ChangeMicSeatResponse);
rpc ConfirmMicPublishing(ConfirmMicPublishingRequest) returns (ConfirmMicPublishingResponse); rpc ConfirmMicPublishing(ConfirmMicPublishingRequest) returns (ConfirmMicPublishingResponse);
rpc MicHeartbeat(MicHeartbeatRequest) returns (MicHeartbeatResponse);
rpc SetMicMute(SetMicMuteRequest) returns (SetMicMuteResponse); rpc SetMicMute(SetMicMuteRequest) returns (SetMicMuteResponse);
rpc ApplyRTCEvent(ApplyRTCEventRequest) returns (ApplyRTCEventResponse); rpc ApplyRTCEvent(ApplyRTCEventRequest) returns (ApplyRTCEventResponse);
rpc SetMicSeatLock(SetMicSeatLockRequest) returns (SetMicSeatLockResponse); rpc SetMicSeatLock(SetMicSeatLockRequest) returns (SetMicSeatLockResponse);
@ -1534,67 +1086,23 @@ service RoomCommandService {
service RoomGuardService { service RoomGuardService {
rpc CheckSpeakPermission(CheckSpeakPermissionRequest) returns (CheckSpeakPermissionResponse); rpc CheckSpeakPermission(CheckSpeakPermissionRequest) returns (CheckSpeakPermissionResponse);
rpc VerifyRoomPresence(VerifyRoomPresenceRequest) returns (VerifyRoomPresenceResponse); rpc VerifyRoomPresence(VerifyRoomPresenceRequest) returns (VerifyRoomPresenceResponse);
rpc ResolveRoomAppCode(ResolveRoomAppCodeRequest) returns (ResolveRoomAppCodeResponse);
} }
// RoomQueryService Room Cell // RoomQueryService Room Cell
service RoomQueryService { service RoomQueryService {
rpc AdminListRooms(AdminListRoomsRequest) returns (AdminListRoomsResponse); rpc AdminListRooms(AdminListRoomsRequest) returns (AdminListRoomsResponse);
rpc AdminGetRoom(AdminGetRoomRequest) returns (AdminGetRoomResponse); rpc AdminGetRoom(AdminGetRoomRequest) returns (AdminGetRoomResponse);
rpc AdminGetRoomRocketConfig(AdminGetRoomRocketConfigRequest) returns (AdminGetRoomRocketConfigResponse); rpc AdminGetRoomTreasureConfig(AdminGetRoomTreasureConfigRequest) returns (AdminGetRoomTreasureConfigResponse);
rpc AdminGetRoomSeatConfig(AdminGetRoomSeatConfigRequest) returns (AdminGetRoomSeatConfigResponse); rpc AdminGetRoomSeatConfig(AdminGetRoomSeatConfigRequest) returns (AdminGetRoomSeatConfigResponse);
rpc AdminGetHumanRoomRobotConfig(AdminGetHumanRoomRobotConfigRequest) returns (AdminGetHumanRoomRobotConfigResponse);
rpc AdminListRoomPins(AdminListRoomPinsRequest) returns (AdminListRoomPinsResponse); rpc AdminListRoomPins(AdminListRoomPinsRequest) returns (AdminListRoomPinsResponse);
rpc AdminListRobotRooms(AdminListRobotRoomsRequest) returns (AdminListRobotRoomsResponse);
rpc AdminFilterAvailableRoomRobots(AdminFilterAvailableRoomRobotsRequest) returns (AdminFilterAvailableRoomRobotsResponse);
rpc ListRooms(ListRoomsRequest) returns (ListRoomsResponse); rpc ListRooms(ListRoomsRequest) returns (ListRoomsResponse);
rpc ListRoomsByOwners(ListRoomsByOwnersRequest) returns (ListRoomsResponse);
rpc ListRoomFeeds(ListRoomFeedsRequest) returns (ListRoomsResponse); rpc ListRoomFeeds(ListRoomFeedsRequest) returns (ListRoomsResponse);
rpc ListRoomGiftLeaderboard(ListRoomGiftLeaderboardRequest) returns (ListRoomGiftLeaderboardResponse); rpc ListRoomGiftLeaderboard(ListRoomGiftLeaderboardRequest) returns (ListRoomGiftLeaderboardResponse);
rpc ListRoomContributionRank(ListRoomContributionRankRequest) returns (ListRoomContributionRankResponse);
rpc GetMyRoom(GetMyRoomRequest) returns (GetMyRoomResponse); rpc GetMyRoom(GetMyRoomRequest) returns (GetMyRoomResponse);
rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse); rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse);
rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse); rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse);
rpc ListRoomBackgrounds(ListRoomBackgroundsRequest) returns (ListRoomBackgroundsResponse); rpc ListRoomBackgrounds(ListRoomBackgroundsRequest) returns (ListRoomBackgroundsResponse);
rpc GetRoomRocket(GetRoomRocketRequest) returns (GetRoomRocketResponse); rpc GetRoomTreasure(GetRoomTreasureRequest) returns (GetRoomTreasureResponse);
rpc ListRoomOnlineUsers(ListRoomOnlineUsersRequest) returns (ListRoomOnlineUsersResponse); rpc ListRoomOnlineUsers(ListRoomOnlineUsersRequest) returns (ListRoomOnlineUsersResponse);
rpc ListRoomBannedUsers(ListRoomBannedUsersRequest) returns (ListRoomBannedUsersResponse); rpc ListRoomBannedUsers(ListRoomBannedUsersRequest) returns (ListRoomBannedUsersResponse);
} }
// SendGiftLuckyHit V2
// protobuf message index
message SendGiftLuckyHit {
int32 gift_index = 1;
string draw_id = 2;
int64 target_user_id = 3;
string selected_tier_id = 4;
int64 multiplier_ppm = 5;
int64 reward_coins = 6;
}
// SendGiftResultV2
// SendGiftResponse V1 V2
//
message SendGiftResultV2 {
int32 api_version = 1;
string command_id = 2;
string combo_session_id = 3;
int64 batch_seq = 4;
bool idempotent_replay = 5;
int64 committed_room_version = 6;
string billing_receipt_id = 7;
int64 coin_balance_after = 8;
int64 gift_income_balance_after = 9;
int32 settled_gift_count = 10;
repeated int64 settled_target_user_ids = 11;
// balance_version sender wallet
int64 balance_version = 12;
LuckyGiftDrawResult lucky_gift = 13;
repeated LuckyGiftDrawResult lucky_gifts = 14;
repeated SendGiftLuckyHit lucky_hits = 15;
SendGiftBatchDisplay batch_display = 16;
int64 room_heat = 17;
repeated RankItem gift_rank = 18;
// operation_status committed V2
string operation_status = 19;
}

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.6.2 // - protoc-gen-go-grpc v1.6.2
// - protoc v5.29.2 // - protoc v7.35.0
// source: proto/room/v1/room.proto // source: proto/room/v1/room.proto
package roomv1 package roomv1
@ -29,19 +29,14 @@ const (
RoomCommandService_CloseRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/CloseRoom" RoomCommandService_CloseRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/CloseRoom"
RoomCommandService_AdminUpdateRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoom" RoomCommandService_AdminUpdateRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoom"
RoomCommandService_AdminDeleteRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminDeleteRoom" RoomCommandService_AdminDeleteRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminDeleteRoom"
RoomCommandService_AdminUpdateRoomRocketConfig_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoomRocketConfig" RoomCommandService_AdminUpdateRoomTreasureConfig_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoomTreasureConfig"
RoomCommandService_AdminUpdateRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoomSeatConfig" RoomCommandService_AdminUpdateRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoomSeatConfig"
RoomCommandService_AdminUpdateHumanRoomRobotConfig_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateHumanRoomRobotConfig"
RoomCommandService_AdminCreateRoomPin_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCreateRoomPin" RoomCommandService_AdminCreateRoomPin_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCreateRoomPin"
RoomCommandService_AdminCancelRoomPin_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCancelRoomPin" RoomCommandService_AdminCancelRoomPin_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCancelRoomPin"
RoomCommandService_AdminCreateRobotRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCreateRobotRoom"
RoomCommandService_AdminSetRobotRoomStatus_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminSetRobotRoomStatus"
RoomCommandService_AdminRenameOwnerCountryCode_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminRenameOwnerCountryCode"
RoomCommandService_MicUp_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicUp" RoomCommandService_MicUp_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicUp"
RoomCommandService_MicDown_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicDown" RoomCommandService_MicDown_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicDown"
RoomCommandService_ChangeMicSeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/ChangeMicSeat" RoomCommandService_ChangeMicSeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/ChangeMicSeat"
RoomCommandService_ConfirmMicPublishing_FullMethodName = "/hyapp.room.v1.RoomCommandService/ConfirmMicPublishing" 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_SetMicMute_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicMute"
RoomCommandService_ApplyRTCEvent_FullMethodName = "/hyapp.room.v1.RoomCommandService/ApplyRTCEvent" RoomCommandService_ApplyRTCEvent_FullMethodName = "/hyapp.room.v1.RoomCommandService/ApplyRTCEvent"
RoomCommandService_SetMicSeatLock_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicSeatLock" RoomCommandService_SetMicSeatLock_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetMicSeatLock"
@ -73,19 +68,14 @@ type RoomCommandServiceClient interface {
CloseRoom(ctx context.Context, in *CloseRoomRequest, opts ...grpc.CallOption) (*CloseRoomResponse, error) CloseRoom(ctx context.Context, in *CloseRoomRequest, opts ...grpc.CallOption) (*CloseRoomResponse, error)
AdminUpdateRoom(ctx context.Context, in *AdminUpdateRoomRequest, opts ...grpc.CallOption) (*AdminUpdateRoomResponse, error) AdminUpdateRoom(ctx context.Context, in *AdminUpdateRoomRequest, opts ...grpc.CallOption) (*AdminUpdateRoomResponse, error)
AdminDeleteRoom(ctx context.Context, in *AdminDeleteRoomRequest, opts ...grpc.CallOption) (*AdminDeleteRoomResponse, error) AdminDeleteRoom(ctx context.Context, in *AdminDeleteRoomRequest, opts ...grpc.CallOption) (*AdminDeleteRoomResponse, error)
AdminUpdateRoomRocketConfig(ctx context.Context, in *AdminUpdateRoomRocketConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomRocketConfigResponse, error) AdminUpdateRoomTreasureConfig(ctx context.Context, in *AdminUpdateRoomTreasureConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomTreasureConfigResponse, error)
AdminUpdateRoomSeatConfig(ctx context.Context, in *AdminUpdateRoomSeatConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomSeatConfigResponse, error) AdminUpdateRoomSeatConfig(ctx context.Context, in *AdminUpdateRoomSeatConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomSeatConfigResponse, error)
AdminUpdateHumanRoomRobotConfig(ctx context.Context, in *AdminUpdateHumanRoomRobotConfigRequest, opts ...grpc.CallOption) (*AdminUpdateHumanRoomRobotConfigResponse, error)
AdminCreateRoomPin(ctx context.Context, in *AdminCreateRoomPinRequest, opts ...grpc.CallOption) (*AdminCreateRoomPinResponse, error) AdminCreateRoomPin(ctx context.Context, in *AdminCreateRoomPinRequest, opts ...grpc.CallOption) (*AdminCreateRoomPinResponse, error)
AdminCancelRoomPin(ctx context.Context, in *AdminCancelRoomPinRequest, opts ...grpc.CallOption) (*AdminCancelRoomPinResponse, error) AdminCancelRoomPin(ctx context.Context, in *AdminCancelRoomPinRequest, opts ...grpc.CallOption) (*AdminCancelRoomPinResponse, error)
AdminCreateRobotRoom(ctx context.Context, in *AdminCreateRobotRoomRequest, opts ...grpc.CallOption) (*AdminCreateRobotRoomResponse, error)
AdminSetRobotRoomStatus(ctx context.Context, in *AdminSetRobotRoomStatusRequest, opts ...grpc.CallOption) (*AdminSetRobotRoomStatusResponse, error)
AdminRenameOwnerCountryCode(ctx context.Context, in *AdminRenameOwnerCountryCodeRequest, opts ...grpc.CallOption) (*AdminRenameOwnerCountryCodeResponse, error)
MicUp(ctx context.Context, in *MicUpRequest, opts ...grpc.CallOption) (*MicUpResponse, error) MicUp(ctx context.Context, in *MicUpRequest, opts ...grpc.CallOption) (*MicUpResponse, error)
MicDown(ctx context.Context, in *MicDownRequest, opts ...grpc.CallOption) (*MicDownResponse, error) MicDown(ctx context.Context, in *MicDownRequest, opts ...grpc.CallOption) (*MicDownResponse, error)
ChangeMicSeat(ctx context.Context, in *ChangeMicSeatRequest, opts ...grpc.CallOption) (*ChangeMicSeatResponse, error) ChangeMicSeat(ctx context.Context, in *ChangeMicSeatRequest, opts ...grpc.CallOption) (*ChangeMicSeatResponse, error)
ConfirmMicPublishing(ctx context.Context, in *ConfirmMicPublishingRequest, opts ...grpc.CallOption) (*ConfirmMicPublishingResponse, 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) SetMicMute(ctx context.Context, in *SetMicMuteRequest, opts ...grpc.CallOption) (*SetMicMuteResponse, error)
ApplyRTCEvent(ctx context.Context, in *ApplyRTCEventRequest, opts ...grpc.CallOption) (*ApplyRTCEventResponse, error) ApplyRTCEvent(ctx context.Context, in *ApplyRTCEventRequest, opts ...grpc.CallOption) (*ApplyRTCEventResponse, error)
SetMicSeatLock(ctx context.Context, in *SetMicSeatLockRequest, opts ...grpc.CallOption) (*SetMicSeatLockResponse, error) SetMicSeatLock(ctx context.Context, in *SetMicSeatLockRequest, opts ...grpc.CallOption) (*SetMicSeatLockResponse, error)
@ -209,10 +199,10 @@ func (c *roomCommandServiceClient) AdminDeleteRoom(ctx context.Context, in *Admi
return out, nil return out, nil
} }
func (c *roomCommandServiceClient) AdminUpdateRoomRocketConfig(ctx context.Context, in *AdminUpdateRoomRocketConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomRocketConfigResponse, error) { func (c *roomCommandServiceClient) AdminUpdateRoomTreasureConfig(ctx context.Context, in *AdminUpdateRoomTreasureConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomTreasureConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminUpdateRoomRocketConfigResponse) out := new(AdminUpdateRoomTreasureConfigResponse)
err := c.cc.Invoke(ctx, RoomCommandService_AdminUpdateRoomRocketConfig_FullMethodName, in, out, cOpts...) err := c.cc.Invoke(ctx, RoomCommandService_AdminUpdateRoomTreasureConfig_FullMethodName, in, out, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -229,16 +219,6 @@ func (c *roomCommandServiceClient) AdminUpdateRoomSeatConfig(ctx context.Context
return out, nil return out, nil
} }
func (c *roomCommandServiceClient) AdminUpdateHumanRoomRobotConfig(ctx context.Context, in *AdminUpdateHumanRoomRobotConfigRequest, opts ...grpc.CallOption) (*AdminUpdateHumanRoomRobotConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminUpdateHumanRoomRobotConfigResponse)
err := c.cc.Invoke(ctx, RoomCommandService_AdminUpdateHumanRoomRobotConfig_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomCommandServiceClient) AdminCreateRoomPin(ctx context.Context, in *AdminCreateRoomPinRequest, opts ...grpc.CallOption) (*AdminCreateRoomPinResponse, error) { func (c *roomCommandServiceClient) AdminCreateRoomPin(ctx context.Context, in *AdminCreateRoomPinRequest, opts ...grpc.CallOption) (*AdminCreateRoomPinResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminCreateRoomPinResponse) out := new(AdminCreateRoomPinResponse)
@ -259,36 +239,6 @@ func (c *roomCommandServiceClient) AdminCancelRoomPin(ctx context.Context, in *A
return out, nil return out, nil
} }
func (c *roomCommandServiceClient) AdminCreateRobotRoom(ctx context.Context, in *AdminCreateRobotRoomRequest, opts ...grpc.CallOption) (*AdminCreateRobotRoomResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminCreateRobotRoomResponse)
err := c.cc.Invoke(ctx, RoomCommandService_AdminCreateRobotRoom_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomCommandServiceClient) AdminSetRobotRoomStatus(ctx context.Context, in *AdminSetRobotRoomStatusRequest, opts ...grpc.CallOption) (*AdminSetRobotRoomStatusResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminSetRobotRoomStatusResponse)
err := c.cc.Invoke(ctx, RoomCommandService_AdminSetRobotRoomStatus_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomCommandServiceClient) AdminRenameOwnerCountryCode(ctx context.Context, in *AdminRenameOwnerCountryCodeRequest, opts ...grpc.CallOption) (*AdminRenameOwnerCountryCodeResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminRenameOwnerCountryCodeResponse)
err := c.cc.Invoke(ctx, RoomCommandService_AdminRenameOwnerCountryCode_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomCommandServiceClient) MicUp(ctx context.Context, in *MicUpRequest, opts ...grpc.CallOption) (*MicUpResponse, error) { func (c *roomCommandServiceClient) MicUp(ctx context.Context, in *MicUpRequest, opts ...grpc.CallOption) (*MicUpResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MicUpResponse) out := new(MicUpResponse)
@ -329,16 +279,6 @@ func (c *roomCommandServiceClient) ConfirmMicPublishing(ctx context.Context, in
return out, nil 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) { func (c *roomCommandServiceClient) SetMicMute(ctx context.Context, in *SetMicMuteRequest, opts ...grpc.CallOption) (*SetMicMuteResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SetMicMuteResponse) out := new(SetMicMuteResponse)
@ -485,19 +425,14 @@ type RoomCommandServiceServer interface {
CloseRoom(context.Context, *CloseRoomRequest) (*CloseRoomResponse, error) CloseRoom(context.Context, *CloseRoomRequest) (*CloseRoomResponse, error)
AdminUpdateRoom(context.Context, *AdminUpdateRoomRequest) (*AdminUpdateRoomResponse, error) AdminUpdateRoom(context.Context, *AdminUpdateRoomRequest) (*AdminUpdateRoomResponse, error)
AdminDeleteRoom(context.Context, *AdminDeleteRoomRequest) (*AdminDeleteRoomResponse, error) AdminDeleteRoom(context.Context, *AdminDeleteRoomRequest) (*AdminDeleteRoomResponse, error)
AdminUpdateRoomRocketConfig(context.Context, *AdminUpdateRoomRocketConfigRequest) (*AdminUpdateRoomRocketConfigResponse, error) AdminUpdateRoomTreasureConfig(context.Context, *AdminUpdateRoomTreasureConfigRequest) (*AdminUpdateRoomTreasureConfigResponse, error)
AdminUpdateRoomSeatConfig(context.Context, *AdminUpdateRoomSeatConfigRequest) (*AdminUpdateRoomSeatConfigResponse, error) AdminUpdateRoomSeatConfig(context.Context, *AdminUpdateRoomSeatConfigRequest) (*AdminUpdateRoomSeatConfigResponse, error)
AdminUpdateHumanRoomRobotConfig(context.Context, *AdminUpdateHumanRoomRobotConfigRequest) (*AdminUpdateHumanRoomRobotConfigResponse, error)
AdminCreateRoomPin(context.Context, *AdminCreateRoomPinRequest) (*AdminCreateRoomPinResponse, error) AdminCreateRoomPin(context.Context, *AdminCreateRoomPinRequest) (*AdminCreateRoomPinResponse, error)
AdminCancelRoomPin(context.Context, *AdminCancelRoomPinRequest) (*AdminCancelRoomPinResponse, error) AdminCancelRoomPin(context.Context, *AdminCancelRoomPinRequest) (*AdminCancelRoomPinResponse, error)
AdminCreateRobotRoom(context.Context, *AdminCreateRobotRoomRequest) (*AdminCreateRobotRoomResponse, error)
AdminSetRobotRoomStatus(context.Context, *AdminSetRobotRoomStatusRequest) (*AdminSetRobotRoomStatusResponse, error)
AdminRenameOwnerCountryCode(context.Context, *AdminRenameOwnerCountryCodeRequest) (*AdminRenameOwnerCountryCodeResponse, error)
MicUp(context.Context, *MicUpRequest) (*MicUpResponse, error) MicUp(context.Context, *MicUpRequest) (*MicUpResponse, error)
MicDown(context.Context, *MicDownRequest) (*MicDownResponse, error) MicDown(context.Context, *MicDownRequest) (*MicDownResponse, error)
ChangeMicSeat(context.Context, *ChangeMicSeatRequest) (*ChangeMicSeatResponse, error) ChangeMicSeat(context.Context, *ChangeMicSeatRequest) (*ChangeMicSeatResponse, error)
ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error) ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error)
MicHeartbeat(context.Context, *MicHeartbeatRequest) (*MicHeartbeatResponse, error)
SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error) SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error)
ApplyRTCEvent(context.Context, *ApplyRTCEventRequest) (*ApplyRTCEventResponse, error) ApplyRTCEvent(context.Context, *ApplyRTCEventRequest) (*ApplyRTCEventResponse, error)
SetMicSeatLock(context.Context, *SetMicSeatLockRequest) (*SetMicSeatLockResponse, error) SetMicSeatLock(context.Context, *SetMicSeatLockRequest) (*SetMicSeatLockResponse, error)
@ -551,30 +486,18 @@ func (UnimplementedRoomCommandServiceServer) AdminUpdateRoom(context.Context, *A
func (UnimplementedRoomCommandServiceServer) AdminDeleteRoom(context.Context, *AdminDeleteRoomRequest) (*AdminDeleteRoomResponse, error) { func (UnimplementedRoomCommandServiceServer) AdminDeleteRoom(context.Context, *AdminDeleteRoomRequest) (*AdminDeleteRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminDeleteRoom not implemented") return nil, status.Error(codes.Unimplemented, "method AdminDeleteRoom not implemented")
} }
func (UnimplementedRoomCommandServiceServer) AdminUpdateRoomRocketConfig(context.Context, *AdminUpdateRoomRocketConfigRequest) (*AdminUpdateRoomRocketConfigResponse, error) { func (UnimplementedRoomCommandServiceServer) AdminUpdateRoomTreasureConfig(context.Context, *AdminUpdateRoomTreasureConfigRequest) (*AdminUpdateRoomTreasureConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoomRocketConfig not implemented") return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoomTreasureConfig not implemented")
} }
func (UnimplementedRoomCommandServiceServer) AdminUpdateRoomSeatConfig(context.Context, *AdminUpdateRoomSeatConfigRequest) (*AdminUpdateRoomSeatConfigResponse, error) { func (UnimplementedRoomCommandServiceServer) AdminUpdateRoomSeatConfig(context.Context, *AdminUpdateRoomSeatConfigRequest) (*AdminUpdateRoomSeatConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoomSeatConfig not implemented") return nil, status.Error(codes.Unimplemented, "method AdminUpdateRoomSeatConfig not implemented")
} }
func (UnimplementedRoomCommandServiceServer) AdminUpdateHumanRoomRobotConfig(context.Context, *AdminUpdateHumanRoomRobotConfigRequest) (*AdminUpdateHumanRoomRobotConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminUpdateHumanRoomRobotConfig not implemented")
}
func (UnimplementedRoomCommandServiceServer) AdminCreateRoomPin(context.Context, *AdminCreateRoomPinRequest) (*AdminCreateRoomPinResponse, error) { func (UnimplementedRoomCommandServiceServer) AdminCreateRoomPin(context.Context, *AdminCreateRoomPinRequest) (*AdminCreateRoomPinResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminCreateRoomPin not implemented") return nil, status.Error(codes.Unimplemented, "method AdminCreateRoomPin not implemented")
} }
func (UnimplementedRoomCommandServiceServer) AdminCancelRoomPin(context.Context, *AdminCancelRoomPinRequest) (*AdminCancelRoomPinResponse, error) { func (UnimplementedRoomCommandServiceServer) AdminCancelRoomPin(context.Context, *AdminCancelRoomPinRequest) (*AdminCancelRoomPinResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminCancelRoomPin not implemented") return nil, status.Error(codes.Unimplemented, "method AdminCancelRoomPin not implemented")
} }
func (UnimplementedRoomCommandServiceServer) AdminCreateRobotRoom(context.Context, *AdminCreateRobotRoomRequest) (*AdminCreateRobotRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminCreateRobotRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) AdminSetRobotRoomStatus(context.Context, *AdminSetRobotRoomStatusRequest) (*AdminSetRobotRoomStatusResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminSetRobotRoomStatus not implemented")
}
func (UnimplementedRoomCommandServiceServer) AdminRenameOwnerCountryCode(context.Context, *AdminRenameOwnerCountryCodeRequest) (*AdminRenameOwnerCountryCodeResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminRenameOwnerCountryCode not implemented")
}
func (UnimplementedRoomCommandServiceServer) MicUp(context.Context, *MicUpRequest) (*MicUpResponse, error) { func (UnimplementedRoomCommandServiceServer) MicUp(context.Context, *MicUpRequest) (*MicUpResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MicUp not implemented") return nil, status.Error(codes.Unimplemented, "method MicUp not implemented")
} }
@ -587,9 +510,6 @@ func (UnimplementedRoomCommandServiceServer) ChangeMicSeat(context.Context, *Cha
func (UnimplementedRoomCommandServiceServer) ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error) { func (UnimplementedRoomCommandServiceServer) ConfirmMicPublishing(context.Context, *ConfirmMicPublishingRequest) (*ConfirmMicPublishingResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ConfirmMicPublishing not implemented") return nil, status.Error(codes.Unimplemented, "method ConfirmMicPublishing not implemented")
} }
func (UnimplementedRoomCommandServiceServer) MicHeartbeat(context.Context, *MicHeartbeatRequest) (*MicHeartbeatResponse, error) {
return nil, status.Error(codes.Unimplemented, "method MicHeartbeat not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error) { func (UnimplementedRoomCommandServiceServer) SetMicMute(context.Context, *SetMicMuteRequest) (*SetMicMuteResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetMicMute not implemented") return nil, status.Error(codes.Unimplemented, "method SetMicMute not implemented")
} }
@ -830,20 +750,20 @@ func _RoomCommandService_AdminDeleteRoom_Handler(srv interface{}, ctx context.Co
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _RoomCommandService_AdminUpdateRoomRocketConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _RoomCommandService_AdminUpdateRoomTreasureConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminUpdateRoomRocketConfigRequest) in := new(AdminUpdateRoomTreasureConfigRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(RoomCommandServiceServer).AdminUpdateRoomRocketConfig(ctx, in) return srv.(RoomCommandServiceServer).AdminUpdateRoomTreasureConfig(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: RoomCommandService_AdminUpdateRoomRocketConfig_FullMethodName, FullMethod: RoomCommandService_AdminUpdateRoomTreasureConfig_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomCommandServiceServer).AdminUpdateRoomRocketConfig(ctx, req.(*AdminUpdateRoomRocketConfigRequest)) return srv.(RoomCommandServiceServer).AdminUpdateRoomTreasureConfig(ctx, req.(*AdminUpdateRoomTreasureConfigRequest))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
@ -866,24 +786,6 @@ func _RoomCommandService_AdminUpdateRoomSeatConfig_Handler(srv interface{}, ctx
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _RoomCommandService_AdminUpdateHumanRoomRobotConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminUpdateHumanRoomRobotConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomCommandServiceServer).AdminUpdateHumanRoomRobotConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomCommandService_AdminUpdateHumanRoomRobotConfig_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomCommandServiceServer).AdminUpdateHumanRoomRobotConfig(ctx, req.(*AdminUpdateHumanRoomRobotConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomCommandService_AdminCreateRoomPin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _RoomCommandService_AdminCreateRoomPin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminCreateRoomPinRequest) in := new(AdminCreateRoomPinRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -920,60 +822,6 @@ func _RoomCommandService_AdminCancelRoomPin_Handler(srv interface{}, ctx context
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _RoomCommandService_AdminCreateRobotRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminCreateRobotRoomRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomCommandServiceServer).AdminCreateRobotRoom(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomCommandService_AdminCreateRobotRoom_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomCommandServiceServer).AdminCreateRobotRoom(ctx, req.(*AdminCreateRobotRoomRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomCommandService_AdminSetRobotRoomStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminSetRobotRoomStatusRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomCommandServiceServer).AdminSetRobotRoomStatus(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomCommandService_AdminSetRobotRoomStatus_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomCommandServiceServer).AdminSetRobotRoomStatus(ctx, req.(*AdminSetRobotRoomStatusRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomCommandService_AdminRenameOwnerCountryCode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminRenameOwnerCountryCodeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomCommandServiceServer).AdminRenameOwnerCountryCode(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomCommandService_AdminRenameOwnerCountryCode_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomCommandServiceServer).AdminRenameOwnerCountryCode(ctx, req.(*AdminRenameOwnerCountryCodeRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomCommandService_MicUp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _RoomCommandService_MicUp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MicUpRequest) in := new(MicUpRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -1046,24 +894,6 @@ func _RoomCommandService_ConfirmMicPublishing_Handler(srv interface{}, ctx conte
return interceptor(ctx, in, info, handler) 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) { func _RoomCommandService_SetMicMute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetMicMuteRequest) in := new(SetMicMuteRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -1346,17 +1176,13 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
Handler: _RoomCommandService_AdminDeleteRoom_Handler, Handler: _RoomCommandService_AdminDeleteRoom_Handler,
}, },
{ {
MethodName: "AdminUpdateRoomRocketConfig", MethodName: "AdminUpdateRoomTreasureConfig",
Handler: _RoomCommandService_AdminUpdateRoomRocketConfig_Handler, Handler: _RoomCommandService_AdminUpdateRoomTreasureConfig_Handler,
}, },
{ {
MethodName: "AdminUpdateRoomSeatConfig", MethodName: "AdminUpdateRoomSeatConfig",
Handler: _RoomCommandService_AdminUpdateRoomSeatConfig_Handler, Handler: _RoomCommandService_AdminUpdateRoomSeatConfig_Handler,
}, },
{
MethodName: "AdminUpdateHumanRoomRobotConfig",
Handler: _RoomCommandService_AdminUpdateHumanRoomRobotConfig_Handler,
},
{ {
MethodName: "AdminCreateRoomPin", MethodName: "AdminCreateRoomPin",
Handler: _RoomCommandService_AdminCreateRoomPin_Handler, Handler: _RoomCommandService_AdminCreateRoomPin_Handler,
@ -1365,18 +1191,6 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
MethodName: "AdminCancelRoomPin", MethodName: "AdminCancelRoomPin",
Handler: _RoomCommandService_AdminCancelRoomPin_Handler, Handler: _RoomCommandService_AdminCancelRoomPin_Handler,
}, },
{
MethodName: "AdminCreateRobotRoom",
Handler: _RoomCommandService_AdminCreateRobotRoom_Handler,
},
{
MethodName: "AdminSetRobotRoomStatus",
Handler: _RoomCommandService_AdminSetRobotRoomStatus_Handler,
},
{
MethodName: "AdminRenameOwnerCountryCode",
Handler: _RoomCommandService_AdminRenameOwnerCountryCode_Handler,
},
{ {
MethodName: "MicUp", MethodName: "MicUp",
Handler: _RoomCommandService_MicUp_Handler, Handler: _RoomCommandService_MicUp_Handler,
@ -1393,10 +1207,6 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ConfirmMicPublishing", MethodName: "ConfirmMicPublishing",
Handler: _RoomCommandService_ConfirmMicPublishing_Handler, Handler: _RoomCommandService_ConfirmMicPublishing_Handler,
}, },
{
MethodName: "MicHeartbeat",
Handler: _RoomCommandService_MicHeartbeat_Handler,
},
{ {
MethodName: "SetMicMute", MethodName: "SetMicMute",
Handler: _RoomCommandService_SetMicMute_Handler, Handler: _RoomCommandService_SetMicMute_Handler,
@ -1457,7 +1267,6 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
const ( const (
RoomGuardService_CheckSpeakPermission_FullMethodName = "/hyapp.room.v1.RoomGuardService/CheckSpeakPermission" RoomGuardService_CheckSpeakPermission_FullMethodName = "/hyapp.room.v1.RoomGuardService/CheckSpeakPermission"
RoomGuardService_VerifyRoomPresence_FullMethodName = "/hyapp.room.v1.RoomGuardService/VerifyRoomPresence" RoomGuardService_VerifyRoomPresence_FullMethodName = "/hyapp.room.v1.RoomGuardService/VerifyRoomPresence"
RoomGuardService_ResolveRoomAppCode_FullMethodName = "/hyapp.room.v1.RoomGuardService/ResolveRoomAppCode"
) )
// RoomGuardServiceClient is the client API for RoomGuardService service. // RoomGuardServiceClient is the client API for RoomGuardService service.
@ -1468,7 +1277,6 @@ const (
type RoomGuardServiceClient interface { type RoomGuardServiceClient interface {
CheckSpeakPermission(ctx context.Context, in *CheckSpeakPermissionRequest, opts ...grpc.CallOption) (*CheckSpeakPermissionResponse, error) CheckSpeakPermission(ctx context.Context, in *CheckSpeakPermissionRequest, opts ...grpc.CallOption) (*CheckSpeakPermissionResponse, error)
VerifyRoomPresence(ctx context.Context, in *VerifyRoomPresenceRequest, opts ...grpc.CallOption) (*VerifyRoomPresenceResponse, error) VerifyRoomPresence(ctx context.Context, in *VerifyRoomPresenceRequest, opts ...grpc.CallOption) (*VerifyRoomPresenceResponse, error)
ResolveRoomAppCode(ctx context.Context, in *ResolveRoomAppCodeRequest, opts ...grpc.CallOption) (*ResolveRoomAppCodeResponse, error)
} }
type roomGuardServiceClient struct { type roomGuardServiceClient struct {
@ -1499,16 +1307,6 @@ func (c *roomGuardServiceClient) VerifyRoomPresence(ctx context.Context, in *Ver
return out, nil return out, nil
} }
func (c *roomGuardServiceClient) ResolveRoomAppCode(ctx context.Context, in *ResolveRoomAppCodeRequest, opts ...grpc.CallOption) (*ResolveRoomAppCodeResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResolveRoomAppCodeResponse)
err := c.cc.Invoke(ctx, RoomGuardService_ResolveRoomAppCode_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// RoomGuardServiceServer is the server API for RoomGuardService service. // RoomGuardServiceServer is the server API for RoomGuardService service.
// All implementations must embed UnimplementedRoomGuardServiceServer // All implementations must embed UnimplementedRoomGuardServiceServer
// for forward compatibility. // for forward compatibility.
@ -1517,7 +1315,6 @@ func (c *roomGuardServiceClient) ResolveRoomAppCode(ctx context.Context, in *Res
type RoomGuardServiceServer interface { type RoomGuardServiceServer interface {
CheckSpeakPermission(context.Context, *CheckSpeakPermissionRequest) (*CheckSpeakPermissionResponse, error) CheckSpeakPermission(context.Context, *CheckSpeakPermissionRequest) (*CheckSpeakPermissionResponse, error)
VerifyRoomPresence(context.Context, *VerifyRoomPresenceRequest) (*VerifyRoomPresenceResponse, error) VerifyRoomPresence(context.Context, *VerifyRoomPresenceRequest) (*VerifyRoomPresenceResponse, error)
ResolveRoomAppCode(context.Context, *ResolveRoomAppCodeRequest) (*ResolveRoomAppCodeResponse, error)
mustEmbedUnimplementedRoomGuardServiceServer() mustEmbedUnimplementedRoomGuardServiceServer()
} }
@ -1534,9 +1331,6 @@ func (UnimplementedRoomGuardServiceServer) CheckSpeakPermission(context.Context,
func (UnimplementedRoomGuardServiceServer) VerifyRoomPresence(context.Context, *VerifyRoomPresenceRequest) (*VerifyRoomPresenceResponse, error) { func (UnimplementedRoomGuardServiceServer) VerifyRoomPresence(context.Context, *VerifyRoomPresenceRequest) (*VerifyRoomPresenceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method VerifyRoomPresence not implemented") return nil, status.Error(codes.Unimplemented, "method VerifyRoomPresence not implemented")
} }
func (UnimplementedRoomGuardServiceServer) ResolveRoomAppCode(context.Context, *ResolveRoomAppCodeRequest) (*ResolveRoomAppCodeResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ResolveRoomAppCode not implemented")
}
func (UnimplementedRoomGuardServiceServer) mustEmbedUnimplementedRoomGuardServiceServer() {} func (UnimplementedRoomGuardServiceServer) mustEmbedUnimplementedRoomGuardServiceServer() {}
func (UnimplementedRoomGuardServiceServer) testEmbeddedByValue() {} func (UnimplementedRoomGuardServiceServer) testEmbeddedByValue() {}
@ -1594,24 +1388,6 @@ func _RoomGuardService_VerifyRoomPresence_Handler(srv interface{}, ctx context.C
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _RoomGuardService_ResolveRoomAppCode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ResolveRoomAppCodeRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomGuardServiceServer).ResolveRoomAppCode(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomGuardService_ResolveRoomAppCode_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomGuardServiceServer).ResolveRoomAppCode(ctx, req.(*ResolveRoomAppCodeRequest))
}
return interceptor(ctx, in, info, handler)
}
// RoomGuardService_ServiceDesc is the grpc.ServiceDesc for RoomGuardService service. // RoomGuardService_ServiceDesc is the grpc.ServiceDesc for RoomGuardService service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@ -1627,10 +1403,6 @@ var RoomGuardService_ServiceDesc = grpc.ServiceDesc{
MethodName: "VerifyRoomPresence", MethodName: "VerifyRoomPresence",
Handler: _RoomGuardService_VerifyRoomPresence_Handler, Handler: _RoomGuardService_VerifyRoomPresence_Handler,
}, },
{
MethodName: "ResolveRoomAppCode",
Handler: _RoomGuardService_ResolveRoomAppCode_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "proto/room/v1/room.proto", Metadata: "proto/room/v1/room.proto",
@ -1639,22 +1411,17 @@ var RoomGuardService_ServiceDesc = grpc.ServiceDesc{
const ( const (
RoomQueryService_AdminListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRooms" RoomQueryService_AdminListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRooms"
RoomQueryService_AdminGetRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoom" RoomQueryService_AdminGetRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoom"
RoomQueryService_AdminGetRoomRocketConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomRocketConfig" RoomQueryService_AdminGetRoomTreasureConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomTreasureConfig"
RoomQueryService_AdminGetRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomSeatConfig" RoomQueryService_AdminGetRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomSeatConfig"
RoomQueryService_AdminGetHumanRoomRobotConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetHumanRoomRobotConfig"
RoomQueryService_AdminListRoomPins_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRoomPins" RoomQueryService_AdminListRoomPins_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRoomPins"
RoomQueryService_AdminListRobotRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRobotRooms"
RoomQueryService_AdminFilterAvailableRoomRobots_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminFilterAvailableRoomRobots"
RoomQueryService_ListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRooms" RoomQueryService_ListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRooms"
RoomQueryService_ListRoomsByOwners_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomsByOwners"
RoomQueryService_ListRoomFeeds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomFeeds" RoomQueryService_ListRoomFeeds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomFeeds"
RoomQueryService_ListRoomGiftLeaderboard_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomGiftLeaderboard" RoomQueryService_ListRoomGiftLeaderboard_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomGiftLeaderboard"
RoomQueryService_ListRoomContributionRank_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomContributionRank"
RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom" RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom"
RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom" RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom"
RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot" RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot"
RoomQueryService_ListRoomBackgrounds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBackgrounds" RoomQueryService_ListRoomBackgrounds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBackgrounds"
RoomQueryService_GetRoomRocket_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomRocket" RoomQueryService_GetRoomTreasure_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomTreasure"
RoomQueryService_ListRoomOnlineUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomOnlineUsers" RoomQueryService_ListRoomOnlineUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomOnlineUsers"
RoomQueryService_ListRoomBannedUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBannedUsers" RoomQueryService_ListRoomBannedUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBannedUsers"
) )
@ -1667,22 +1434,17 @@ const (
type RoomQueryServiceClient interface { type RoomQueryServiceClient interface {
AdminListRooms(ctx context.Context, in *AdminListRoomsRequest, opts ...grpc.CallOption) (*AdminListRoomsResponse, error) AdminListRooms(ctx context.Context, in *AdminListRoomsRequest, opts ...grpc.CallOption) (*AdminListRoomsResponse, error)
AdminGetRoom(ctx context.Context, in *AdminGetRoomRequest, opts ...grpc.CallOption) (*AdminGetRoomResponse, error) AdminGetRoom(ctx context.Context, in *AdminGetRoomRequest, opts ...grpc.CallOption) (*AdminGetRoomResponse, error)
AdminGetRoomRocketConfig(ctx context.Context, in *AdminGetRoomRocketConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomRocketConfigResponse, error) AdminGetRoomTreasureConfig(ctx context.Context, in *AdminGetRoomTreasureConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomTreasureConfigResponse, error)
AdminGetRoomSeatConfig(ctx context.Context, in *AdminGetRoomSeatConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomSeatConfigResponse, error) AdminGetRoomSeatConfig(ctx context.Context, in *AdminGetRoomSeatConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomSeatConfigResponse, error)
AdminGetHumanRoomRobotConfig(ctx context.Context, in *AdminGetHumanRoomRobotConfigRequest, opts ...grpc.CallOption) (*AdminGetHumanRoomRobotConfigResponse, error)
AdminListRoomPins(ctx context.Context, in *AdminListRoomPinsRequest, opts ...grpc.CallOption) (*AdminListRoomPinsResponse, error) AdminListRoomPins(ctx context.Context, in *AdminListRoomPinsRequest, opts ...grpc.CallOption) (*AdminListRoomPinsResponse, error)
AdminListRobotRooms(ctx context.Context, in *AdminListRobotRoomsRequest, opts ...grpc.CallOption) (*AdminListRobotRoomsResponse, error)
AdminFilterAvailableRoomRobots(ctx context.Context, in *AdminFilterAvailableRoomRobotsRequest, opts ...grpc.CallOption) (*AdminFilterAvailableRoomRobotsResponse, error)
ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error)
ListRoomsByOwners(ctx context.Context, in *ListRoomsByOwnersRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error)
ListRoomFeeds(ctx context.Context, in *ListRoomFeedsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) ListRoomFeeds(ctx context.Context, in *ListRoomFeedsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error)
ListRoomGiftLeaderboard(ctx context.Context, in *ListRoomGiftLeaderboardRequest, opts ...grpc.CallOption) (*ListRoomGiftLeaderboardResponse, error) ListRoomGiftLeaderboard(ctx context.Context, in *ListRoomGiftLeaderboardRequest, opts ...grpc.CallOption) (*ListRoomGiftLeaderboardResponse, error)
ListRoomContributionRank(ctx context.Context, in *ListRoomContributionRankRequest, opts ...grpc.CallOption) (*ListRoomContributionRankResponse, error)
GetMyRoom(ctx context.Context, in *GetMyRoomRequest, opts ...grpc.CallOption) (*GetMyRoomResponse, error) GetMyRoom(ctx context.Context, in *GetMyRoomRequest, opts ...grpc.CallOption) (*GetMyRoomResponse, error)
GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error) GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error)
GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, error) GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, error)
ListRoomBackgrounds(ctx context.Context, in *ListRoomBackgroundsRequest, opts ...grpc.CallOption) (*ListRoomBackgroundsResponse, error) ListRoomBackgrounds(ctx context.Context, in *ListRoomBackgroundsRequest, opts ...grpc.CallOption) (*ListRoomBackgroundsResponse, error)
GetRoomRocket(ctx context.Context, in *GetRoomRocketRequest, opts ...grpc.CallOption) (*GetRoomRocketResponse, error) GetRoomTreasure(ctx context.Context, in *GetRoomTreasureRequest, opts ...grpc.CallOption) (*GetRoomTreasureResponse, error)
ListRoomOnlineUsers(ctx context.Context, in *ListRoomOnlineUsersRequest, opts ...grpc.CallOption) (*ListRoomOnlineUsersResponse, error) ListRoomOnlineUsers(ctx context.Context, in *ListRoomOnlineUsersRequest, opts ...grpc.CallOption) (*ListRoomOnlineUsersResponse, error)
ListRoomBannedUsers(ctx context.Context, in *ListRoomBannedUsersRequest, opts ...grpc.CallOption) (*ListRoomBannedUsersResponse, error) ListRoomBannedUsers(ctx context.Context, in *ListRoomBannedUsersRequest, opts ...grpc.CallOption) (*ListRoomBannedUsersResponse, error)
} }
@ -1715,10 +1477,10 @@ func (c *roomQueryServiceClient) AdminGetRoom(ctx context.Context, in *AdminGetR
return out, nil return out, nil
} }
func (c *roomQueryServiceClient) AdminGetRoomRocketConfig(ctx context.Context, in *AdminGetRoomRocketConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomRocketConfigResponse, error) { func (c *roomQueryServiceClient) AdminGetRoomTreasureConfig(ctx context.Context, in *AdminGetRoomTreasureConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomTreasureConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminGetRoomRocketConfigResponse) out := new(AdminGetRoomTreasureConfigResponse)
err := c.cc.Invoke(ctx, RoomQueryService_AdminGetRoomRocketConfig_FullMethodName, in, out, cOpts...) err := c.cc.Invoke(ctx, RoomQueryService_AdminGetRoomTreasureConfig_FullMethodName, in, out, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1735,16 +1497,6 @@ func (c *roomQueryServiceClient) AdminGetRoomSeatConfig(ctx context.Context, in
return out, nil return out, nil
} }
func (c *roomQueryServiceClient) AdminGetHumanRoomRobotConfig(ctx context.Context, in *AdminGetHumanRoomRobotConfigRequest, opts ...grpc.CallOption) (*AdminGetHumanRoomRobotConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminGetHumanRoomRobotConfigResponse)
err := c.cc.Invoke(ctx, RoomQueryService_AdminGetHumanRoomRobotConfig_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomQueryServiceClient) AdminListRoomPins(ctx context.Context, in *AdminListRoomPinsRequest, opts ...grpc.CallOption) (*AdminListRoomPinsResponse, error) { func (c *roomQueryServiceClient) AdminListRoomPins(ctx context.Context, in *AdminListRoomPinsRequest, opts ...grpc.CallOption) (*AdminListRoomPinsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminListRoomPinsResponse) out := new(AdminListRoomPinsResponse)
@ -1755,26 +1507,6 @@ func (c *roomQueryServiceClient) AdminListRoomPins(ctx context.Context, in *Admi
return out, nil return out, nil
} }
func (c *roomQueryServiceClient) AdminListRobotRooms(ctx context.Context, in *AdminListRobotRoomsRequest, opts ...grpc.CallOption) (*AdminListRobotRoomsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminListRobotRoomsResponse)
err := c.cc.Invoke(ctx, RoomQueryService_AdminListRobotRooms_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomQueryServiceClient) AdminFilterAvailableRoomRobots(ctx context.Context, in *AdminFilterAvailableRoomRobotsRequest, opts ...grpc.CallOption) (*AdminFilterAvailableRoomRobotsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AdminFilterAvailableRoomRobotsResponse)
err := c.cc.Invoke(ctx, RoomQueryService_AdminFilterAvailableRoomRobots_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomQueryServiceClient) ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) { func (c *roomQueryServiceClient) ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListRoomsResponse) out := new(ListRoomsResponse)
@ -1785,16 +1517,6 @@ func (c *roomQueryServiceClient) ListRooms(ctx context.Context, in *ListRoomsReq
return out, nil return out, nil
} }
func (c *roomQueryServiceClient) ListRoomsByOwners(ctx context.Context, in *ListRoomsByOwnersRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListRoomsResponse)
err := c.cc.Invoke(ctx, RoomQueryService_ListRoomsByOwners_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomQueryServiceClient) ListRoomFeeds(ctx context.Context, in *ListRoomFeedsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) { func (c *roomQueryServiceClient) ListRoomFeeds(ctx context.Context, in *ListRoomFeedsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListRoomsResponse) out := new(ListRoomsResponse)
@ -1815,16 +1537,6 @@ func (c *roomQueryServiceClient) ListRoomGiftLeaderboard(ctx context.Context, in
return out, nil return out, nil
} }
func (c *roomQueryServiceClient) ListRoomContributionRank(ctx context.Context, in *ListRoomContributionRankRequest, opts ...grpc.CallOption) (*ListRoomContributionRankResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListRoomContributionRankResponse)
err := c.cc.Invoke(ctx, RoomQueryService_ListRoomContributionRank_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomQueryServiceClient) GetMyRoom(ctx context.Context, in *GetMyRoomRequest, opts ...grpc.CallOption) (*GetMyRoomResponse, error) { func (c *roomQueryServiceClient) GetMyRoom(ctx context.Context, in *GetMyRoomRequest, opts ...grpc.CallOption) (*GetMyRoomResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetMyRoomResponse) out := new(GetMyRoomResponse)
@ -1865,10 +1577,10 @@ func (c *roomQueryServiceClient) ListRoomBackgrounds(ctx context.Context, in *Li
return out, nil return out, nil
} }
func (c *roomQueryServiceClient) GetRoomRocket(ctx context.Context, in *GetRoomRocketRequest, opts ...grpc.CallOption) (*GetRoomRocketResponse, error) { func (c *roomQueryServiceClient) GetRoomTreasure(ctx context.Context, in *GetRoomTreasureRequest, opts ...grpc.CallOption) (*GetRoomTreasureResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetRoomRocketResponse) out := new(GetRoomTreasureResponse)
err := c.cc.Invoke(ctx, RoomQueryService_GetRoomRocket_FullMethodName, in, out, cOpts...) err := c.cc.Invoke(ctx, RoomQueryService_GetRoomTreasure_FullMethodName, in, out, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1903,22 +1615,17 @@ func (c *roomQueryServiceClient) ListRoomBannedUsers(ctx context.Context, in *Li
type RoomQueryServiceServer interface { type RoomQueryServiceServer interface {
AdminListRooms(context.Context, *AdminListRoomsRequest) (*AdminListRoomsResponse, error) AdminListRooms(context.Context, *AdminListRoomsRequest) (*AdminListRoomsResponse, error)
AdminGetRoom(context.Context, *AdminGetRoomRequest) (*AdminGetRoomResponse, error) AdminGetRoom(context.Context, *AdminGetRoomRequest) (*AdminGetRoomResponse, error)
AdminGetRoomRocketConfig(context.Context, *AdminGetRoomRocketConfigRequest) (*AdminGetRoomRocketConfigResponse, error) AdminGetRoomTreasureConfig(context.Context, *AdminGetRoomTreasureConfigRequest) (*AdminGetRoomTreasureConfigResponse, error)
AdminGetRoomSeatConfig(context.Context, *AdminGetRoomSeatConfigRequest) (*AdminGetRoomSeatConfigResponse, error) AdminGetRoomSeatConfig(context.Context, *AdminGetRoomSeatConfigRequest) (*AdminGetRoomSeatConfigResponse, error)
AdminGetHumanRoomRobotConfig(context.Context, *AdminGetHumanRoomRobotConfigRequest) (*AdminGetHumanRoomRobotConfigResponse, error)
AdminListRoomPins(context.Context, *AdminListRoomPinsRequest) (*AdminListRoomPinsResponse, error) AdminListRoomPins(context.Context, *AdminListRoomPinsRequest) (*AdminListRoomPinsResponse, error)
AdminListRobotRooms(context.Context, *AdminListRobotRoomsRequest) (*AdminListRobotRoomsResponse, error)
AdminFilterAvailableRoomRobots(context.Context, *AdminFilterAvailableRoomRobotsRequest) (*AdminFilterAvailableRoomRobotsResponse, error)
ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error)
ListRoomsByOwners(context.Context, *ListRoomsByOwnersRequest) (*ListRoomsResponse, error)
ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error) ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error)
ListRoomGiftLeaderboard(context.Context, *ListRoomGiftLeaderboardRequest) (*ListRoomGiftLeaderboardResponse, error) ListRoomGiftLeaderboard(context.Context, *ListRoomGiftLeaderboardRequest) (*ListRoomGiftLeaderboardResponse, error)
ListRoomContributionRank(context.Context, *ListRoomContributionRankRequest) (*ListRoomContributionRankResponse, error)
GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error) GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error)
GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error)
GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error)
ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error) ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error)
GetRoomRocket(context.Context, *GetRoomRocketRequest) (*GetRoomRocketResponse, error) GetRoomTreasure(context.Context, *GetRoomTreasureRequest) (*GetRoomTreasureResponse, error)
ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error) ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error)
ListRoomBannedUsers(context.Context, *ListRoomBannedUsersRequest) (*ListRoomBannedUsersResponse, error) ListRoomBannedUsers(context.Context, *ListRoomBannedUsersRequest) (*ListRoomBannedUsersResponse, error)
mustEmbedUnimplementedRoomQueryServiceServer() mustEmbedUnimplementedRoomQueryServiceServer()
@ -1937,39 +1644,24 @@ func (UnimplementedRoomQueryServiceServer) AdminListRooms(context.Context, *Admi
func (UnimplementedRoomQueryServiceServer) AdminGetRoom(context.Context, *AdminGetRoomRequest) (*AdminGetRoomResponse, error) { func (UnimplementedRoomQueryServiceServer) AdminGetRoom(context.Context, *AdminGetRoomRequest) (*AdminGetRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminGetRoom not implemented") return nil, status.Error(codes.Unimplemented, "method AdminGetRoom not implemented")
} }
func (UnimplementedRoomQueryServiceServer) AdminGetRoomRocketConfig(context.Context, *AdminGetRoomRocketConfigRequest) (*AdminGetRoomRocketConfigResponse, error) { func (UnimplementedRoomQueryServiceServer) AdminGetRoomTreasureConfig(context.Context, *AdminGetRoomTreasureConfigRequest) (*AdminGetRoomTreasureConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminGetRoomRocketConfig not implemented") return nil, status.Error(codes.Unimplemented, "method AdminGetRoomTreasureConfig not implemented")
} }
func (UnimplementedRoomQueryServiceServer) AdminGetRoomSeatConfig(context.Context, *AdminGetRoomSeatConfigRequest) (*AdminGetRoomSeatConfigResponse, error) { func (UnimplementedRoomQueryServiceServer) AdminGetRoomSeatConfig(context.Context, *AdminGetRoomSeatConfigRequest) (*AdminGetRoomSeatConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminGetRoomSeatConfig not implemented") return nil, status.Error(codes.Unimplemented, "method AdminGetRoomSeatConfig not implemented")
} }
func (UnimplementedRoomQueryServiceServer) AdminGetHumanRoomRobotConfig(context.Context, *AdminGetHumanRoomRobotConfigRequest) (*AdminGetHumanRoomRobotConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminGetHumanRoomRobotConfig not implemented")
}
func (UnimplementedRoomQueryServiceServer) AdminListRoomPins(context.Context, *AdminListRoomPinsRequest) (*AdminListRoomPinsResponse, error) { func (UnimplementedRoomQueryServiceServer) AdminListRoomPins(context.Context, *AdminListRoomPinsRequest) (*AdminListRoomPinsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminListRoomPins not implemented") return nil, status.Error(codes.Unimplemented, "method AdminListRoomPins not implemented")
} }
func (UnimplementedRoomQueryServiceServer) AdminListRobotRooms(context.Context, *AdminListRobotRoomsRequest) (*AdminListRobotRoomsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminListRobotRooms not implemented")
}
func (UnimplementedRoomQueryServiceServer) AdminFilterAvailableRoomRobots(context.Context, *AdminFilterAvailableRoomRobotsRequest) (*AdminFilterAvailableRoomRobotsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AdminFilterAvailableRoomRobots not implemented")
}
func (UnimplementedRoomQueryServiceServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) { func (UnimplementedRoomQueryServiceServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRooms not implemented") return nil, status.Error(codes.Unimplemented, "method ListRooms not implemented")
} }
func (UnimplementedRoomQueryServiceServer) ListRoomsByOwners(context.Context, *ListRoomsByOwnersRequest) (*ListRoomsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomsByOwners not implemented")
}
func (UnimplementedRoomQueryServiceServer) ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error) { func (UnimplementedRoomQueryServiceServer) ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomFeeds not implemented") return nil, status.Error(codes.Unimplemented, "method ListRoomFeeds not implemented")
} }
func (UnimplementedRoomQueryServiceServer) ListRoomGiftLeaderboard(context.Context, *ListRoomGiftLeaderboardRequest) (*ListRoomGiftLeaderboardResponse, error) { func (UnimplementedRoomQueryServiceServer) ListRoomGiftLeaderboard(context.Context, *ListRoomGiftLeaderboardRequest) (*ListRoomGiftLeaderboardResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomGiftLeaderboard not implemented") return nil, status.Error(codes.Unimplemented, "method ListRoomGiftLeaderboard not implemented")
} }
func (UnimplementedRoomQueryServiceServer) ListRoomContributionRank(context.Context, *ListRoomContributionRankRequest) (*ListRoomContributionRankResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomContributionRank not implemented")
}
func (UnimplementedRoomQueryServiceServer) GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error) { func (UnimplementedRoomQueryServiceServer) GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetMyRoom not implemented") return nil, status.Error(codes.Unimplemented, "method GetMyRoom not implemented")
} }
@ -1982,8 +1674,8 @@ func (UnimplementedRoomQueryServiceServer) GetRoomSnapshot(context.Context, *Get
func (UnimplementedRoomQueryServiceServer) ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error) { func (UnimplementedRoomQueryServiceServer) ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomBackgrounds not implemented") return nil, status.Error(codes.Unimplemented, "method ListRoomBackgrounds not implemented")
} }
func (UnimplementedRoomQueryServiceServer) GetRoomRocket(context.Context, *GetRoomRocketRequest) (*GetRoomRocketResponse, error) { func (UnimplementedRoomQueryServiceServer) GetRoomTreasure(context.Context, *GetRoomTreasureRequest) (*GetRoomTreasureResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRoomRocket not implemented") return nil, status.Error(codes.Unimplemented, "method GetRoomTreasure not implemented")
} }
func (UnimplementedRoomQueryServiceServer) ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error) { func (UnimplementedRoomQueryServiceServer) ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomOnlineUsers not implemented") return nil, status.Error(codes.Unimplemented, "method ListRoomOnlineUsers not implemented")
@ -2048,20 +1740,20 @@ func _RoomQueryService_AdminGetRoom_Handler(srv interface{}, ctx context.Context
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _RoomQueryService_AdminGetRoomRocketConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _RoomQueryService_AdminGetRoomTreasureConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminGetRoomRocketConfigRequest) in := new(AdminGetRoomTreasureConfigRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(RoomQueryServiceServer).AdminGetRoomRocketConfig(ctx, in) return srv.(RoomQueryServiceServer).AdminGetRoomTreasureConfig(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: RoomQueryService_AdminGetRoomRocketConfig_FullMethodName, FullMethod: RoomQueryService_AdminGetRoomTreasureConfig_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).AdminGetRoomRocketConfig(ctx, req.(*AdminGetRoomRocketConfigRequest)) return srv.(RoomQueryServiceServer).AdminGetRoomTreasureConfig(ctx, req.(*AdminGetRoomTreasureConfigRequest))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
@ -2084,24 +1776,6 @@ func _RoomQueryService_AdminGetRoomSeatConfig_Handler(srv interface{}, ctx conte
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _RoomQueryService_AdminGetHumanRoomRobotConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminGetHumanRoomRobotConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomQueryServiceServer).AdminGetHumanRoomRobotConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomQueryService_AdminGetHumanRoomRobotConfig_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).AdminGetHumanRoomRobotConfig(ctx, req.(*AdminGetHumanRoomRobotConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomQueryService_AdminListRoomPins_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _RoomQueryService_AdminListRoomPins_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminListRoomPinsRequest) in := new(AdminListRoomPinsRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -2120,42 +1794,6 @@ func _RoomQueryService_AdminListRoomPins_Handler(srv interface{}, ctx context.Co
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _RoomQueryService_AdminListRobotRooms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminListRobotRoomsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomQueryServiceServer).AdminListRobotRooms(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomQueryService_AdminListRobotRooms_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).AdminListRobotRooms(ctx, req.(*AdminListRobotRoomsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomQueryService_AdminFilterAvailableRoomRobots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AdminFilterAvailableRoomRobotsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomQueryServiceServer).AdminFilterAvailableRoomRobots(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomQueryService_AdminFilterAvailableRoomRobots_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).AdminFilterAvailableRoomRobots(ctx, req.(*AdminFilterAvailableRoomRobotsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomQueryService_ListRooms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _RoomQueryService_ListRooms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRoomsRequest) in := new(ListRoomsRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -2174,24 +1812,6 @@ func _RoomQueryService_ListRooms_Handler(srv interface{}, ctx context.Context, d
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _RoomQueryService_ListRoomsByOwners_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRoomsByOwnersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomQueryServiceServer).ListRoomsByOwners(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomQueryService_ListRoomsByOwners_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).ListRoomsByOwners(ctx, req.(*ListRoomsByOwnersRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomQueryService_ListRoomFeeds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _RoomQueryService_ListRoomFeeds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRoomFeedsRequest) in := new(ListRoomFeedsRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -2228,24 +1848,6 @@ func _RoomQueryService_ListRoomGiftLeaderboard_Handler(srv interface{}, ctx cont
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _RoomQueryService_ListRoomContributionRank_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRoomContributionRankRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomQueryServiceServer).ListRoomContributionRank(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomQueryService_ListRoomContributionRank_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).ListRoomContributionRank(ctx, req.(*ListRoomContributionRankRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomQueryService_GetMyRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _RoomQueryService_GetMyRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMyRoomRequest) in := new(GetMyRoomRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -2318,20 +1920,20 @@ func _RoomQueryService_ListRoomBackgrounds_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _RoomQueryService_GetRoomRocket_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _RoomQueryService_GetRoomTreasure_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetRoomRocketRequest) in := new(GetRoomTreasureRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(RoomQueryServiceServer).GetRoomRocket(ctx, in) return srv.(RoomQueryServiceServer).GetRoomTreasure(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: RoomQueryService_GetRoomRocket_FullMethodName, FullMethod: RoomQueryService_GetRoomTreasure_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).GetRoomRocket(ctx, req.(*GetRoomRocketRequest)) return srv.(RoomQueryServiceServer).GetRoomTreasure(ctx, req.(*GetRoomTreasureRequest))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
@ -2388,37 +1990,21 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
Handler: _RoomQueryService_AdminGetRoom_Handler, Handler: _RoomQueryService_AdminGetRoom_Handler,
}, },
{ {
MethodName: "AdminGetRoomRocketConfig", MethodName: "AdminGetRoomTreasureConfig",
Handler: _RoomQueryService_AdminGetRoomRocketConfig_Handler, Handler: _RoomQueryService_AdminGetRoomTreasureConfig_Handler,
}, },
{ {
MethodName: "AdminGetRoomSeatConfig", MethodName: "AdminGetRoomSeatConfig",
Handler: _RoomQueryService_AdminGetRoomSeatConfig_Handler, Handler: _RoomQueryService_AdminGetRoomSeatConfig_Handler,
}, },
{
MethodName: "AdminGetHumanRoomRobotConfig",
Handler: _RoomQueryService_AdminGetHumanRoomRobotConfig_Handler,
},
{ {
MethodName: "AdminListRoomPins", MethodName: "AdminListRoomPins",
Handler: _RoomQueryService_AdminListRoomPins_Handler, Handler: _RoomQueryService_AdminListRoomPins_Handler,
}, },
{
MethodName: "AdminListRobotRooms",
Handler: _RoomQueryService_AdminListRobotRooms_Handler,
},
{
MethodName: "AdminFilterAvailableRoomRobots",
Handler: _RoomQueryService_AdminFilterAvailableRoomRobots_Handler,
},
{ {
MethodName: "ListRooms", MethodName: "ListRooms",
Handler: _RoomQueryService_ListRooms_Handler, Handler: _RoomQueryService_ListRooms_Handler,
}, },
{
MethodName: "ListRoomsByOwners",
Handler: _RoomQueryService_ListRoomsByOwners_Handler,
},
{ {
MethodName: "ListRoomFeeds", MethodName: "ListRoomFeeds",
Handler: _RoomQueryService_ListRoomFeeds_Handler, Handler: _RoomQueryService_ListRoomFeeds_Handler,
@ -2427,10 +2013,6 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListRoomGiftLeaderboard", MethodName: "ListRoomGiftLeaderboard",
Handler: _RoomQueryService_ListRoomGiftLeaderboard_Handler, Handler: _RoomQueryService_ListRoomGiftLeaderboard_Handler,
}, },
{
MethodName: "ListRoomContributionRank",
Handler: _RoomQueryService_ListRoomContributionRank_Handler,
},
{ {
MethodName: "GetMyRoom", MethodName: "GetMyRoom",
Handler: _RoomQueryService_GetMyRoom_Handler, Handler: _RoomQueryService_GetMyRoom_Handler,
@ -2448,8 +2030,8 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
Handler: _RoomQueryService_ListRoomBackgrounds_Handler, Handler: _RoomQueryService_ListRoomBackgrounds_Handler,
}, },
{ {
MethodName: "GetRoomRocket", MethodName: "GetRoomTreasure",
Handler: _RoomQueryService_GetRoomRocket_Handler, Handler: _RoomQueryService_GetRoomTreasure_Handler,
}, },
{ {
MethodName: "ListRoomOnlineUsers", MethodName: "ListRoomOnlineUsers",

File diff suppressed because it is too large Load Diff

View File

@ -37,42 +37,6 @@ message LoginThirdPartyRequest {
string campaign = 20; string campaign = 20;
} }
// RegisterThirdPartyRequest 使
// profile_required credential session
message RegisterThirdPartyRequest {
RequestMeta meta = 1;
string provider = 2;
string credential = 3;
string device_id = 4;
string username = 5;
string avatar = 6;
string gender = 7;
string country = 8;
string invite_code = 9;
string device = 10;
string os_version = 11;
string birth = 12;
string app_version = 13;
string build_number = 14;
string source = 15;
string install_channel = 16;
string campaign = 17;
string platform = 18;
string language = 19;
string timezone = 20;
}
// CheckThirdPartyRegisteredRequest
message CheckThirdPartyRegisteredRequest {
RequestMeta meta = 1;
string provider = 2;
string credential = 3;
}
message CheckThirdPartyRegisteredResponse {
bool registered = 1;
}
// AuthResponse // AuthResponse
message AuthResponse { message AuthResponse {
AuthToken token = 1; AuthToken token = 1;
@ -93,7 +57,7 @@ message SetPasswordResponse {
bool password_set = 1; bool password_set = 1;
} }
// QuickCreateAccountRequest source=game_robot // QuickCreateAccountRequest
message QuickCreateAccountRequest { message QuickCreateAccountRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
string password = 2; string password = 2;
@ -116,7 +80,7 @@ message QuickCreateAccountRequest {
string timezone = 19; string timezone = 19;
} }
// QuickCreateAccountResponse source=game_robot token user_id/display_user_id // QuickCreateAccountResponse display_user_id
message QuickCreateAccountResponse { message QuickCreateAccountResponse {
AuthToken token = 1; AuthToken token = 1;
bool password_set = 2; bool password_set = 2;
@ -160,87 +124,13 @@ message RecordLoginBlockedResponse {
bool recorded = 1; bool recorded = 1;
} }
// CheckLoginRiskIPWhitelistRequest IP
message CheckLoginRiskIPWhitelistRequest {
RequestMeta meta = 1;
string client_ip = 2;
}
// CheckLoginRiskIPWhitelistResponse IP
message CheckLoginRiskIPWhitelistResponse {
bool whitelisted = 1;
}
// RefreshLoginRiskWhitelistCacheRequest user-service MySQL
message RefreshLoginRiskWhitelistCacheRequest {
RequestMeta meta = 1;
}
// RefreshLoginRiskWhitelistCacheResponse 便
message RefreshLoginRiskWhitelistCacheResponse {
bool refreshed = 1;
int32 ip_whitelist_count = 2;
int32 user_whitelist_count = 3;
int64 user_cache_expires_at_ms = 4;
}
// RegisterRiskConfig
message RegisterRiskConfig {
string app_code = 1;
int32 max_accounts_per_device = 2;
int64 updated_at_ms = 3;
int64 updated_by_admin_id = 4;
}
// GetRegisterRiskConfigRequest App
message GetRegisterRiskConfigRequest {
RequestMeta meta = 1;
}
message GetRegisterRiskConfigResponse {
RegisterRiskConfig config = 1;
}
// UpdateRegisterRiskConfigRequest App
message UpdateRegisterRiskConfigRequest {
RequestMeta meta = 1;
int32 max_accounts_per_device = 2;
int64 operator_admin_id = 3;
}
message UpdateRegisterRiskConfigResponse {
RegisterRiskConfig config = 1;
}
// AppHeartbeatRequest App 线
message AppHeartbeatRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string session_id = 3;
}
// AppHeartbeatResponse access token
message AppHeartbeatResponse {
bool accepted = 1;
int64 heartbeat_at_ms = 2;
int64 server_time_ms = 3;
AuthToken token = 4;
}
// AuthService token // AuthService token
service AuthService { service AuthService {
rpc LoginPassword(LoginPasswordRequest) returns (AuthResponse); rpc LoginPassword(LoginPasswordRequest) returns (AuthResponse);
rpc LoginThirdParty(LoginThirdPartyRequest) returns (AuthResponse); rpc LoginThirdParty(LoginThirdPartyRequest) returns (AuthResponse);
rpc RegisterThirdParty(RegisterThirdPartyRequest) returns (AuthResponse);
rpc CheckThirdPartyRegistered(CheckThirdPartyRegisteredRequest) returns (CheckThirdPartyRegisteredResponse);
rpc SetPassword(SetPasswordRequest) returns (SetPasswordResponse); rpc SetPassword(SetPasswordRequest) returns (SetPasswordResponse);
rpc QuickCreateAccount(QuickCreateAccountRequest) returns (QuickCreateAccountResponse); rpc QuickCreateAccount(QuickCreateAccountRequest) returns (QuickCreateAccountResponse);
rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse); rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse);
rpc Logout(LogoutRequest) returns (LogoutResponse); rpc Logout(LogoutRequest) returns (LogoutResponse);
rpc RecordLoginBlocked(RecordLoginBlockedRequest) returns (RecordLoginBlockedResponse); rpc RecordLoginBlocked(RecordLoginBlockedRequest) returns (RecordLoginBlockedResponse);
rpc CheckLoginRiskIPWhitelist(CheckLoginRiskIPWhitelistRequest) returns (CheckLoginRiskIPWhitelistResponse);
rpc RefreshLoginRiskWhitelistCache(RefreshLoginRiskWhitelistCacheRequest) returns (RefreshLoginRiskWhitelistCacheResponse);
rpc GetRegisterRiskConfig(GetRegisterRiskConfigRequest) returns (GetRegisterRiskConfigResponse);
rpc UpdateRegisterRiskConfig(UpdateRegisterRiskConfigRequest) returns (UpdateRegisterRiskConfigResponse);
rpc AppHeartbeat(AppHeartbeatRequest) returns (AppHeartbeatResponse);
} }

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT. // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions: // versions:
// - protoc-gen-go-grpc v1.6.2 // - protoc-gen-go-grpc v1.6.2
// - protoc v5.29.2 // - protoc v7.35.0
// source: proto/user/v1/auth.proto // source: proto/user/v1/auth.proto
package userv1 package userv1
@ -21,18 +21,11 @@ const _ = grpc.SupportPackageIsVersion9
const ( const (
AuthService_LoginPassword_FullMethodName = "/hyapp.user.v1.AuthService/LoginPassword" AuthService_LoginPassword_FullMethodName = "/hyapp.user.v1.AuthService/LoginPassword"
AuthService_LoginThirdParty_FullMethodName = "/hyapp.user.v1.AuthService/LoginThirdParty" AuthService_LoginThirdParty_FullMethodName = "/hyapp.user.v1.AuthService/LoginThirdParty"
AuthService_RegisterThirdParty_FullMethodName = "/hyapp.user.v1.AuthService/RegisterThirdParty"
AuthService_CheckThirdPartyRegistered_FullMethodName = "/hyapp.user.v1.AuthService/CheckThirdPartyRegistered"
AuthService_SetPassword_FullMethodName = "/hyapp.user.v1.AuthService/SetPassword" AuthService_SetPassword_FullMethodName = "/hyapp.user.v1.AuthService/SetPassword"
AuthService_QuickCreateAccount_FullMethodName = "/hyapp.user.v1.AuthService/QuickCreateAccount" AuthService_QuickCreateAccount_FullMethodName = "/hyapp.user.v1.AuthService/QuickCreateAccount"
AuthService_RefreshToken_FullMethodName = "/hyapp.user.v1.AuthService/RefreshToken" AuthService_RefreshToken_FullMethodName = "/hyapp.user.v1.AuthService/RefreshToken"
AuthService_Logout_FullMethodName = "/hyapp.user.v1.AuthService/Logout" AuthService_Logout_FullMethodName = "/hyapp.user.v1.AuthService/Logout"
AuthService_RecordLoginBlocked_FullMethodName = "/hyapp.user.v1.AuthService/RecordLoginBlocked" AuthService_RecordLoginBlocked_FullMethodName = "/hyapp.user.v1.AuthService/RecordLoginBlocked"
AuthService_CheckLoginRiskIPWhitelist_FullMethodName = "/hyapp.user.v1.AuthService/CheckLoginRiskIPWhitelist"
AuthService_RefreshLoginRiskWhitelistCache_FullMethodName = "/hyapp.user.v1.AuthService/RefreshLoginRiskWhitelistCache"
AuthService_GetRegisterRiskConfig_FullMethodName = "/hyapp.user.v1.AuthService/GetRegisterRiskConfig"
AuthService_UpdateRegisterRiskConfig_FullMethodName = "/hyapp.user.v1.AuthService/UpdateRegisterRiskConfig"
AuthService_AppHeartbeat_FullMethodName = "/hyapp.user.v1.AuthService/AppHeartbeat"
) )
// AuthServiceClient is the client API for AuthService service. // AuthServiceClient is the client API for AuthService service.
@ -43,18 +36,11 @@ const (
type AuthServiceClient interface { type AuthServiceClient interface {
LoginPassword(ctx context.Context, in *LoginPasswordRequest, opts ...grpc.CallOption) (*AuthResponse, error) LoginPassword(ctx context.Context, in *LoginPasswordRequest, opts ...grpc.CallOption) (*AuthResponse, error)
LoginThirdParty(ctx context.Context, in *LoginThirdPartyRequest, opts ...grpc.CallOption) (*AuthResponse, error) LoginThirdParty(ctx context.Context, in *LoginThirdPartyRequest, opts ...grpc.CallOption) (*AuthResponse, error)
RegisterThirdParty(ctx context.Context, in *RegisterThirdPartyRequest, opts ...grpc.CallOption) (*AuthResponse, error)
CheckThirdPartyRegistered(ctx context.Context, in *CheckThirdPartyRegisteredRequest, opts ...grpc.CallOption) (*CheckThirdPartyRegisteredResponse, error)
SetPassword(ctx context.Context, in *SetPasswordRequest, opts ...grpc.CallOption) (*SetPasswordResponse, error) SetPassword(ctx context.Context, in *SetPasswordRequest, opts ...grpc.CallOption) (*SetPasswordResponse, error)
QuickCreateAccount(ctx context.Context, in *QuickCreateAccountRequest, opts ...grpc.CallOption) (*QuickCreateAccountResponse, error) QuickCreateAccount(ctx context.Context, in *QuickCreateAccountRequest, opts ...grpc.CallOption) (*QuickCreateAccountResponse, error)
RefreshToken(ctx context.Context, in *RefreshTokenRequest, opts ...grpc.CallOption) (*RefreshTokenResponse, error) RefreshToken(ctx context.Context, in *RefreshTokenRequest, opts ...grpc.CallOption) (*RefreshTokenResponse, error)
Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error)
RecordLoginBlocked(ctx context.Context, in *RecordLoginBlockedRequest, opts ...grpc.CallOption) (*RecordLoginBlockedResponse, error) RecordLoginBlocked(ctx context.Context, in *RecordLoginBlockedRequest, opts ...grpc.CallOption) (*RecordLoginBlockedResponse, error)
CheckLoginRiskIPWhitelist(ctx context.Context, in *CheckLoginRiskIPWhitelistRequest, opts ...grpc.CallOption) (*CheckLoginRiskIPWhitelistResponse, error)
RefreshLoginRiskWhitelistCache(ctx context.Context, in *RefreshLoginRiskWhitelistCacheRequest, opts ...grpc.CallOption) (*RefreshLoginRiskWhitelistCacheResponse, error)
GetRegisterRiskConfig(ctx context.Context, in *GetRegisterRiskConfigRequest, opts ...grpc.CallOption) (*GetRegisterRiskConfigResponse, error)
UpdateRegisterRiskConfig(ctx context.Context, in *UpdateRegisterRiskConfigRequest, opts ...grpc.CallOption) (*UpdateRegisterRiskConfigResponse, error)
AppHeartbeat(ctx context.Context, in *AppHeartbeatRequest, opts ...grpc.CallOption) (*AppHeartbeatResponse, error)
} }
type authServiceClient struct { type authServiceClient struct {
@ -85,26 +71,6 @@ func (c *authServiceClient) LoginThirdParty(ctx context.Context, in *LoginThirdP
return out, nil return out, nil
} }
func (c *authServiceClient) RegisterThirdParty(ctx context.Context, in *RegisterThirdPartyRequest, opts ...grpc.CallOption) (*AuthResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AuthResponse)
err := c.cc.Invoke(ctx, AuthService_RegisterThirdParty_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) CheckThirdPartyRegistered(ctx context.Context, in *CheckThirdPartyRegisteredRequest, opts ...grpc.CallOption) (*CheckThirdPartyRegisteredResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CheckThirdPartyRegisteredResponse)
err := c.cc.Invoke(ctx, AuthService_CheckThirdPartyRegistered_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) SetPassword(ctx context.Context, in *SetPasswordRequest, opts ...grpc.CallOption) (*SetPasswordResponse, error) { func (c *authServiceClient) SetPassword(ctx context.Context, in *SetPasswordRequest, opts ...grpc.CallOption) (*SetPasswordResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SetPasswordResponse) out := new(SetPasswordResponse)
@ -155,56 +121,6 @@ func (c *authServiceClient) RecordLoginBlocked(ctx context.Context, in *RecordLo
return out, nil return out, nil
} }
func (c *authServiceClient) CheckLoginRiskIPWhitelist(ctx context.Context, in *CheckLoginRiskIPWhitelistRequest, opts ...grpc.CallOption) (*CheckLoginRiskIPWhitelistResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CheckLoginRiskIPWhitelistResponse)
err := c.cc.Invoke(ctx, AuthService_CheckLoginRiskIPWhitelist_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) RefreshLoginRiskWhitelistCache(ctx context.Context, in *RefreshLoginRiskWhitelistCacheRequest, opts ...grpc.CallOption) (*RefreshLoginRiskWhitelistCacheResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RefreshLoginRiskWhitelistCacheResponse)
err := c.cc.Invoke(ctx, AuthService_RefreshLoginRiskWhitelistCache_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) GetRegisterRiskConfig(ctx context.Context, in *GetRegisterRiskConfigRequest, opts ...grpc.CallOption) (*GetRegisterRiskConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetRegisterRiskConfigResponse)
err := c.cc.Invoke(ctx, AuthService_GetRegisterRiskConfig_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) UpdateRegisterRiskConfig(ctx context.Context, in *UpdateRegisterRiskConfigRequest, opts ...grpc.CallOption) (*UpdateRegisterRiskConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateRegisterRiskConfigResponse)
err := c.cc.Invoke(ctx, AuthService_UpdateRegisterRiskConfig_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) AppHeartbeat(ctx context.Context, in *AppHeartbeatRequest, opts ...grpc.CallOption) (*AppHeartbeatResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(AppHeartbeatResponse)
err := c.cc.Invoke(ctx, AuthService_AppHeartbeat_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AuthServiceServer is the server API for AuthService service. // AuthServiceServer is the server API for AuthService service.
// All implementations must embed UnimplementedAuthServiceServer // All implementations must embed UnimplementedAuthServiceServer
// for forward compatibility. // for forward compatibility.
@ -213,18 +129,11 @@ func (c *authServiceClient) AppHeartbeat(ctx context.Context, in *AppHeartbeatRe
type AuthServiceServer interface { type AuthServiceServer interface {
LoginPassword(context.Context, *LoginPasswordRequest) (*AuthResponse, error) LoginPassword(context.Context, *LoginPasswordRequest) (*AuthResponse, error)
LoginThirdParty(context.Context, *LoginThirdPartyRequest) (*AuthResponse, error) LoginThirdParty(context.Context, *LoginThirdPartyRequest) (*AuthResponse, error)
RegisterThirdParty(context.Context, *RegisterThirdPartyRequest) (*AuthResponse, error)
CheckThirdPartyRegistered(context.Context, *CheckThirdPartyRegisteredRequest) (*CheckThirdPartyRegisteredResponse, error)
SetPassword(context.Context, *SetPasswordRequest) (*SetPasswordResponse, error) SetPassword(context.Context, *SetPasswordRequest) (*SetPasswordResponse, error)
QuickCreateAccount(context.Context, *QuickCreateAccountRequest) (*QuickCreateAccountResponse, error) QuickCreateAccount(context.Context, *QuickCreateAccountRequest) (*QuickCreateAccountResponse, error)
RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error) RefreshToken(context.Context, *RefreshTokenRequest) (*RefreshTokenResponse, error)
Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error)
RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error) RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error)
CheckLoginRiskIPWhitelist(context.Context, *CheckLoginRiskIPWhitelistRequest) (*CheckLoginRiskIPWhitelistResponse, error)
RefreshLoginRiskWhitelistCache(context.Context, *RefreshLoginRiskWhitelistCacheRequest) (*RefreshLoginRiskWhitelistCacheResponse, error)
GetRegisterRiskConfig(context.Context, *GetRegisterRiskConfigRequest) (*GetRegisterRiskConfigResponse, error)
UpdateRegisterRiskConfig(context.Context, *UpdateRegisterRiskConfigRequest) (*UpdateRegisterRiskConfigResponse, error)
AppHeartbeat(context.Context, *AppHeartbeatRequest) (*AppHeartbeatResponse, error)
mustEmbedUnimplementedAuthServiceServer() mustEmbedUnimplementedAuthServiceServer()
} }
@ -241,12 +150,6 @@ func (UnimplementedAuthServiceServer) LoginPassword(context.Context, *LoginPassw
func (UnimplementedAuthServiceServer) LoginThirdParty(context.Context, *LoginThirdPartyRequest) (*AuthResponse, error) { func (UnimplementedAuthServiceServer) LoginThirdParty(context.Context, *LoginThirdPartyRequest) (*AuthResponse, error) {
return nil, status.Error(codes.Unimplemented, "method LoginThirdParty not implemented") return nil, status.Error(codes.Unimplemented, "method LoginThirdParty not implemented")
} }
func (UnimplementedAuthServiceServer) RegisterThirdParty(context.Context, *RegisterThirdPartyRequest) (*AuthResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RegisterThirdParty not implemented")
}
func (UnimplementedAuthServiceServer) CheckThirdPartyRegistered(context.Context, *CheckThirdPartyRegisteredRequest) (*CheckThirdPartyRegisteredResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CheckThirdPartyRegistered not implemented")
}
func (UnimplementedAuthServiceServer) SetPassword(context.Context, *SetPasswordRequest) (*SetPasswordResponse, error) { func (UnimplementedAuthServiceServer) SetPassword(context.Context, *SetPasswordRequest) (*SetPasswordResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetPassword not implemented") return nil, status.Error(codes.Unimplemented, "method SetPassword not implemented")
} }
@ -262,21 +165,6 @@ func (UnimplementedAuthServiceServer) Logout(context.Context, *LogoutRequest) (*
func (UnimplementedAuthServiceServer) RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error) { func (UnimplementedAuthServiceServer) RecordLoginBlocked(context.Context, *RecordLoginBlockedRequest) (*RecordLoginBlockedResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RecordLoginBlocked not implemented") return nil, status.Error(codes.Unimplemented, "method RecordLoginBlocked not implemented")
} }
func (UnimplementedAuthServiceServer) CheckLoginRiskIPWhitelist(context.Context, *CheckLoginRiskIPWhitelistRequest) (*CheckLoginRiskIPWhitelistResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CheckLoginRiskIPWhitelist not implemented")
}
func (UnimplementedAuthServiceServer) RefreshLoginRiskWhitelistCache(context.Context, *RefreshLoginRiskWhitelistCacheRequest) (*RefreshLoginRiskWhitelistCacheResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RefreshLoginRiskWhitelistCache not implemented")
}
func (UnimplementedAuthServiceServer) GetRegisterRiskConfig(context.Context, *GetRegisterRiskConfigRequest) (*GetRegisterRiskConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRegisterRiskConfig not implemented")
}
func (UnimplementedAuthServiceServer) UpdateRegisterRiskConfig(context.Context, *UpdateRegisterRiskConfigRequest) (*UpdateRegisterRiskConfigResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateRegisterRiskConfig not implemented")
}
func (UnimplementedAuthServiceServer) AppHeartbeat(context.Context, *AppHeartbeatRequest) (*AppHeartbeatResponse, error) {
return nil, status.Error(codes.Unimplemented, "method AppHeartbeat not implemented")
}
func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {} func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}
func (UnimplementedAuthServiceServer) testEmbeddedByValue() {} func (UnimplementedAuthServiceServer) testEmbeddedByValue() {}
@ -334,42 +222,6 @@ func _AuthService_LoginThirdParty_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _AuthService_RegisterThirdParty_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RegisterThirdPartyRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).RegisterThirdParty(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_RegisterThirdParty_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).RegisterThirdParty(ctx, req.(*RegisterThirdPartyRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_CheckThirdPartyRegistered_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CheckThirdPartyRegisteredRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).CheckThirdPartyRegistered(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_CheckThirdPartyRegistered_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).CheckThirdPartyRegistered(ctx, req.(*CheckThirdPartyRegisteredRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_SetPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _AuthService_SetPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetPasswordRequest) in := new(SetPasswordRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -460,96 +312,6 @@ func _AuthService_RecordLoginBlocked_Handler(srv interface{}, ctx context.Contex
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _AuthService_CheckLoginRiskIPWhitelist_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CheckLoginRiskIPWhitelistRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).CheckLoginRiskIPWhitelist(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_CheckLoginRiskIPWhitelist_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).CheckLoginRiskIPWhitelist(ctx, req.(*CheckLoginRiskIPWhitelistRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_RefreshLoginRiskWhitelistCache_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RefreshLoginRiskWhitelistCacheRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).RefreshLoginRiskWhitelistCache(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_RefreshLoginRiskWhitelistCache_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).RefreshLoginRiskWhitelistCache(ctx, req.(*RefreshLoginRiskWhitelistCacheRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_GetRegisterRiskConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetRegisterRiskConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).GetRegisterRiskConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_GetRegisterRiskConfig_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).GetRegisterRiskConfig(ctx, req.(*GetRegisterRiskConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_UpdateRegisterRiskConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateRegisterRiskConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).UpdateRegisterRiskConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_UpdateRegisterRiskConfig_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).UpdateRegisterRiskConfig(ctx, req.(*UpdateRegisterRiskConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_AppHeartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AppHeartbeatRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).AppHeartbeat(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_AppHeartbeat_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).AppHeartbeat(ctx, req.(*AppHeartbeatRequest))
}
return interceptor(ctx, in, info, handler)
}
// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service. // AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@ -565,14 +327,6 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{
MethodName: "LoginThirdParty", MethodName: "LoginThirdParty",
Handler: _AuthService_LoginThirdParty_Handler, Handler: _AuthService_LoginThirdParty_Handler,
}, },
{
MethodName: "RegisterThirdParty",
Handler: _AuthService_RegisterThirdParty_Handler,
},
{
MethodName: "CheckThirdPartyRegistered",
Handler: _AuthService_CheckThirdPartyRegistered_Handler,
},
{ {
MethodName: "SetPassword", MethodName: "SetPassword",
Handler: _AuthService_SetPassword_Handler, Handler: _AuthService_SetPassword_Handler,
@ -593,26 +347,6 @@ var AuthService_ServiceDesc = grpc.ServiceDesc{
MethodName: "RecordLoginBlocked", MethodName: "RecordLoginBlocked",
Handler: _AuthService_RecordLoginBlocked_Handler, Handler: _AuthService_RecordLoginBlocked_Handler,
}, },
{
MethodName: "CheckLoginRiskIPWhitelist",
Handler: _AuthService_CheckLoginRiskIPWhitelist_Handler,
},
{
MethodName: "RefreshLoginRiskWhitelistCache",
Handler: _AuthService_RefreshLoginRiskWhitelistCache_Handler,
},
{
MethodName: "GetRegisterRiskConfig",
Handler: _AuthService_GetRegisterRiskConfig_Handler,
},
{
MethodName: "UpdateRegisterRiskConfig",
Handler: _AuthService_UpdateRegisterRiskConfig_Handler,
},
{
MethodName: "AppHeartbeat",
Handler: _AuthService_AppHeartbeat_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "proto/user/v1/auth.proto", Metadata: "proto/user/v1/auth.proto",

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,6 @@ message Agency {
string name = 5; string name = 5;
string status = 6; string status = 6;
bool join_enabled = 7; bool join_enabled = 7;
// Deprecated: Agency
int32 max_hosts = 8; int32 max_hosts = 8;
int64 created_by_user_id = 9; int64 created_by_user_id = 9;
int64 created_at_ms = 10; int64 created_at_ms = 10;
@ -59,7 +58,6 @@ message CoinSellerProfile {
int64 created_by_user_id = 4; int64 created_by_user_id = 4;
int64 created_at_ms = 5; int64 created_at_ms = 5;
int64 updated_at_ms = 6; int64 updated_at_ms = 6;
bool can_manage_sub_coin_sellers = 7;
} }
// CoinSellerListItem App // CoinSellerListItem App
@ -78,34 +76,6 @@ message CoinSellerListItem {
string status = 12; string status = 12;
string merchant_asset_type = 13; string merchant_asset_type = 13;
int64 updated_at_ms = 14; int64 updated_at_ms = 14;
string contact_info = 15;
bool can_manage_sub_coin_sellers = 16;
}
// CoinSellerSubRelation
message CoinSellerSubRelation {
int64 relation_id = 1;
int64 parent_user_id = 2;
int64 child_user_id = 3;
string status = 4;
int64 created_by_user_id = 5;
int64 created_at_ms = 6;
int64 updated_at_ms = 7;
}
// CoinSellerSubApplication
message CoinSellerSubApplication {
int64 application_id = 1;
string command_id = 2;
int64 parent_user_id = 3;
int64 target_user_id = 4;
string status = 5;
int64 relation_id = 6;
int64 reviewed_by_admin_id = 7;
string review_reason = 8;
int64 reviewed_at_ms = 9;
int64 created_at_ms = 10;
int64 updated_at_ms = 11;
} }
// UserRoleSummary App 使 // UserRoleSummary App 使
@ -124,8 +94,6 @@ message UserRoleSummary {
string coin_seller_status = 11; string coin_seller_status = 11;
int64 pending_role_invitations = 12; int64 pending_role_invitations = 12;
bool is_manager = 13; bool is_manager = 13;
// roles App
repeated string roles = 14;
} }
// AgencyMembership host Agency // AgencyMembership host Agency
@ -253,94 +221,6 @@ message InviteBDResponse {
RoleInvitation invitation = 1; RoleInvitation invitation = 1;
} }
message InviteHostRequest {
RequestMeta meta = 1;
string command_id = 2;
int64 inviter_user_id = 3;
int64 target_user_id = 4;
}
message InviteHostResponse {
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 ListManagerTeamAgenciesRequest {
RequestMeta meta = 1;
int64 manager_user_id = 2;
// page_size truncated=true
int32 page_size = 3;
}
// ManagerTeamAgency
// Agency //
message ManagerTeamAgency {
int64 agency_id = 1;
int64 owner_user_id = 2;
int64 parent_bd_user_id = 3;
}
message ListManagerTeamAgenciesResponse {
// agencies BD Leader Agency + Leader BD Agency
repeated ManagerTeamAgency agencies = 1;
int32 total_bd_leaders = 2;
int32 total_bds = 3;
bool truncated = 4;
}
message ListBDAgenciesRequest {
RequestMeta meta = 1;
int64 bd_user_id = 2;
string status = 3;
int32 page_size = 4;
}
message ListBDAgenciesResponse {
repeated Agency agencies = 1;
}
message ListRoleInvitationsRequest {
RequestMeta meta = 1;
int64 target_user_id = 2;
string status = 3;
int32 page_size = 4;
}
message ListRoleInvitationsResponse {
repeated RoleInvitation invitations = 1;
}
message GetRoleInvitationRequest {
RequestMeta meta = 1;
int64 actor_user_id = 2;
int64 invitation_id = 3;
}
message GetRoleInvitationResponse {
RoleInvitation invitation = 1;
}
message ProcessRoleInvitationRequest { message ProcessRoleInvitationRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
string command_id = 2; string command_id = 2;
@ -367,19 +247,9 @@ message GetHostProfileResponse {
HostProfile host_profile = 1; HostProfile host_profile = 1;
} }
message BatchGetHostProfilesRequest {
RequestMeta meta = 1;
repeated int64 user_ids = 2;
}
message BatchGetHostProfilesResponse {
map<int64, HostProfile> host_profiles = 1;
}
message GetBDProfileRequest { message GetBDProfileRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
int64 user_id = 2; int64 user_id = 2;
string role = 3;
} }
message GetBDProfileResponse { message GetBDProfileResponse {
@ -404,55 +274,6 @@ message ListActiveCoinSellersInMyRegionResponse {
repeated CoinSellerListItem coin_sellers = 1; repeated CoinSellerListItem coin_sellers = 1;
} }
message CreateSubCoinSellerRequest {
RequestMeta meta = 1;
string command_id = 2;
int64 parent_user_id = 3;
int64 child_user_id = 4;
}
message CreateSubCoinSellerResponse {
CoinSellerSubRelation relation = 1;
CoinSellerListItem child = 2;
}
message SubmitSubCoinSellerApplicationRequest {
RequestMeta meta = 1;
string command_id = 2;
int64 parent_user_id = 3;
int64 target_user_id = 4;
}
message SubmitSubCoinSellerApplicationResponse {
CoinSellerSubApplication application = 1;
}
message ListSubCoinSellersRequest {
RequestMeta meta = 1;
int64 parent_user_id = 2;
int32 page = 3;
int32 page_size = 4;
}
message ListSubCoinSellersResponse {
repeated CoinSellerListItem children = 1;
int64 total = 2;
int32 page = 3;
int32 page_size = 4;
}
message CheckCoinSellerSubRelationRequest {
RequestMeta meta = 1;
int64 parent_user_id = 2;
int64 child_user_id = 3;
}
message CheckCoinSellerSubRelationResponse {
bool allowed = 1;
string reason = 2;
CoinSellerListItem child = 3;
}
message GetUserRoleSummaryRequest { message GetUserRoleSummaryRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
int64 user_id = 2; int64 user_id = 2;
@ -462,15 +283,6 @@ message GetUserRoleSummaryResponse {
UserRoleSummary summary = 1; UserRoleSummary summary = 1;
} }
message GetAgencyRequest {
RequestMeta meta = 1;
int64 agency_id = 2;
}
message GetAgencyResponse {
Agency agency = 1;
}
message CheckBusinessCapabilityRequest { message CheckBusinessCapabilityRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
int64 actor_user_id = 2; int64 actor_user_id = 2;
@ -482,24 +294,6 @@ message CheckBusinessCapabilityResponse {
string reason = 2; string reason = 2;
} }
// RoleScopePolicy App scope app_code
message RoleScopePolicy {
string scene = 1;
string base_scope = 2;
bool region_expansion_configurable = 3;
// expanded_scope region
string expanded_scope = 4;
}
message GetRoleScopePolicyRequest {
RequestMeta meta = 1;
string scene = 2;
}
message GetRoleScopePolicyResponse {
RoleScopePolicy policy = 1;
}
message GetAgencyMembersRequest { message GetAgencyMembersRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
int64 agency_id = 2; int64 agency_id = 2;
@ -526,6 +320,7 @@ message CreateBDLeaderRequest {
int64 admin_user_id = 3; int64 admin_user_id = 3;
int64 target_user_id = 4; int64 target_user_id = 4;
string reason = 5; string reason = 5;
int64 region_id = 6;
} }
message CreateBDLeaderResponse { message CreateBDLeaderResponse {
@ -552,7 +347,6 @@ message SetBDStatusRequest {
int64 target_user_id = 4; int64 target_user_id = 4;
string status = 5; string status = 5;
string reason = 6; string reason = 6;
string role = 7;
} }
message SetBDStatusResponse { message SetBDStatusResponse {
@ -565,7 +359,6 @@ message CreateCoinSellerRequest {
int64 admin_user_id = 3; int64 admin_user_id = 3;
int64 target_user_id = 4; int64 target_user_id = 4;
string reason = 5; string reason = 5;
optional bool can_manage_sub_coin_sellers = 6;
} }
message CreateCoinSellerResponse { message CreateCoinSellerResponse {
@ -579,28 +372,12 @@ message SetCoinSellerStatusRequest {
int64 target_user_id = 4; int64 target_user_id = 4;
string status = 5; string status = 5;
string reason = 6; string reason = 6;
optional bool can_manage_sub_coin_sellers = 7;
} }
message SetCoinSellerStatusResponse { message SetCoinSellerStatusResponse {
CoinSellerProfile coin_seller_profile = 1; CoinSellerProfile coin_seller_profile = 1;
} }
message ReviewSubCoinSellerApplicationRequest {
RequestMeta meta = 1;
string command_id = 2;
int64 admin_user_id = 3;
int64 application_id = 4;
string decision = 5;
string reason = 6;
}
message ReviewSubCoinSellerApplicationResponse {
CoinSellerSubApplication application = 1;
CoinSellerSubRelation relation = 2;
CoinSellerListItem child = 3;
}
message CreateAgencyRequest { message CreateAgencyRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
string command_id = 2; string command_id = 2;
@ -609,7 +386,6 @@ message CreateAgencyRequest {
int64 parent_bd_user_id = 5; int64 parent_bd_user_id = 5;
string name = 6; string name = 6;
bool join_enabled = 7; bool join_enabled = 7;
// Deprecated: Agency
int32 max_hosts = 8; int32 max_hosts = 8;
string reason = 9; string reason = 9;
} }
@ -620,21 +396,6 @@ message CreateAgencyResponse {
AgencyMembership membership = 3; AgencyMembership membership = 3;
} }
message AdminAddAgencyHostRequest {
RequestMeta meta = 1;
string command_id = 2;
int64 admin_user_id = 3;
int64 agency_id = 4;
int64 target_user_id = 5;
string reason = 6;
}
message AdminAddAgencyHostResponse {
Agency agency = 1;
HostProfile host_profile = 2;
AgencyMembership membership = 3;
}
message CloseAgencyRequest { message CloseAgencyRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
string command_id = 2; string command_id = 2;
@ -647,31 +408,6 @@ message CloseAgencyResponse {
Agency agency = 1; 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 SetAgencyStatusRequest {
RequestMeta meta = 1;
string command_id = 2;
int64 admin_user_id = 3;
int64 agency_id = 4;
string status = 5;
string reason = 6;
}
message SetAgencyStatusResponse {
Agency agency = 1;
}
message SetAgencyJoinEnabledRequest { message SetAgencyJoinEnabledRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
string command_id = 2; string command_id = 2;
@ -685,52 +421,6 @@ message SetAgencyJoinEnabledResponse {
Agency agency = 1; Agency agency = 1;
} }
message UpdateAgencyProfileRequest {
RequestMeta meta = 1;
string command_id = 2;
int64 operator_user_id = 3;
int64 agency_id = 4;
optional string name = 5;
optional string avatar = 6;
}
message UpdateAgencyProfileResponse {
Agency agency = 1;
}
// HostEngagementStats Fami 使/ wallet-service
message HostEngagementStats {
int64 online_duration_ms = 1;
int64 valid_mic_duration_ms = 2;
int64 valid_mic_days = 3;
int64 private_message_senders = 4;
int64 new_followers = 5;
}
message GetHostEngagementStatsRequest {
RequestMeta meta = 1;
int64 host_user_id = 2;
int64 start_at_ms = 3;
int64 end_at_ms = 4;
}
message GetHostEngagementStatsResponse {
HostEngagementStats stats = 1;
}
// RecordPrivateMessageEvent gateway event_id
message RecordPrivateMessageEventRequest {
RequestMeta meta = 1;
string event_id = 2;
int64 sender_user_id = 3;
int64 target_user_id = 4;
int64 occurred_at_ms = 5;
}
message RecordPrivateMessageEventResponse {
bool created = 1;
}
// UserHostService user-service host domain gRPC // UserHostService user-service host domain gRPC
service UserHostService { service UserHostService {
rpc SearchAgencies(SearchAgenciesRequest) returns (SearchAgenciesResponse); rpc SearchAgencies(SearchAgenciesRequest) returns (SearchAgenciesResponse);
@ -739,33 +429,15 @@ service UserHostService {
rpc KickAgencyHost(KickAgencyHostRequest) returns (KickAgencyHostResponse); rpc KickAgencyHost(KickAgencyHostRequest) returns (KickAgencyHostResponse);
rpc InviteAgency(InviteAgencyRequest) returns (InviteAgencyResponse); rpc InviteAgency(InviteAgencyRequest) returns (InviteAgencyResponse);
rpc InviteBD(InviteBDRequest) returns (InviteBDResponse); rpc InviteBD(InviteBDRequest) returns (InviteBDResponse);
rpc InviteHost(InviteHostRequest) returns (InviteHostResponse);
rpc ListBDLeaderBDs(ListBDLeaderBDsRequest) returns (ListBDLeaderBDsResponse);
rpc ListBDLeaderAgencies(ListBDLeaderAgenciesRequest) returns (ListBDLeaderAgenciesResponse);
rpc ListBDAgencies(ListBDAgenciesRequest) returns (ListBDAgenciesResponse);
// ListManagerTeamAgencies Agency
rpc ListManagerTeamAgencies(ListManagerTeamAgenciesRequest) returns (ListManagerTeamAgenciesResponse);
rpc ListRoleInvitations(ListRoleInvitationsRequest) returns (ListRoleInvitationsResponse);
rpc GetRoleInvitation(GetRoleInvitationRequest) returns (GetRoleInvitationResponse);
rpc ProcessRoleInvitation(ProcessRoleInvitationRequest) returns (ProcessRoleInvitationResponse); rpc ProcessRoleInvitation(ProcessRoleInvitationRequest) returns (ProcessRoleInvitationResponse);
rpc GetHostProfile(GetHostProfileRequest) returns (GetHostProfileResponse); rpc GetHostProfile(GetHostProfileRequest) returns (GetHostProfileResponse);
rpc BatchGetHostProfiles(BatchGetHostProfilesRequest) returns (BatchGetHostProfilesResponse);
rpc GetBDProfile(GetBDProfileRequest) returns (GetBDProfileResponse); rpc GetBDProfile(GetBDProfileRequest) returns (GetBDProfileResponse);
rpc GetCoinSellerProfile(GetCoinSellerProfileRequest) returns (GetCoinSellerProfileResponse); rpc GetCoinSellerProfile(GetCoinSellerProfileRequest) returns (GetCoinSellerProfileResponse);
rpc ListActiveCoinSellersInMyRegion(ListActiveCoinSellersInMyRegionRequest) returns (ListActiveCoinSellersInMyRegionResponse); rpc ListActiveCoinSellersInMyRegion(ListActiveCoinSellersInMyRegionRequest) returns (ListActiveCoinSellersInMyRegionResponse);
rpc CreateSubCoinSeller(CreateSubCoinSellerRequest) returns (CreateSubCoinSellerResponse);
rpc SubmitSubCoinSellerApplication(SubmitSubCoinSellerApplicationRequest) returns (SubmitSubCoinSellerApplicationResponse);
rpc ListSubCoinSellers(ListSubCoinSellersRequest) returns (ListSubCoinSellersResponse);
rpc CheckCoinSellerSubRelation(CheckCoinSellerSubRelationRequest) returns (CheckCoinSellerSubRelationResponse);
rpc GetUserRoleSummary(GetUserRoleSummaryRequest) returns (GetUserRoleSummaryResponse); rpc GetUserRoleSummary(GetUserRoleSummaryRequest) returns (GetUserRoleSummaryResponse);
rpc GetAgency(GetAgencyRequest) returns (GetAgencyResponse);
rpc CheckBusinessCapability(CheckBusinessCapabilityRequest) returns (CheckBusinessCapabilityResponse); rpc CheckBusinessCapability(CheckBusinessCapabilityRequest) returns (CheckBusinessCapabilityResponse);
rpc GetRoleScopePolicy(GetRoleScopePolicyRequest) returns (GetRoleScopePolicyResponse);
rpc GetAgencyMembers(GetAgencyMembersRequest) returns (GetAgencyMembersResponse); rpc GetAgencyMembers(GetAgencyMembersRequest) returns (GetAgencyMembersResponse);
rpc GetAgencyApplications(GetAgencyApplicationsRequest) returns (GetAgencyApplicationsResponse); rpc GetAgencyApplications(GetAgencyApplicationsRequest) returns (GetAgencyApplicationsResponse);
rpc UpdateAgencyProfile(UpdateAgencyProfileRequest) returns (UpdateAgencyProfileResponse);
rpc GetHostEngagementStats(GetHostEngagementStatsRequest) returns (GetHostEngagementStatsResponse);
rpc RecordPrivateMessageEvent(RecordPrivateMessageEventRequest) returns (RecordPrivateMessageEventResponse);
} }
// UserHostAdminService admin admin-server // UserHostAdminService admin admin-server
@ -775,11 +447,7 @@ service UserHostAdminService {
rpc SetBDStatus(SetBDStatusRequest) returns (SetBDStatusResponse); rpc SetBDStatus(SetBDStatusRequest) returns (SetBDStatusResponse);
rpc CreateCoinSeller(CreateCoinSellerRequest) returns (CreateCoinSellerResponse); rpc CreateCoinSeller(CreateCoinSellerRequest) returns (CreateCoinSellerResponse);
rpc SetCoinSellerStatus(SetCoinSellerStatusRequest) returns (SetCoinSellerStatusResponse); rpc SetCoinSellerStatus(SetCoinSellerStatusRequest) returns (SetCoinSellerStatusResponse);
rpc ReviewSubCoinSellerApplication(ReviewSubCoinSellerApplicationRequest) returns (ReviewSubCoinSellerApplicationResponse);
rpc CreateAgency(CreateAgencyRequest) returns (CreateAgencyResponse); rpc CreateAgency(CreateAgencyRequest) returns (CreateAgencyResponse);
rpc AdminAddAgencyHost(AdminAddAgencyHostRequest) returns (AdminAddAgencyHostResponse);
rpc CloseAgency(CloseAgencyRequest) returns (CloseAgencyResponse); rpc CloseAgency(CloseAgencyRequest) returns (CloseAgencyResponse);
rpc DeleteAgency(DeleteAgencyRequest) returns (DeleteAgencyResponse);
rpc SetAgencyStatus(SetAgencyStatusRequest) returns (SetAgencyStatusResponse);
rpc SetAgencyJoinEnabled(SetAgencyJoinEnabledRequest) returns (SetAgencyJoinEnabledResponse); rpc SetAgencyJoinEnabled(SetAgencyJoinEnabledRequest) returns (SetAgencyJoinEnabledResponse);
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -19,16 +19,6 @@ message RequestMeta {
string platform = 11; string platform = 11;
string language = 12; string language = 12;
string timezone = 13; string timezone = 13;
// app_version/build_number RPC login_audit
string app_version = 14;
string build_number = 15;
// device_model/device_manufacturer/os_version/imei gateway
// X-Device-Model/X-Device-Manufacturer/X-OS-Version/X-Device-IMEI device_id
// user_devices tabIMEI Android 10+
string device_model = 16;
string device_manufacturer = 17;
string os_version = 18;
string imei = 19;
} }
// App user-service App // App user-service App
@ -93,11 +83,6 @@ message User {
bool country_enabled = 26; bool country_enabled = 26;
string app_code = 27; string app_code = 27;
InviteOverview invite = 28; InviteOverview invite = 28;
string pretty_id = 29;
string pretty_display_user_id = 30;
string profile_bg_img = 31;
string contact_info = 32;
string withdraw_usdt_trc20_address = 33;
} }
// InviteOverview read model // InviteOverview read model
@ -116,29 +101,6 @@ message InviteBinding {
int64 inviter_user_id = 3; int64 inviter_user_id = 3;
} }
// InviteAttribution
message InviteAttribution {
bool found = 1;
string app_code = 2;
int64 invited_user_id = 3;
int64 inviter_user_id = 4;
string invite_code = 5;
int64 bound_at_ms = 6;
string validity_status = 7;
int64 accumulated_recharge_coin_amount = 8;
int64 required_recharge_coin_amount = 9;
int64 valid_at_ms = 10;
}
message GetInviteAttributionRequest {
RequestMeta meta = 1;
int64 invited_user_id = 2;
}
message GetInviteAttributionResponse {
InviteAttribution attribution = 1;
}
// UserMicLifetimeStats / // UserMicLifetimeStats /
message UserMicLifetimeStats { message UserMicLifetimeStats {
int64 user_id = 1; int64 user_id = 1;
@ -170,7 +132,6 @@ message CronBatchRequest {
int64 lock_ttl_ms = 5; int64 lock_ttl_ms = 5;
int64 pending_publish_max_age_ms = 6; int64 pending_publish_max_age_ms = 6;
int64 publishing_session_max_age_ms = 7; int64 publishing_session_max_age_ms = 7;
int64 open_session_max_age_ms = 8;
} }
// CronBatchResponse run // CronBatchResponse run
@ -399,269 +360,6 @@ message ListFriendApplicationsResponse {
int64 total = 2; int64 total = 2;
} }
// CPUserProfile CP
message CPUserProfile {
int64 user_id = 1;
string display_user_id = 2;
string username = 3;
string avatar = 4;
}
// CPGiftSnapshot
message CPGiftSnapshot {
string gift_id = 1;
string gift_name = 2;
string gift_icon_url = 3;
string gift_animation_url = 4;
int32 gift_count = 5;
int64 gift_value = 6;
string billing_receipt_id = 7;
}
// CPApplication A B CP//
message CPApplication {
string application_id = 1;
string relation_type = 2;
string status = 3;
CPUserProfile requester = 4;
CPUserProfile target = 5;
string room_id = 6;
int64 room_region_id = 7;
CPGiftSnapshot gift = 8;
int64 created_at_ms = 9;
int64 updated_at_ms = 10;
int64 expires_at_ms = 11;
int64 decided_at_ms = 12;
}
// CPRelationship active
message CPRelationship {
string relationship_id = 1;
string relation_type = 2;
string status = 3;
CPUserProfile me = 4;
CPUserProfile partner = 5;
int64 intimacy_value = 6;
int32 level = 7;
reserved 8;
reserved "equipped";
int64 formed_at_ms = 9;
int64 updated_at_ms = 10;
int64 current_level_threshold = 11;
int64 next_level_threshold = 12;
int64 needed_for_next_level = 13;
int32 level_progress_percent = 14;
bool max_level = 15;
// breakup_cost_coins active ended
int64 breakup_cost_coins = 16;
int64 ended_at_ms = 17;
}
// CPAvatarFrameSnapshot
message CPAvatarFrameSnapshot {
int64 resource_id = 1;
string resource_code = 2;
string name = 3;
string asset_url = 4;
string preview_url = 5;
string animation_url = 6;
string metadata_json = 7;
}
// CPIntimacyLeaderboardUser CP
message CPIntimacyLeaderboardUser {
int64 user_id = 1;
string display_user_id = 2;
string username = 3;
string avatar = 4;
CPAvatarFrameSnapshot avatar_frame = 5;
}
// CPIntimacyLeaderboardItem active
message CPIntimacyLeaderboardItem {
int64 rank = 1;
string relationship_id = 2;
string relation_type = 3;
int64 intimacy_value = 4;
int32 level = 5;
CPIntimacyLeaderboardUser user_a = 6;
CPIntimacyLeaderboardUser user_b = 7;
int64 formed_at_ms = 8;
int64 updated_at_ms = 9;
}
// CPWeeklyRankEntry activity-service CP
message CPWeeklyRankEntry {
int64 rank = 1;
string relationship_id = 2;
string relation_type = 3;
int64 score = 4;
CPIntimacyLeaderboardUser user_a = 5;
CPIntimacyLeaderboardUser user_b = 6;
int64 formed_at_ms = 7;
int64 updated_at_ms = 8;
int64 first_scored_at_ms = 9;
int64 last_scored_at_ms = 10;
}
message ListCPWeeklyRankEntriesRequest {
RequestMeta meta = 1;
string relation_type = 2;
int64 start_ms = 3;
int64 end_ms = 4;
int32 limit = 5;
}
message ListCPWeeklyRankEntriesResponse {
repeated CPWeeklyRankEntry entries = 1;
int64 server_time_ms = 2;
}
message ListCPApplicationsRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string direction = 3;
string status = 4;
int32 page = 5;
int32 page_size = 6;
}
message ListCPApplicationsResponse {
repeated CPApplication applications = 1;
int64 total = 2;
int64 server_time_ms = 3;
}
message AcceptCPApplicationRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string application_id = 3;
}
message AcceptCPApplicationResponse {
CPApplication application = 1;
CPRelationship relationship = 2;
}
message RejectCPApplicationRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string application_id = 3;
string reason = 4;
}
message RejectCPApplicationResponse {
CPApplication application = 1;
}
message ListCPRelationshipsRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string relation_type = 3;
int32 page = 4;
int32 page_size = 5;
}
message ListCPRelationshipsResponse {
repeated CPRelationship relationships = 1;
int64 total = 2;
reserved 3;
reserved "max_count_by_relation_type";
}
message ListCPIntimacyLeaderboardRequest {
RequestMeta meta = 1;
string relation_type = 2;
int32 page = 3;
int32 page_size = 4;
}
message ListCPIntimacyLeaderboardResponse {
repeated CPIntimacyLeaderboardItem items = 1;
int64 total = 2;
int32 page = 3;
int32 page_size = 4;
int64 server_time_ms = 5;
}
message PrepareBreakCPRelationshipRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string relationship_id = 3;
string command_id = 4;
}
message PrepareBreakCPRelationshipResponse {
CPRelationship relationship = 1;
string command_id = 2;
string status = 3;
int64 breakup_cost_coins = 4;
string wallet_transaction_id = 5;
int64 coin_balance_after = 6;
}
message ConfirmBreakCPRelationshipRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string relationship_id = 3;
string command_id = 4;
string wallet_transaction_id = 5;
int64 paid_coin_amount = 6;
int64 coin_balance_after = 7;
}
message ConfirmBreakCPRelationshipResponse {
CPRelationship relationship = 1;
int64 breakup_cost_coins = 2;
string wallet_transaction_id = 3;
int64 coin_balance_after = 4;
}
message CancelBreakCPRelationshipRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string relationship_id = 3;
string command_id = 4;
string reason = 5;
}
message CancelBreakCPRelationshipResponse {
string command_id = 1;
string status = 2;
}
message RoomGiftCPEvent {
RequestMeta meta = 1;
string event_id = 2;
string room_id = 3;
int64 room_version = 4;
int64 occurred_at_ms = 5;
int64 sender_user_id = 6;
int64 target_user_id = 7;
string gift_id = 8;
int32 gift_count = 9;
int64 gift_value = 10;
string billing_receipt_id = 11;
int64 visible_region_id = 12;
string command_id = 13;
string gift_type_code = 14;
string cp_relation_type = 15;
string gift_name = 16;
string gift_icon_url = 17;
string gift_animation_url = 18;
}
message ConsumeRoomGiftCPEventRequest {
RequestMeta meta = 1;
RoomGiftCPEvent event = 2;
}
message ConsumeRoomGiftCPEventResponse {
bool consumed = 1;
CPApplication application = 2;
CPRelationship relationship = 3;
}
// UserReport App // UserReport App
message UserReport { message UserReport {
string report_id = 1; string report_id = 1;
@ -702,85 +400,6 @@ message BatchGetUsersResponse {
map<int64, User> users = 1; map<int64, User> users = 1;
} }
// ActiveUserBanSummary
// expires_at_ms=0 permanent=true source adminmanager legacy
message ActiveUserBanSummary {
bool active = 1;
string source = 2;
string ban_id = 3;
bool permanent = 4;
int64 expires_at_ms = 5;
UserStatus user_status = 6;
string operator_type = 7;
int64 operator_user_id = 8;
string reason = 9;
int64 created_at_ms = 10;
int32 active_ban_count = 11;
}
// UserAdminProfile user-service owner
// roles hostagencybdbd_leadercoin_sellermanager
message UserAdminProfile {
User user = 1;
string birth = 2;
string register_device = 3;
int64 last_success_login_at_ms = 4;
repeated string roles = 5;
ActiveUserBanSummary ban = 6;
string last_operator_type = 7;
int64 last_operator_user_id = 8;
string last_operation_reason = 9;
int64 last_operation_at_ms = 10;
}
message BatchGetUserAdminProfilesRequest {
RequestMeta meta = 1;
repeated int64 user_ids = 2;
}
message BatchGetUserAdminProfilesResponse {
map<int64, UserAdminProfile> profiles = 1;
}
// AdminIssueUserAccessTokenRequest active session access token
// login_audit session token /
// admin
message AdminIssueUserAccessTokenRequest {
RequestMeta meta = 1;
string app_code = 2;
int64 user_id = 3;
}
// AdminIssueUserAccessTokenResponse access token session
// refresh tokenrefresh
message AdminIssueUserAccessTokenResponse {
string access_token = 1;
string token_type = 2;
int64 expires_in_sec = 3;
string session_id = 4;
string device_id = 5;
int64 session_last_heartbeat_at_ms = 6;
}
// RoomBasicUser
message RoomBasicUser {
int64 user_id = 1;
string username = 2;
string avatar = 3;
string display_user_id = 4;
}
// BatchGetRoomBasicUsersRequest lite
message BatchGetRoomBasicUsersRequest {
RequestMeta meta = 1;
repeated int64 user_ids = 2;
}
// BatchGetRoomBasicUsersResponse user_id
message BatchGetRoomBasicUsersResponse {
map<int64, RoomBasicUser> users = 1;
}
// ListUserIDsRequest fanout/ user_id // ListUserIDsRequest fanout/ user_id
message ListUserIDsRequest { message ListUserIDsRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
@ -813,58 +432,13 @@ message UpdateUserProfileResponse {
User user = 1; User user = 1;
} }
// UpdateUserProfileBackgroundRequest // ChangeUserCountryRequest
message UpdateUserProfileBackgroundRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string profile_bg_img = 3;
}
// UpdateUserProfileBackgroundResponse
message UpdateUserProfileBackgroundResponse {
User user = 1;
}
// UpdateUserContactInfoRequest
message UpdateUserContactInfoRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string contact_info = 3;
}
// UpdateUserContactInfoResponse
message UpdateUserContactInfoResponse {
User user = 1;
}
// UpdateUserWithdrawAddressRequest wallet-service
message UpdateUserWithdrawAddressRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string usdt_trc20_address = 3;
}
// UpdateUserWithdrawAddressResponse
message UpdateUserWithdrawAddressResponse {
User user = 1;
}
// ChangeUserCountryRequest App
message ChangeUserCountryRequest { message ChangeUserCountryRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
int64 user_id = 2; int64 user_id = 2;
string country = 3; 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 // ChangeUserCountryResponse
message ChangeUserCountryResponse { message ChangeUserCountryResponse {
User user = 1; User user = 1;
@ -902,108 +476,6 @@ message SetUserStatusResponse {
string access_token_revoke_error = 11; string access_token_revoke_error = 11;
} }
// AdminUserBan expires_at_ms=0
message AdminUserBan {
string ban_id = 1;
string command_id = 2;
int64 target_user_id = 3;
int64 expires_at_ms = 4;
bool permanent = 5;
string status = 6;
string reason = 7;
int64 operator_admin_id = 8;
int64 created_at_ms = 9;
int64 updated_at_ms = 10;
int64 released_by_admin_id = 11;
string released_reason = 12;
int64 released_at_ms = 13;
}
message AdminBanUserRequest {
RequestMeta meta = 1;
string command_id = 2;
int64 target_user_id = 3;
int64 expires_at_ms = 4;
int64 operator_admin_id = 5;
string reason = 6;
}
message AdminBanUserResponse {
AdminUserBan ban = 1;
SetUserStatusResponse status = 2;
}
// AdminUnbanUserRequest legacy
message AdminUnbanUserRequest {
RequestMeta meta = 1;
// ban_id target_user_id
string ban_id = 2;
int64 target_user_id = 3;
int64 operator_admin_id = 4;
string reason = 5;
}
message AdminUnbanUserResponse {
// ban legacy
AdminUserBan ban = 1;
SetUserStatusResponse status = 2;
int64 released_admin_ban_count = 3;
int64 released_manager_block_count = 4;
bool status_restored = 5;
}
message ManagerUserBlock {
string block_id = 1;
int64 manager_user_id = 2;
int64 target_user_id = 3;
string target_display_user_id = 4;
string target_username = 5;
string target_avatar = 6;
string country = 7;
int64 blocked_until_ms = 8;
string status = 9;
string reason = 10;
int64 created_at_ms = 11;
int64 updated_at_ms = 12;
}
message CreateManagerUserBlockRequest {
RequestMeta meta = 1;
string command_id = 2;
int64 manager_user_id = 3;
int64 target_user_id = 4;
int64 blocked_until_ms = 5;
string reason = 6;
}
message CreateManagerUserBlockResponse {
ManagerUserBlock block = 1;
SetUserStatusResponse status = 2;
}
message ListManagerUserBlocksRequest {
RequestMeta meta = 1;
int64 manager_user_id = 2;
string status = 3;
int32 page_size = 4;
}
message ListManagerUserBlocksResponse {
repeated ManagerUserBlock blocks = 1;
}
message UnblockManagerUserRequest {
RequestMeta meta = 1;
int64 manager_user_id = 2;
string block_id = 3;
string reason = 4;
}
message UnblockManagerUserResponse {
ManagerUserBlock block = 1;
SetUserStatusResponse status = 2;
}
// CompleteOnboardingRequest // CompleteOnboardingRequest
message CompleteOnboardingRequest { message CompleteOnboardingRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
@ -1025,29 +497,6 @@ message CompleteOnboardingResponse {
InviteBinding invite = 6; InviteBinding invite = 6;
} }
// SearchInviteReferrerRequest
message SearchInviteReferrerRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string invite_code = 3;
}
message SearchInviteReferrerResponse {
User inviter = 1;
}
// BindInviteReferrerRequest user-service
message BindInviteReferrerRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string invite_code = 3;
}
message BindInviteReferrerResponse {
InviteBinding invite = 1;
User inviter = 2;
}
// BindPushTokenRequest token // BindPushTokenRequest token
// push token 线 // push token 线
message BindPushTokenRequest { message BindPushTokenRequest {
@ -1205,8 +654,6 @@ message UserIdentity {
string display_user_id_kind = 5; string display_user_id_kind = 5;
int64 display_user_id_expires_at_ms = 6; int64 display_user_id_expires_at_ms = 6;
string app_code = 7; string app_code = 7;
string pretty_id = 8;
string pretty_display_user_id = 9;
} }
// GetUserIdentityRequest user_id // GetUserIdentityRequest user_id
@ -1272,218 +719,18 @@ message ExpirePrettyDisplayUserIDResponse {
UserIdentity identity = 1; UserIdentity identity = 1;
} }
// PrettyDisplayIDPool
message PrettyDisplayIDPool {
string app_code = 1;
string pool_id = 2;
string name = 3;
string level_track = 4;
int32 min_level = 5;
int32 max_level = 6;
string rule_type = 7;
string rule_config_json = 8;
string status = 9;
int32 sort_order = 10;
int64 created_by_admin_id = 11;
int64 updated_by_admin_id = 12;
int64 created_at_ms = 13;
int64 updated_at_ms = 14;
}
// PrettyDisplayID
message PrettyDisplayID {
string app_code = 1;
string pretty_id = 2;
string pool_id = 3;
string source = 4;
string display_user_id = 5;
string status = 6;
int64 assigned_user_id = 7;
string assigned_lease_id = 8;
int64 assigned_at_ms = 9;
int64 released_at_ms = 10;
string release_reason = 11;
string generated_batch_id = 12;
int64 created_by_admin_id = 13;
int64 created_at_ms = 14;
int64 updated_at_ms = 15;
PrettyDisplayIDPool pool = 16;
}
message PrettyDisplayIDGenerationBatch {
string app_code = 1;
string batch_id = 2;
string pool_id = 3;
string rule_type = 4;
string rule_config_json = 5;
int32 requested_count = 6;
int32 generated_count = 7;
int32 skipped_conflict_count = 8;
string status = 9;
int64 operator_admin_id = 10;
string request_id = 11;
int64 created_at_ms = 12;
int64 updated_at_ms = 13;
}
message ListAvailablePrettyDisplayIDsRequest {
RequestMeta meta = 1;
int64 user_id = 2;
int32 page = 3;
int32 page_size = 4;
}
message ListAvailablePrettyDisplayIDsResponse {
repeated PrettyDisplayID items = 1;
int64 total = 2;
}
message ApplyPrettyDisplayIDFromPoolRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string pretty_id = 3;
}
message ApplyPrettyDisplayIDFromPoolResponse {
UserIdentity identity = 1;
string pretty_id = 2;
string lease_id = 3;
}
message ListPrettyDisplayIDPoolsRequest {
RequestMeta meta = 1;
string status = 2;
string level_track = 3;
int32 page = 4;
int32 page_size = 5;
}
message ListPrettyDisplayIDPoolsResponse {
repeated PrettyDisplayIDPool items = 1;
int64 total = 2;
}
message CreatePrettyDisplayIDPoolRequest {
RequestMeta meta = 1;
string name = 2;
string level_track = 3;
int32 min_level = 4;
int32 max_level = 5;
string rule_type = 6;
string rule_config_json = 7;
string status = 8;
int32 sort_order = 9;
int64 operator_admin_id = 10;
}
message UpdatePrettyDisplayIDPoolRequest {
RequestMeta meta = 1;
string pool_id = 2;
string name = 3;
string level_track = 4;
int32 min_level = 5;
int32 max_level = 6;
string rule_type = 7;
string rule_config_json = 8;
string status = 9;
int32 sort_order = 10;
int64 operator_admin_id = 11;
}
message PrettyDisplayIDPoolResponse {
PrettyDisplayIDPool pool = 1;
}
message GeneratePrettyDisplayIDsRequest {
RequestMeta meta = 1;
string pool_id = 2;
string rule_type = 3;
string rule_config_json = 4;
int32 count = 5;
int64 operator_admin_id = 6;
}
message GeneratePrettyDisplayIDsResponse {
PrettyDisplayIDGenerationBatch batch = 1;
}
message ListPrettyDisplayIDsRequest {
RequestMeta meta = 1;
string pool_id = 2;
string source = 3;
string status = 4;
string keyword = 5;
int64 assigned_user_id = 6;
int32 page = 7;
int32 page_size = 8;
}
message ListPrettyDisplayIDsResponse {
repeated PrettyDisplayID items = 1;
int64 total = 2;
}
message SetPrettyDisplayIDStatusRequest {
RequestMeta meta = 1;
string pretty_id = 2;
string status = 3;
int64 operator_admin_id = 4;
string reason = 5;
}
message RecyclePrettyDisplayIDRequest {
RequestMeta meta = 1;
string pretty_id = 2;
string reason = 3;
int64 operator_admin_id = 4;
}
message PrettyDisplayIDResponse {
PrettyDisplayID item = 1;
}
message AdminGrantPrettyDisplayIDRequest {
RequestMeta meta = 1;
int64 target_user_id = 2;
string display_user_id = 3;
int64 duration_ms = 4;
string reason = 5;
int64 operator_admin_id = 6;
}
message AdminGrantPrettyDisplayIDResponse {
UserIdentity identity = 1;
string pretty_id = 2;
string lease_id = 3;
}
// UserService // UserService
service UserService { service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse); rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc GetInviteAttribution(GetInviteAttributionRequest) returns (GetInviteAttributionResponse);
rpc BusinessUserLookup(BusinessUserLookupRequest) returns (BusinessUserLookupResponse); rpc BusinessUserLookup(BusinessUserLookupRequest) returns (BusinessUserLookupResponse);
rpc GetMyProfileStats(GetMyProfileStatsRequest) returns (GetMyProfileStatsResponse); rpc GetMyProfileStats(GetMyProfileStatsRequest) returns (GetMyProfileStatsResponse);
rpc BatchGetUsers(BatchGetUsersRequest) returns (BatchGetUsersResponse); rpc BatchGetUsers(BatchGetUsersRequest) returns (BatchGetUsersResponse);
rpc BatchGetUserAdminProfiles(BatchGetUserAdminProfilesRequest) returns (BatchGetUserAdminProfilesResponse);
rpc AdminIssueUserAccessToken(AdminIssueUserAccessTokenRequest) returns (AdminIssueUserAccessTokenResponse);
rpc BatchGetRoomBasicUsers(BatchGetRoomBasicUsersRequest) returns (BatchGetRoomBasicUsersResponse);
rpc ListUserIDs(ListUserIDsRequest) returns (ListUserIDsResponse); rpc ListUserIDs(ListUserIDsRequest) returns (ListUserIDsResponse);
rpc GetUserMicLifetimeStats(GetUserMicLifetimeStatsRequest) returns (GetUserMicLifetimeStatsResponse); rpc GetUserMicLifetimeStats(GetUserMicLifetimeStatsRequest) returns (GetUserMicLifetimeStatsResponse);
rpc UpdateUserProfile(UpdateUserProfileRequest) returns (UpdateUserProfileResponse); rpc UpdateUserProfile(UpdateUserProfileRequest) returns (UpdateUserProfileResponse);
rpc UpdateUserProfileBackground(UpdateUserProfileBackgroundRequest) returns (UpdateUserProfileBackgroundResponse);
rpc UpdateUserContactInfo(UpdateUserContactInfoRequest) returns (UpdateUserContactInfoResponse);
rpc UpdateUserWithdrawAddress(UpdateUserWithdrawAddressRequest) returns (UpdateUserWithdrawAddressResponse);
rpc ChangeUserCountry(ChangeUserCountryRequest) returns (ChangeUserCountryResponse); rpc ChangeUserCountry(ChangeUserCountryRequest) returns (ChangeUserCountryResponse);
rpc AdminChangeUserCountry(AdminChangeUserCountryRequest) returns (ChangeUserCountryResponse);
rpc SetUserStatus(SetUserStatusRequest) returns (SetUserStatusResponse); rpc SetUserStatus(SetUserStatusRequest) returns (SetUserStatusResponse);
rpc AdminBanUser(AdminBanUserRequest) returns (AdminBanUserResponse);
rpc AdminUnbanUser(AdminUnbanUserRequest) returns (AdminUnbanUserResponse);
rpc CreateManagerUserBlock(CreateManagerUserBlockRequest) returns (CreateManagerUserBlockResponse);
rpc ListManagerUserBlocks(ListManagerUserBlocksRequest) returns (ListManagerUserBlocksResponse);
rpc UnblockManagerUser(UnblockManagerUserRequest) returns (UnblockManagerUserResponse);
rpc CompleteOnboarding(CompleteOnboardingRequest) returns (CompleteOnboardingResponse); rpc CompleteOnboarding(CompleteOnboardingRequest) returns (CompleteOnboardingResponse);
rpc SearchInviteReferrer(SearchInviteReferrerRequest) returns (SearchInviteReferrerResponse);
rpc BindInviteReferrer(BindInviteReferrerRequest) returns (BindInviteReferrerResponse);
} }
// UserSocialService 访 // UserSocialService 访
@ -1501,33 +748,11 @@ service UserSocialService {
rpc SubmitReport(SubmitReportRequest) returns (SubmitReportResponse); rpc SubmitReport(SubmitReportRequest) returns (SubmitReportResponse);
} }
// UserCPService App CP//
service UserCPService {
rpc ListCPApplications(ListCPApplicationsRequest) returns (ListCPApplicationsResponse);
rpc AcceptCPApplication(AcceptCPApplicationRequest) returns (AcceptCPApplicationResponse);
rpc RejectCPApplication(RejectCPApplicationRequest) returns (RejectCPApplicationResponse);
rpc ListCPRelationships(ListCPRelationshipsRequest) returns (ListCPRelationshipsResponse);
rpc ListCPIntimacyLeaderboard(ListCPIntimacyLeaderboardRequest) returns (ListCPIntimacyLeaderboardResponse);
rpc PrepareBreakCPRelationship(PrepareBreakCPRelationshipRequest) returns (PrepareBreakCPRelationshipResponse);
rpc ConfirmBreakCPRelationship(ConfirmBreakCPRelationshipRequest) returns (ConfirmBreakCPRelationshipResponse);
rpc CancelBreakCPRelationship(CancelBreakCPRelationshipRequest) returns (CancelBreakCPRelationshipResponse);
}
// UserCPInternalService owner outbox worker App
service UserCPInternalService {
rpc ConsumeRoomGiftCPEvent(ConsumeRoomGiftCPEventRequest) returns (ConsumeRoomGiftCPEventResponse);
rpc ListCPWeeklyRankEntries(ListCPWeeklyRankEntriesRequest) returns (ListCPWeeklyRankEntriesResponse);
}
// UserCronService cron-service user-service owner // UserCronService cron-service user-service owner
service UserCronService { service UserCronService {
rpc ProcessLoginIPRiskBatch(CronBatchRequest) returns (CronBatchResponse); rpc ProcessLoginIPRiskBatch(CronBatchRequest) returns (CronBatchResponse);
rpc ProcessRegionRebuildBatch(CronBatchRequest) returns (CronBatchResponse); rpc ProcessRegionRebuildBatch(CronBatchRequest) returns (CronBatchResponse);
rpc CompensateMicOpenSessions(CronBatchRequest) returns (CronBatchResponse); rpc CompensateMicOpenSessions(CronBatchRequest) returns (CronBatchResponse);
rpc CompensateRoomOpenSessions(CronBatchRequest) returns (CronBatchResponse);
rpc ExpireManagerUserBlocks(CronBatchRequest) returns (CronBatchResponse);
rpc ExpireAdminUserBans(CronBatchRequest) returns (CronBatchResponse);
rpc RefreshCPIntimacyLeaderboard(CronBatchRequest) returns (CronBatchResponse);
} }
// UserDeviceService App token // UserDeviceService App token
@ -1569,19 +794,5 @@ service UserIdentityService {
rpc ResolveDisplayUserID(ResolveDisplayUserIDRequest) returns (ResolveDisplayUserIDResponse); rpc ResolveDisplayUserID(ResolveDisplayUserIDRequest) returns (ResolveDisplayUserIDResponse);
rpc ChangeDisplayUserID(ChangeDisplayUserIDRequest) returns (ChangeDisplayUserIDResponse); rpc ChangeDisplayUserID(ChangeDisplayUserIDRequest) returns (ChangeDisplayUserIDResponse);
rpc ApplyPrettyDisplayUserID(ApplyPrettyDisplayUserIDRequest) returns (ApplyPrettyDisplayUserIDResponse); rpc ApplyPrettyDisplayUserID(ApplyPrettyDisplayUserIDRequest) returns (ApplyPrettyDisplayUserIDResponse);
rpc ListAvailablePrettyDisplayIDs(ListAvailablePrettyDisplayIDsRequest) returns (ListAvailablePrettyDisplayIDsResponse);
rpc ApplyPrettyDisplayIDFromPool(ApplyPrettyDisplayIDFromPoolRequest) returns (ApplyPrettyDisplayIDFromPoolResponse);
rpc ExpirePrettyDisplayUserID(ExpirePrettyDisplayUserIDRequest) returns (ExpirePrettyDisplayUserIDResponse); rpc ExpirePrettyDisplayUserID(ExpirePrettyDisplayUserIDRequest) returns (ExpirePrettyDisplayUserIDResponse);
} }
// UserPrettyDisplayIDAdminService
service UserPrettyDisplayIDAdminService {
rpc ListPrettyDisplayIDPools(ListPrettyDisplayIDPoolsRequest) returns (ListPrettyDisplayIDPoolsResponse);
rpc CreatePrettyDisplayIDPool(CreatePrettyDisplayIDPoolRequest) returns (PrettyDisplayIDPoolResponse);
rpc UpdatePrettyDisplayIDPool(UpdatePrettyDisplayIDPoolRequest) returns (PrettyDisplayIDPoolResponse);
rpc GeneratePrettyDisplayIDs(GeneratePrettyDisplayIDsRequest) returns (GeneratePrettyDisplayIDsResponse);
rpc ListPrettyDisplayIDs(ListPrettyDisplayIDsRequest) returns (ListPrettyDisplayIDsResponse);
rpc SetPrettyDisplayIDStatus(SetPrettyDisplayIDStatusRequest) returns (PrettyDisplayIDResponse);
rpc RecyclePrettyDisplayID(RecyclePrettyDisplayIDRequest) returns (PrettyDisplayIDResponse);
rpc AdminGrantPrettyDisplayID(AdminGrantPrettyDisplayIDRequest) returns (AdminGrantPrettyDisplayIDResponse);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -83,21 +83,12 @@ func allServices() []serviceSpec {
appService("user-service", []int{13005, 13105}, "36"), appService("user-service", []int{13005, 13105}, "36"),
appService("activity-service", []int{13006, 13106}, "35"), appService("activity-service", []int{13006, 13106}, "35"),
appService("wallet-service", []int{13004, 13104}, "33"), appService("wallet-service", []int{13004, 13104}, "33"),
appService("lucky-gift-service", []int{13013, 13113}, "31"),
appService("robot-service", []int{13011, 13111}, "93"),
appService("game-service", []int{13008, 13108}, "32"), appService("game-service", []int{13008, 13108}, "32"),
appService("room-service", []int{13001, 13101}, "34"), appService("room-service", []int{13001, 13101}, "34"),
appService("notice-service", []int{13009, 13109}, "96"), appService("notice-service", []int{13009, 13109}, "96"),
appService("cron-service", []int{13007, 13107}, "95"), appService("cron-service", []int{13007, 13107}, "95"),
appService("gateway-service", []int{13000}, "92"), appService("gateway-service", []int{13000}, "92"),
appService("statistics-service", []int{13010, 13110}, "94"), appService("statistics-service", []int{13010, 13110}, "94"),
{
Name: "luck-gateway",
Dir: "server/luck-gateway",
Args: []string{"run", "./cmd/server"},
Ports: []int{13014},
Color: "36",
},
{ {
Name: "admin", Name: "admin",
Dir: "server/admin", Dir: "server/admin",
@ -109,24 +100,15 @@ func allServices() []serviceSpec {
} }
func appService(name string, ports []int, color string) serviceSpec { func appService(name string, ports []int, color string) serviceSpec {
args := []string{"run", "./services/" + name + "/cmd/server", "-config", ""}
if devRunMQEnabled() {
args = []string{"run", "./services/" + name + "/cmd/server", "-config", "services/" + name + "/configs/config.yaml"}
}
return serviceSpec{ return serviceSpec{
Name: name, Name: name,
Dir: ".", Dir: ".",
Args: args, Args: []string{"run", "./services/" + name + "/cmd/server", "-config", "services/" + name + "/configs/config.yaml"},
Ports: ports, Ports: ports,
Color: color, Color: color,
} }
} }
func devRunMQEnabled() bool {
value := strings.ToLower(strings.TrimSpace(os.Getenv("DEV_RUN_MQ")))
return value == "1" || value == "true" || value == "yes"
}
func selectServices(raw string) ([]serviceSpec, error) { func selectServices(raw string) ([]serviceSpec, error) {
all := allServices() all := allServices()
if strings.TrimSpace(raw) == "" || strings.TrimSpace(raw) == "all" { if strings.TrimSpace(raw) == "" || strings.TrimSpace(raw) == "all" {
@ -147,16 +129,8 @@ func selectServices(raw string) ([]serviceSpec, error) {
"us": "user-service", "us": "user-service",
"activity": "activity-service", "activity": "activity-service",
"as": "activity-service", "as": "activity-service",
"lucky": "lucky-gift-service",
"lucky-gift": "lucky-gift-service",
"lgs": "lucky-gift-service",
"luckygift": "lucky-gift-service",
"lg": "luck-gateway",
"cron": "cron-service", "cron": "cron-service",
"cs": "cron-service", "cs": "cron-service",
"robot": "robot-service",
"robots": "robot-service",
"rb": "robot-service",
"game": "game-service", "game": "game-service",
"games": "game-service", "games": "game-service",
"notice": "notice-service", "notice": "notice-service",

View File

@ -24,10 +24,9 @@ services:
command: ["sh", "mqbroker", "-n", "rocketmq-namesrv:9876", "-c", "/home/rocketmq/broker.conf"] command: ["sh", "mqbroker", "-n", "rocketmq-namesrv:9876", "-c", "/home/rocketmq/broker.conf"]
ports: ports:
- "19009:10909" - "19009:10909"
- "10911:10911"
- "19011:10911" - "19011:10911"
volumes: volumes:
- ${ROCKETMQ_BROKER_CONFIG:-./deploy/rocketmq/broker.local.conf}:/home/rocketmq/broker.conf:ro - ${DEPLOY_PLATFORM_ROOT:-../deploy-platform}/deploy/rocketmq/broker.conf:/home/rocketmq/broker.conf:ro
- rocketmq-store:/home/rocketmq/store - rocketmq-store:/home/rocketmq/store
depends_on: depends_on:
- rocketmq-namesrv - rocketmq-namesrv
@ -49,8 +48,6 @@ services:
condition: service_healthy condition: service_healthy
game-service: game-service:
condition: service_healthy condition: service_healthy
lucky-gift-service:
condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
rocketmq-broker: rocketmq-broker:
@ -82,8 +79,6 @@ services:
condition: service_healthy condition: service_healthy
rocketmq-broker: rocketmq-broker:
condition: service_started condition: service_started
lucky-gift-service:
condition: service_healthy
healthcheck: healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:13101/healthz/ready >/dev/null"] test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:13101/healthz/ready >/dev/null"]
interval: 5s interval: 5s
@ -155,8 +150,6 @@ services:
depends_on: depends_on:
mysql: mysql:
condition: service_healthy condition: service_healthy
redis:
condition: service_healthy
rocketmq-broker: rocketmq-broker:
condition: service_started condition: service_started
user-service: user-service:
@ -168,53 +161,6 @@ services:
retries: 20 retries: 20
start_period: 10s start_period: 10s
lucky-gift-service:
build:
context: .
dockerfile: services/lucky-gift-service/Dockerfile
args: *go-build-args
container_name: lucky-gift-service
environment:
TZ: UTC
ports:
- "13013:13013"
- "13113:13113"
depends_on:
mysql:
condition: service_healthy
wallet-service:
condition: service_healthy
rocketmq-broker:
condition: service_started
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:13113/healthz/ready >/dev/null"]
interval: 5s
timeout: 3s
retries: 20
start_period: 10s
luck-gateway:
build:
context: .
dockerfile: server/luck-gateway/Dockerfile
args: *go-build-args
container_name: luck-gateway
environment:
TZ: UTC
LUCKY_GIFT_GRPC_ADDR: "lucky-gift-service:13013"
LUCK_GATEWAY_ALLOWED_APPS: "aslan,yumi"
ports:
- "13014:13014"
depends_on:
lucky-gift-service:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:13014/healthz/ready >/dev/null"]
interval: 5s
timeout: 3s
retries: 20
start_period: 10s
cron-service: cron-service:
build: build:
context: . context: .
@ -244,27 +190,6 @@ services:
retries: 20 retries: 20
start_period: 10s start_period: 10s
robot-service:
build:
context: .
dockerfile: services/robot-service/Dockerfile
args: *go-build-args
container_name: robot-service
environment:
TZ: UTC
ports:
- "13011:13011"
- "13111:13111"
depends_on:
mysql:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:13111/healthz/ready >/dev/null"]
interval: 5s
timeout: 3s
retries: 20
start_period: 10s
game-service: game-service:
build: build:
context: . context: .
@ -287,8 +212,6 @@ services:
condition: service_healthy condition: service_healthy
activity-service: activity-service:
condition: service_healthy condition: service_healthy
robot-service:
condition: service_healthy
healthcheck: healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:13108/healthz/ready >/dev/null"] test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:13108/healthz/ready >/dev/null"]
interval: 5s interval: 5s
@ -361,17 +284,15 @@ services:
- ./services/user-service/deploy/mysql/initdb/001_user_service.sql:/docker-entrypoint-initdb.d/002_user_service.sql:ro - ./services/user-service/deploy/mysql/initdb/001_user_service.sql:/docker-entrypoint-initdb.d/002_user_service.sql:ro
- ./services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql:/docker-entrypoint-initdb.d/003_wallet_service.sql:ro - ./services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql:/docker-entrypoint-initdb.d/003_wallet_service.sql:ro
- ./services/activity-service/deploy/mysql/initdb/001_activity_service.sql:/docker-entrypoint-initdb.d/004_activity_service.sql:ro - ./services/activity-service/deploy/mysql/initdb/001_activity_service.sql:/docker-entrypoint-initdb.d/004_activity_service.sql:ro
- ./tmp/mysql/initdb/005_utf8mb4_chinese_support.sql:/docker-entrypoint-initdb.d/005_utf8mb4_chinese_support.sql:ro - ${DEPLOY_PLATFORM_ROOT:-../deploy-platform}/deploy/mysql/initdb/005_utf8mb4_chinese_support.sql:/docker-entrypoint-initdb.d/005_utf8mb4_chinese_support.sql:ro
- ./tmp/mysql/initdb/006_admin_database.sql:/docker-entrypoint-initdb.d/006_admin_database.sql:ro - ${DEPLOY_PLATFORM_ROOT:-../deploy-platform}/deploy/mysql/initdb/006_admin_database.sql:/docker-entrypoint-initdb.d/006_admin_database.sql:ro
- ./tmp/mysql/initdb/007_resource_group_wallet_asset_items.sql:/docker-entrypoint-initdb.d/007_resource_group_wallet_asset_items.sql:ro - ${DEPLOY_PLATFORM_ROOT:-../deploy-platform}/deploy/mysql/initdb/007_resource_group_wallet_asset_items.sql:/docker-entrypoint-initdb.d/007_resource_group_wallet_asset_items.sql:ro
- ./services/lucky-gift-service/deploy/mysql/initdb/001_lucky_gift_service.sql:/docker-entrypoint-initdb.d/008_lucky_gift_service.sql:ro - ${DEPLOY_PLATFORM_ROOT:-../deploy-platform}/deploy/mysql/initdb/008_remove_taiwan_country.sql:/docker-entrypoint-initdb.d/008_remove_taiwan_country.sql:ro
- ./services/cron-service/deploy/mysql/initdb/001_cron_service.sql:/docker-entrypoint-initdb.d/009_cron_service.sql:ro - ./services/cron-service/deploy/mysql/initdb/001_cron_service.sql:/docker-entrypoint-initdb.d/009_cron_service.sql:ro
- ./services/game-service/deploy/mysql/initdb/001_game_service.sql:/docker-entrypoint-initdb.d/010_game_service.sql:ro - ./services/game-service/deploy/mysql/initdb/001_game_service.sql:/docker-entrypoint-initdb.d/010_game_service.sql:ro
- ./services/game-service/deploy/mysql/initdata/001_self_games.sql:/docker-entrypoint-initdb.d/010_game_service_data.sql:ro
- ./services/robot-service/deploy/mysql/initdb/001_robot_service.sql:/docker-entrypoint-initdb.d/011_robot_service.sql:ro
- ./services/notice-service/deploy/mysql/initdb/001_notice_service.sql:/docker-entrypoint-initdb.d/011_notice_service.sql:ro - ./services/notice-service/deploy/mysql/initdb/001_notice_service.sql:/docker-entrypoint-initdb.d/011_notice_service.sql:ro
- ./services/statistics-service/deploy/mysql/initdb/001_statistics_service.sql:/docker-entrypoint-initdb.d/012_statistics_service.sql:ro - ./services/statistics-service/deploy/mysql/initdb/001_statistics_service.sql:/docker-entrypoint-initdb.d/012_statistics_service.sql:ro
- ./tmp/mysql/initdb/999_local_grants.sql:/docker-entrypoint-initdb.d/999_local_grants.sql:ro - ${DEPLOY_PLATFORM_ROOT:-../deploy-platform}/deploy/mysql/initdb/999_local_grants.sql:/docker-entrypoint-initdb.d/999_local_grants.sql:ro
healthcheck: healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -proot --silent"] test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -proot --silent"]
interval: 5s interval: 5s

View File

@ -32,7 +32,7 @@
| 系统消息生产者 | `TODO` | wallet、room、user host domain 已有各自业务/outbox 方向 | 尚无统一 `CreateInboxMessage` 或 MQ consumer | | 系统消息生产者 | `TODO` | wallet、room、user host domain 已有各自业务/outbox 方向 | 尚无统一 `CreateInboxMessage` 或 MQ consumer |
| 活动消息生产者 | `TODO` | activity-service 目前只有活动状态查询 | 需要活动上线、奖励、任务、运营触达入箱 | | 活动消息生产者 | `TODO` | activity-service 目前只有活动状态查询 | 需要活动上线、奖励、任务、运营触达入箱 |
| 未读数缓存 | `TODO` | 无 Redis key 设计落地 | 需要缓存失效和 fallback 统计 | | 未读数缓存 | `TODO` | 无 Redis key 设计落地 | 需要缓存失效和 fallback 统计 |
| 广播/fanout | `DONE` | `message_fanout_jobs` + `CreateFanoutJob` + `ActivityCronService.ProcessMessageFanoutBatch`region/country/all_active_users/all_registered_users 通过 user-service `ListUserIDs` 游标取目标 | 还需要后台运营生产者接入 | | 广播/fanout | `DONE` | `message_fanout_jobs` + `CreateFanoutJob` + `ActivityCronService.ProcessMessageFanoutBatch`region/country/all_active_users 通过 user-service `ListUserIDs` 游标取目标 | 还需要后台运营生产者接入 |
这张表只描述当前仓库事实和缺口。后续实现必须先补 Redis 未读缓存和第一个领域生产者,不能让生产服务直接写 inbox 表。 这张表只描述当前仓库事实和缺口。后续实现必须先补 Redis 未读缓存和第一个领域生产者,不能让生产服务直接写 inbox 表。
@ -238,7 +238,6 @@ message_fanout_jobs(
| `region` | 区域活动、区域公告 | user-service `users.region_id` | | `region` | 区域活动、区域公告 | user-service `users.region_id` |
| `country` | 国家定向通知 | user-service `users.country` | | `country` | 国家定向通知 | user-service `users.country` |
| `all_active_users` | 全 App 活跃用户 | user-service active users cursor | | `all_active_users` | 全 App 活跃用户 | user-service active users cursor |
| `all_registered_users` | 全 App 注册用户 | user-service registered users cursor |
### Global Broadcast State ### Global Broadcast State
@ -516,7 +515,7 @@ tasks:
timeout: 10s timeout: 10s
lock_ttl: 30s lock_ttl: 30s
batch_size: 500 batch_size: 500
app_codes: ["lalu","fami","huwaa"] app_codes: ["lalu"]
``` ```
如果消息 owner 后续拆成独立 `message-service`这些配置迁移到新服务gateway 只需要更新 message gRPC 地址。 如果消息 owner 后续拆成独立 `message-service`这些配置迁移到新服务gateway 只需要更新 message gRPC 地址。
@ -528,7 +527,7 @@ tasks:
3. 在 `activity-service` 增加 inbox domain/repository/service先实现单用户入箱、列表、未读、已读、删除。 3. 在 `activity-service` 增加 inbox domain/repository/service先实现单用户入箱、列表、未读、已读、删除。
4. 在 `gateway-service` 增加 activity/message client 和 App HTTP`/api/v1/messages/tabs``/api/v1/messages``/api/v1/messages/{id}/read``/api/v1/messages/read-all``DELETE /api/v1/messages/{id}` 4. 在 `gateway-service` 增加 activity/message client 和 App HTTP`/api/v1/messages/tabs``/api/v1/messages``/api/v1/messages/{id}/read``/api/v1/messages/read-all``DELETE /api/v1/messages/{id}`
5. 接入第一个系统生产者,建议从提现审核或 host 申请结果开始,因为它们天然是单用户消息。 5. 接入第一个系统生产者,建议从提现审核或 host 申请结果开始,因为它们天然是单用户消息。
6. 增加 fanout job 和 `ActivityCronService.ProcessMessageFanoutBatch`,由 cron-service 调度,先支持 `region``country``user_ids``all_active_users``all_registered_users` 6. 增加 fanout job 和 `ActivityCronService.ProcessMessageFanoutBatch`,由 cron-service 调度,先支持 `region``country``user_ids``all_active_users`
7. 增加 `/api/v1/users/profiles:batch`,给腾讯云 IM 用户会话列表补头像、昵称、短号。 7. 增加 `/api/v1/users/profiles:batch`,给腾讯云 IM 用户会话列表补头像、昵称、短号。
8. 增加 Redis 未读数缓存和缓存失效逻辑。 8. 增加 Redis 未读数缓存和缓存失效逻辑。
9. 补 OpenAPI 文档、Docker 配置、线上配置示例和端到端冒烟脚本。 9. 补 OpenAPI 文档、Docker 配置、线上配置示例和端到端冒烟脚本。

View File

@ -1,298 +0,0 @@
# CP关系服务端开发文档
本文定义当前 CP、兄弟、姐妹关系的服务端边界。关系事实归 `user-service`,送礼扣费归 `wallet-service`,房间送礼事实归 `room-service`CP 相关 IM 由 `notice-service` 消费 `user_outbox` 后补偿投递。
## 1. 关系规则
| 类型 | 编码 | 说明 |
| --- | --- | --- |
| CP | `cp` | CP 关系 |
| 兄弟 | `brother` | 兄弟关系 |
| 姐妹 | `sister` | 姐妹关系 |
规则:
- CP 关系每个用户最多 1 个,不提供可调数量。
- 兄弟和姐妹的每人 active 数量由后台 CP 配置控制。
- A 和 B 同时只能存在一种 active 关系。
- A 可以给 B 发送 `cp``brother``sister` 多种 CP 礼物,形成多条 pending 申请。
- B 接受其中一个申请后,同一对用户其他 pending 申请会被置为 `blocked`,不能再接受。
- 申请默认 24 小时过期。
- 不提供佩戴 CP 功能;资料卡如果要展示关系,直接展示用户当前 active 关系。
## 2. 服务归属
| 服务 | 负责内容 |
| --- | --- |
| `wallet-service` | 礼物配置、扣费、热度值、礼物 icon 和动画快照 |
| `room-service` | 送礼命令、房间热度、房间礼物 IM、`RoomGiftSent` outbox |
| `user-service` | CP 申请、关系、亲密值、等级、等级奖励幂等、`user_outbox` |
| `gateway-service` | App HTTP 接口、鉴权、request_id 透传 |
| `notice-service` | 申请、同意、拒绝、关系成立的 C2C/房间/区域 IM |
| `cron-service` | 定时触发 user-service 过期申请处理 |
边界:
- `room-service` 不保存 CP 关系状态。
- `user-service` 不直连 `wallet-service``room-service``notice-service` 数据库。
- CP 申请和亲密值变化只能消费 `RoomGiftSent` MQ 事实,不能扫 `room_outbox`
- 下游服务只能消费 owner service 发布到 RocketMQ 的 outbox。
## 3. CP礼物配置
礼物大类使用 `gift_type_code=cp`。CP 子类型使用发送时快照字段 `cp_relation_type`,值为 `cp``brother``sister`
| 配置 | 说明 |
| --- | --- |
| 礼物类型 | `gift_type_code=cp`,礼物面板展示为 CP 礼物 Tab |
| CP 子类型 | `cp_relation_type=cp/brother/sister`,用于 CP Tab 二级分类 |
| 礼物 icon | 来自绑定资源的 `asset_url`,发送事件保存快照 |
| 礼物价格和热度 | 继续由 `wallet-service` 结算,客户端不能传价格或热度 |
`cp_relation_type` 写入礼物 `presentation_json`
```json
{
"cp_relation_type": "cp",
"cp_relation_label": "CP礼物"
}
```
## 4. RoomGiftSent字段
`api/proto/events/room/v1/events.proto``RoomGiftSent` 必须带以下送礼快照:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `gift_type_code` | string | CP 礼物为 `cp` |
| `cp_relation_type` | string | `cp``brother``sister`;非 CP 礼物为空 |
| `gift_name` | string | 礼物名称 |
| `gift_icon_url` | string | 礼物 icon |
| `gift_animation_url` | string | 礼物动画,可为空 |
## 5. 送礼触发申请
```mermaid
sequenceDiagram
participant F as Flutter
participant G as gateway-service
participant R as room-service
participant W as wallet-service
participant U as user-service
participant N as notice-service
participant IM as Tencent IM
F->>G: POST /api/v1/rooms/gift/send
G->>R: SendGift
R->>W: DebitGift
W-->>R: heat_value + gift_type_code + cp_relation_type + gift snapshot
R->>R: update heat/rank/command log
R-->>F: SendGiftResponse
R-->>U: RoomGiftSent MQ
U->>U: create or refresh CP application
U-->>N: UserCPApplicationCreated MQ
N->>IM: A -> B C2C cp_application_created
N->>IM: room cp_application_created, recipient_user_id=B
```
处理规则:
- 只有 `gift_type_code=cp``cp_relation_type` 有效时创建申请。
- 同一个 `RoomGiftSent.event_id` 只能消费一次。
- A 给 B 发送同一类型 CP 礼物时,如果已有 pending 申请,刷新礼物快照和 `expires_at_ms`
- A 给 B 发送不同类型 CP 礼物时,可以各有一条 pending 申请。
- 如果任一方已经有 active 关系,申请可以存在,但接受时会被拒绝;已有关系双方互送礼物会增加亲密值。
## 6. 同意和拒绝
```mermaid
sequenceDiagram
participant F as Flutter B
participant G as gateway-service
participant U as user-service
participant N as notice-service
participant IM as Tencent IM
F->>G: POST /api/v1/cp/applications/{id}/accept
G->>U: AcceptCPApplication
U->>U: lock pair + lock both users + create relationship
U-->>N: UserCPApplicationAccepted MQ
U-->>N: UserCPRelationshipCreated MQ
N->>IM: B -> A C2C cp_application_accepted
N->>IM: A -> B C2C cp_application_accepted
N->>IM: room cp_application_accepted, recipient_user_id=A
N->>IM: region cp_relationship_created
```
处理规则:
- 接受和拒绝都只能由申请目标用户 B 操作。
- pending 申请过期后不能接受gateway envelope 返回 `CONFLICT`;内部 gRPC 语义是 FailedPrecondition。
- 接受时同时校验 A 和 B 是否已经有任意 active 关系;任一方已有关系就返回 `CONFLICT`
- 同意成功后写 active 关系,关闭同一对用户其他 pending 申请。
- 同意会发两条 C2C`B -> A` 表示“我同意了你”,`A -> B` 表示“对方已同意你的申请”。
- 同意和拒绝都会向 A 发送房间 IM`recipient_user_id=A` 限定展示对象。
- 拒绝仍只向 A 发送一条 C2C 和一条房间 IM。
- 同意成功后额外发送区域 IM区域取申请来源房间的 `visible_region_id`
## 7. 亲密值和升级
任意礼物发送成功后,如果发送方和收礼方已经有 active 关系,则按本次礼物的 `heat_value` 增加亲密值。
规则:
- 普通礼物按 100% 热度值增加亲密值。
- 幸运礼物、超级幸运礼物直接使用钱包回执里的最终 `heat_value`,不重复计算比例。
- 多目标送礼时按每个目标的账务回执分别增加对应关系亲密值。
- 等级范围 1 到 5等级亲密值阈值和奖励资源组由后台 CP 配置维护。
- 每个等级可绑定奖励资源组,升级时调用 `wallet-service` 给双方发放资源。
- 奖励发放幂等键为 `cp_level_reward:{relationship_id}:{level}:{user_id}`
- App 关系查询返回当前等级阈值、下一等级阈值、距离下一级和进度百分比。
- 资料卡弹窗只能使用 `/api/v1/cp/relationships` 返回的阈值字段展示进度;客户端不能读取后台配置或硬编码等级表。
## 8. 解除关系
用户可以主动解除自己的 active 关系。解除费用由后台 CP 配置按关系类型维护,单位是金币。
规则:
- App 先通过 `/api/v1/cp/relationships` 读取 `breakup_cost_coins` 展示费用。
- 解除接口必须传 `command_id`,同一次解除重试复用同一个 `command_id`
- gateway 先在 user-service 创建解除占位,再调用 wallet-service 扣金币,最后确认关系为 `ended`
- 如果 wallet-service 扣费失败user-service 会取消解除占位,关系仍保持 active。
- 解除成功后不会发送 C2C、房间 IM 或区域 IM客户端使用接口返回和关系列表刷新 UI。
## 9. App HTTP接口
所有接口走 `gateway-service`,响应使用 `{code,message,request_id,data}`
| 方法 | 地址 | 说明 |
| --- | --- | --- |
| `GET` | `/api/v1/cp/applications` | 查询我的申请 |
| `POST` | `/api/v1/cp/applications/{application_id}/accept` | 同意申请 |
| `POST` | `/api/v1/cp/applications/{application_id}/reject` | 拒绝申请 |
| `GET` | `/api/v1/cp/relationships` | 查询我的 active 关系 |
| `POST` | `/api/v1/cp/relationships/{relationship_id}/break` | 解除关系并扣金币 |
`/api/v1/cp/relationships` 的 relationship item 包含:
| 字段 | 说明 |
| --- | --- |
| `relationship_id` | 关系 ID |
| `relation_type` | `cp``brother``sister` |
| `status` | 当前为 `active` |
| `me` | 当前用户快照 |
| `partner` | 对方用户快照 |
| `intimacy_value` | 当前亲密值 |
| `level` | 当前等级 |
| `current_level_threshold` | 当前等级起始亲密值 |
| `next_level_threshold` | 下一等级所需亲密值;满级时为 0 |
| `needed_for_next_level` | 距离下一级还需要多少亲密值;满级时为 0 |
| `level_progress_percent` | 当前等级内进度百分比0 到 100 |
| `max_level` | 是否已经没有下一等级 |
| `breakup_cost_coins` | 当前关系解除需要支付的金币数 |
| `formed_at_ms` | 关系形成时间 |
| `ended_at_ms` | 解除时间active 关系为 0 |
| `updated_at_ms` | 最近更新时间 |
这些字段由 user-service 从 `user_cp_level_rules` 计算gateway 只做透传。客户端资料卡不能根据本地表推算这些值。
申请列表参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `direction` | string | 否 | `incoming``outgoing`,默认 `incoming` |
| `status` | string | 否 | `pending``accepted``rejected``expired``blocked` |
| `page` | int32 | 否 | 默认 1 |
| `page_size` | int32 | 否 | 默认 20 |
关系列表参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `relation_type` | string | 否 | `cp``brother``sister`;不传返回全部 |
| `page` | int32 | 否 | 默认 1 |
| `page_size` | int32 | 否 | 默认 20 |
拒绝请求体:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `reason` | string | 否 | 拒绝原因,只进入 IM/outbox payload |
解除请求体:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `command_id` | string | 是 | 解除关系幂等键,同一次重试必须复用 |
解除返回值:
| 字段 | 说明 |
| --- | --- |
| `relationship` | 已解除的关系,`status=ended` |
| `breakup_cost_coins` | 本次实际扣除金币数 |
| `wallet_transaction_id` | 钱包扣费交易 ID费用为 0 时为空 |
| `coin_balance_after` | 扣费后金币余额;费用为 0 时为 0 |
## 10. 数据表
表放在 `user-service` 库,所有表带 `app_code`,时间使用 UTC epoch milliseconds。
| 表 | 说明 |
| --- | --- |
| `user_cp_relation_configs` | CP/兄弟/姐妹数量、申请过期和解除费用配置CP 数量服务端固定为 1兄弟/姐妹读取后台配置 |
| `user_cp_level_rules` | 等级阈值和奖励资源组 |
| `user_cp_gift_event_consumption` | `RoomGiftSent` 消费幂等 |
| `user_cp_applications` | CP 关系申请 |
| `user_cp_relationships` | 双人关系事实 |
| `user_cp_relationship_breakups` | 解除关系 pending/confirmed/canceled 占位和幂等事实 |
| `user_cp_level_reward_grants` | 等级奖励发放幂等 |
| `user_outbox` | CP 事件事实 |
关键唯一约束:
- active 关系按用户对唯一:`(app_code, user_a_id, user_b_id, status='active')`
- CP active 关系按单用户唯一:任一用户已有 CP 关系就不能再接受新的 CP 关系。
- 兄弟/姐妹 active 关系按后台配置做单用户数量上限校验。
- pending 申请按用户对和类型唯一:`(app_code, requester_user_id, target_user_id, relation_type, status='pending')`
- 解除关系按 `command_id` 幂等;同一关系同一时间只能有一条 pending 解除占位。
## 11. 相关IM
| IM | 通道 | 发送时机 |
| --- | --- | --- |
| `cp_application_created` | C2C | A 送 CP 礼物触发申请后A 发给 B |
| `cp_application_created` | 房间群 | A 送 CP 礼物触发申请后发到来源房间payload 带 `recipient_user_id=B` |
| `cp_application_accepted` | C2C | B 同意后B 发给 Apayload 带 `c2c_notice_role=accepted_to_requester``message_text=我同意了你` |
| `cp_application_accepted` | C2C | B 同意后A 发给 Bpayload 带 `c2c_notice_role=accepted_to_acceptor``message_text=对方已同意你的申请` |
| `cp_application_accepted` | 房间群 | B 同意后发到来源房间payload 带 `recipient_user_id=A` |
| `cp_application_rejected` | C2C | B 拒绝后B 发给 A |
| `cp_application_rejected` | 房间群 | B 拒绝后发到来源房间payload 带 `recipient_user_id=A` |
| `cp_relationship_created` | 区域群 | 关系建立后发送区域播报 |
| 无 | 无 | 解除关系成功不发 IMApp 根据解除接口返回刷新 |
C2C 使用腾讯云 IM `From_Account` 表达会话方向:申请为 A拒绝为 B同意会发 `B -> A``A -> B` 两条。区域 IM 群 ID 使用 `hy_<app_code>_bc_r_v2_<region_id>`v1 旧群 `hy_<app_code>_bc_r_<region_id>` 仅保留过渡期双发),本地测试可通过 `group_id_prefix` 加前缀。
## 12. 错误码
| 场景 | code | 说明 |
| --- | --- | --- |
| 申请不存在 | `NOT_FOUND` | application_id 不存在或不属于当前用户 |
| 申请已过期 | `CONFLICT` | 不能再同意;内部 gRPC 语义是 FailedPrecondition |
| 已有关系 | `CONFLICT` | A/B 任一方已经有 active 关系 |
| 无权限处理 | `PERMISSION_DENIED` | 不是申请目标用户 |
| 余额不足 | `INSUFFICIENT_BALANCE` | 解除关系扣金币失败 |
| 关系不可解除 | `CONFLICT` | 关系不是 active或已有 pending 解除 |
| 幂等冲突 | `CONFLICT` | command_id 或事件消费 payload 不一致 |
## 13. 验证项
- CP 礼物发送成功后,`RoomGiftSent``gift_type_code=cp``cp_relation_type`、礼物 icon 快照。
- A 给 B 连发 `cp``brother``sister` 三种礼物,会生成三条 pending 申请。
- B 接受其中一条后,其他两条不能再接受。
- A/B 建立关系后A 或 B 都不能再和第三人建立 active 关系。
- pending 申请超过默认 24 小时后不能接受。
- active 关系双方互送任意礼物,亲密值按 `heat_value` 增加。
- active 关系解除会扣除后台配置的金币费用,扣费失败不改变关系状态。
- 同意、拒绝、关系成立的 IM 都写入 `notice_delivery_events`,失败可重试。

View File

@ -1,119 +0,0 @@
# CP关系本地真实链路测试用例
本文只记录本地真实链路验证用例。业务动作优先走 gateway HTTP、room/wallet/user/notice 服务和 RocketMQSQL 只用于准备本地测试数据和核对最终事实。
## 环境前置
- Docker 本地服务MySQL、Redis、RocketMQ、gateway-service、room-service、wallet-service、user-service、notice-service。
- `user-service` 必须启用 `cp_relationship_worker`,并消费 `hyapp_room_outbox`
- `notice-service` 必须消费 `hyapp_user_outbox`,并把 CP 申请/同意/拒绝/关系创建事件投递到腾讯云 IM。
- 本地测试账号 A、B、C 需要是同一个 `app_code=lalu` 下的完整资料用户。
- 本地 CP 礼物需要是 `gift_type_code=cp`,并在礼物配置中写入 `cp_relation_type`
- 送礼前 A 和 B 必须都在同一个房间的 room-service presence 内。
## T01 CP礼物创建申请
步骤:
1. 准备 A、B 用户和 A 的 COIN 余额。
2. 准备 CP 礼物 `local_cp_gift_cp``cp_relation_type=cp`
3. A 创建房间A 和 B 都调用进房接口。
4. A 通过 gateway 给 B 送 CP 礼物。
验证:
- `hyapp_room.room_outbox``RoomGiftSent`payload 中 `cp_relation_type=cp``gift_icon_url` 非空。
- RocketMQ `hyapp_room_outbox` 被 user-service CP consumer 消费。
- `hyapp_user.user_cp_applications` 有 A 到 B 的 pending 申请,`expires_at_ms` 为当前时间加服务端默认 24 小时。
- `hyapp_user.user_cp_gift_event_consumption` 有该 room event 的消费记录。
- `hyapp_user.user_outbox``UserCPApplicationCreated`
- 腾讯云 IM 有 A 发给 B 的 C2C 申请消息,`Desc=cp_application_created`payload 含 `application_id``relation_type=cp``expires_at_ms`
- 腾讯云 IM 有发到房间群的申请消息,`Desc=cp_application_created`payload 含 `recipient_user_id=B``room_id`
## T02 B同意申请建立关系
步骤:
1. 使用 T01 产生的 `application_id`
2. B 调用 gateway 同意申请接口。
验证:
- `user_cp_applications.status=accepted`
- `user_cp_relationships` 新增 active 关系A/B 无序 pair 只有一条 active 关系。
- 同一对用户其他 pending 申请被置为 `blocked`
- `user_outbox``UserCPApplicationAccepted``UserCPRelationshipCreated`
- 腾讯云 IM 向 A 发送 B 发出的同意结果 C2C 消息,`Desc=cp_application_accepted`
- 腾讯云 IM 向房间群发送同意结果消息,`Desc=cp_application_accepted`payload 含 `recipient_user_id=A`
- 腾讯云 IM 向区域群发送关系创建消息,`Desc=cp_relationship_created`payload 含 A/B 头像昵称短 ID、礼物 icon 和 `room_id`
## T03 已有关系后送礼增加亲密值并升级
步骤:
1. 使用服务端默认等级规则,确认 1 级阈值为 0。
2. A 再次给 B 送 CP 礼物。
验证:
- 不新增 pending 申请。
- 原关系 `intimacy_value` 增加礼物结算热度值。
- 原关系 `level_no` 按服务端默认阈值计算。
- `user_outbox``UserCPRelationshipIntimacyChanged`
## T04 同一对用户只能同时存在一种关系
步骤:
1. A 给 B 发送兄弟礼物,生成 brother pending 申请。
2. A 给 B 发送姐妹礼物,生成 sister pending 申请。
3. B 接受其中一个申请。
4. B 再尝试接受另一个申请。
验证:
- 第一个接受成功后产生 active 关系。
- 另一个申请不能接受,接口返回冲突提示。
- A/B active 关系仍只有一种。
## T05 同一用户只能同时存在一个关系
步骤:
1. 使用 T02 建立 A/B active 关系。
2. C 给 A 发送 CP 礼物,生成 C 到 A 的 pending 申请。
3. A 尝试同意 C 的申请。
验证:
- 同意接口返回冲突。
- A/B active 关系仍保留。
- C/A 不创建 active 关系。
## T06 申请过期
步骤:
1. 创建一条 pending 申请。
2. 通过 SQL 把 `expires_at_ms` 调整到当前时间之前。
3. B 查询申请列表或尝试同意。
验证:
- 申请状态被固化为 `expired`
- 同意接口返回 `CONFLICT`,申请被固化为 `expired`,不建立关系。
## T07 拒绝申请
步骤:
1. 创建一条 pending 申请。
2. B 调用拒绝接口。
验证:
- `user_cp_applications.status=rejected`
- 不创建 active 关系。
- `user_outbox``UserCPApplicationRejected`
- 腾讯云 IM 向 A 发送 B 发出的拒绝结果 C2C 消息,`Desc=cp_application_rejected`
- 腾讯云 IM 向房间群发送拒绝结果消息,`Desc=cp_application_rejected`payload 含 `recipient_user_id=A`

File diff suppressed because it is too large Load Diff

View File

@ -1,88 +0,0 @@
# 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

@ -1,339 +0,0 @@
# H5 自研猜拳接入接口
本文只记录独立自研猜拳 `rock/rps` 的 H5 接口。所有地址都有 `/api/v1` 前缀,所有接口都需要 `Authorization: Bearer <access_token>`
成功响应统一格式:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {}
}
```
时间字段都是 Unix epoch milliseconds。用户 ID 建议 H5 按字符串字段使用。
## 1. 获取游戏列表
地址:
```http
GET /api/v1/games
```
参数:
| 字段 | 位置 | 必填 | 说明 |
| --- | --- | --- | --- |
| `scene` | query | 否 | 场景,语音房传 `voice_room`。 |
| `room_id` | query | 否 | 房间 ID。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `games` | array | 游戏列表。 |
| `games[].game_id` | string | 猜拳是 `rock`。 |
| `games[].name` | string | 游戏名称。 |
| `games[].launch_mode` | string | H5 打开方式。 |
| `games[].enabled` | bool | 是否可进入。 |
| `games[].maintenance` | bool | 是否维护中。 |
| `server_time_ms` | number | 服务端时间。 |
相关 IM无。
## 2. 创建 H5 启动会话
地址:
```http
POST /api/v1/games/rock/launch
Content-Type: application/json
```
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `scene` | string | 否 | 场景,语音房传 `voice_room`。 |
| `room_id` | string | 否 | 房间 ID。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `session_id` | string | H5 启动会话 ID。 |
| `launch_url` | string | H5 页面地址。真实地址由后台配置,不在客户端拼。 |
| `expires_at_ms` | number | 启动会话过期时间。 |
| `orientation` | string | 页面方向。 |
| `safe_height` | number | 安全高度。 |
| `safeHeight` | number | 同 `safe_height`。 |
| `server_time_ms` | number | 服务端时间。 |
相关 IM无。
## 3. 查询猜拳配置
地址:
```http
GET /api/v1/games/rps/config
```
参数:
| 字段 | 位置 | 必填 | 说明 |
| --- | --- | --- | --- |
| `game_id` | query | 否 | 固定是 `rock`,不传也按 `rock` 处理。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `config.game_id` | string | 固定返回 `rock`。 |
| `config.status` | string | `active` 才能玩。 |
| `config.stake_options` | array | 可下注金币档位。 |
| `config.fee_bps` | number | 平台抽水,万分比。 |
| `config.pool_bps` | number | 奖池入池,万分比。 |
| `config.min_players` | number | 最少人数。 |
| `config.max_players` | number | 最多人数。 |
| `config.robot_enabled` | bool | 是否启用机器人补位。 |
| `config.robot_match_wait_ms` | number | 等待机器人补位时间。 |
| `config.pool_balance_coin` | number | 当前奖池余额。 |
| `server_time_ms` | number | 服务端时间。 |
`stake_options` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `stake_coin` | number | 下注金币。 |
| `enabled` | bool | 是否可选。 |
| `sort_order` | number | 排序。 |
相关 IM无。
## 4. 快速匹配
地址:
```http
POST /api/v1/games/rps/match
Content-Type: application/json
```
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `stake_coin` | number | 是 | 下注金币,必须是配置里的启用档位。 |
| `gesture` | string | 是 | 本人提前选择的手势:`rock``paper``scissors`。 |
| `rps_gesture` | string | 否 | 同 `gesture`,两个字段传一个即可。 |
| `game_id` | string | 否 | 固定是 `rock`。 |
| `room_id` | string | 否 | 房间 ID。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `match` | object | 当前局详情,字段见“局详情字段”。 |
| `server_time_ms` | number | 服务端时间。 |
相关 IM无。H5 可以轮询详情,也可以等后续接入 IM 后按 IM 刷新。
## 5. 创建等待局
地址:
```http
POST /api/v1/games/rps/matches
Content-Type: application/json
```
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `stake_coin` | number | 是 | 下注金币。 |
| `gesture` | string | 是 | 本人提前选择的手势。 |
| `rps_gesture` | string | 否 | 同 `gesture`。 |
| `game_id` | string | 否 | 固定是 `rock`。 |
| `room_id` | string | 否 | 房间 ID。 |
| `min_players` | number | 否 | 最少人数,不传用配置。 |
| `max_players` | number | 否 | 最多人数,不传用配置。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `match` | object | 当前局详情,字段见“局详情字段”。 |
| `server_time_ms` | number | 服务端时间。 |
相关 IM无。
## 6. 加入等待局
地址:
```http
POST /api/v1/games/rps/matches/{match_id}/join
Content-Type: application/json
```
参数:
| 字段 | 位置 | 必填 | 说明 |
| --- | --- | --- | --- |
| `match_id` | path | 是 | 局 ID。 |
| `gesture` | body | 是 | 本人提前选择的手势。 |
| `rps_gesture` | body | 否 | 同 `gesture`。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `match` | object | 当前局详情,字段见“局详情字段”。 |
| `server_time_ms` | number | 服务端时间。 |
相关 IM无。
## 7. 查询局详情
地址:
```http
GET /api/v1/games/rps/matches/{match_id}
```
参数:
| 字段 | 位置 | 必填 | 说明 |
| --- | --- | --- | --- |
| `match_id` | path | 是 | 局 ID。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `match` | object | 当前局详情,字段见“局详情字段”。 |
| `server_time_ms` | number | 服务端时间。 |
相关 IM无。
## 8. 揭晓并结算
地址:
```http
POST /api/v1/games/rps/matches/{match_id}/roll
Content-Type: application/json
```
参数:
| 字段 | 位置 | 必填 | 说明 |
| --- | --- | --- | --- |
| `match_id` | path | 是 | 局 ID。 |
| `gesture` | body | 否 | 可以不传;如果传,必须等于创建或加入时的预选手势。 |
| `rps_gesture` | body | 否 | 同 `gesture`。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `match` | object | 已结算局详情。 |
| `server_time_ms` | number | 服务端时间。 |
说明:猜拳不会随机出拳。服务端用双方提前选择的 `gesture` 比输赢,然后扣款、派奖和更新余额。
相关 IM无。
## 9. 取消等待局
地址:
```http
POST /api/v1/games/rps/matches/{match_id}/cancel
```
参数:
| 字段 | 位置 | 必填 | 说明 |
| --- | --- | --- | --- |
| `match_id` | path | 是 | 局 ID。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `match` | object | 取消后的局详情。 |
| `server_time_ms` | number | 服务端时间。 |
相关 IM无。
## 10. 局详情字段
`match` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `match_id` | string | 局 ID。 |
| `game_id` | string | 固定是 `rock`。 |
| `room_id` | string | 房间 ID。 |
| `region_id` | number | 用户区域 ID。 |
| `min_players` | number | 最少人数。 |
| `max_players` | number | 最多人数。 |
| `current_players` | number | 当前人数。 |
| `stake_coin` | number | 本局下注金币。 |
| `round_no` | number | 当前轮次。 |
| `status` | string | `created``ready``settled``canceled`。 |
| `phase` | string | `waiting``countdown``settled``canceled`。 |
| `phase_deadline_ms` | number | 当前阶段截止时间。 |
| `result` | string | 当前用户视角结果:`win``lose``draw`。 |
| `participants` | array | 参与者列表。 |
| `join_deadline_ms` | number | 等待加入截止时间。 |
| `ready_at_ms` | number | 匹配完成时间。 |
| `settled_at_ms` | number | 结算时间。 |
| `canceled_at_ms` | number | 取消时间。 |
| `fee_bps` | number | 平台抽水。 |
| `pool_bps` | number | 奖池入池。 |
| `match_mode` | string | `human``robot`。 |
| `forced_result` | string | 机器人奖池不足时的强制结果。 |
| `pool_delta_coin` | number | 本局奖池变化。 |
`participants` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `user_id` | string | 用户 IDH5 优先用这个字段。 |
| `user_id_number` | number | 用户 ID 数字值。 |
| `seat_no` | number | 座位号。 |
| `status` | string | `joined``settled``canceled`。 |
| `stake_coin` | number | 下注金币。 |
| `gesture` | string | 预选手势。 |
| `rps_gesture` | string | 同 `gesture`。 |
| `result` | string | `win``lose``draw`。 |
| `payout_coin` | number | 派彩金币。 |
| `balance_after` | number | 结算后余额。 |
| `joined_at_ms` | number | 加入时间。 |
| `updated_at_ms` | number | 更新时间。 |
| `participant_type` | string | `user``robot`。 |
| `is_robot` | bool | 是否机器人。 |
| `user.display_user_id` | string | 展示 ID。 |
| `user.nickname` | string | 昵称。 |
| `user.avatar` | string | 头像。 |
| `avatar_frame` | object | 头像框资源。 |
## 11. H5 推荐流程
1. App 调 `GET /api/v1/games`,看到 `game_id=rock` 后展示入口。
2. App 调 `POST /api/v1/games/rock/launch` 打开 H5。
3. H5 调 `GET /api/v1/games/rps/config` 展示下注档位。
4. 用户先选择 `rock``paper``scissors`
5. H5 调 `POST /api/v1/games/rps/match` 快速匹配。
6. 返回 `phase=countdown`H5 用 `phase_deadline_ms` 做倒计时。
7. 倒计时结束后H5 调 `POST /api/v1/games/rps/matches/{match_id}/roll` 揭晓并结算。
8. H5 读返回的 `participants[].gesture/result/payout_coin/balance_after` 展示结果。
相关 IM当前独立猜拳没有 IM。H5 用 HTTP 返回和详情轮询刷新。

View File

@ -38,7 +38,7 @@ docs/房间Outbox补偿开发.md
| 播报发送能力 | `activity-service` | 服务端向全局/区域播报群发送自定义消息 | | 播报发送能力 | `activity-service` | 服务端向全局/区域播报群发送自定义消息 |
| 旧区域群成员移除 | `activity-service` | 用户区域变化后,对旧区域群执行 `delete_group_member(user_id)` | | 旧区域群成员移除 | `activity-service` | 用户区域变化后,对旧区域群执行 `delete_group_member(user_id)` |
| 贵重礼物播报触发 | `activity-service` consuming room outbox | 基于 `RoomGiftSent` 事实判断阈值并发区域播报 | | 贵重礼物播报触发 | `activity-service` consuming room outbox | 基于 `RoomGiftSent` 事实判断阈值并发区域播报 |
| 火箭倒计时播报触发 | `activity-service` consuming room outbox | 基于 `RoomRocketIgnited` 事实按后台配置发区域或全局播报 | | 宝箱倒计时播报触发 | `activity-service` consuming room outbox | 基于 `RoomTreasureCountdownStarted` 事实按后台配置发区域或全局播报 |
本阶段不做: 本阶段不做:
@ -57,15 +57,8 @@ GroupID 必须稳定、短、可解析,并满足腾讯 IM 自定义 GroupID
| Group Type | GroupID | Meaning | | Group Type | GroupID | Meaning |
| --- | --- | --- | | --- | --- | --- |
| room | `<room_id>` | 现有房间群,必须满足 `pkg/roomid` 规则 | | room | `<room_id>` | 现有房间群,必须满足 `pkg/roomid` 规则 |
| global_broadcast | `hy_<app_code>_bc_g_v2`v1: `hy_<app_code>_bc_g` | App 内全局播报群 | | global_broadcast | `hy_<app_code>_bc_g` | App 内全局播报群 |
| region_broadcast | `hy_<app_code>_bc_r_v2_<region_id>`v1: `hy_<app_code>_bc_r_<region_id>` | App 内指定区域播报群 | | region_broadcast | `hy_<app_code>_bc_r_<region_id>` | App 内指定区域播报群 |
v1 → v2 迁移2026-07
- v1 播报群是 ChatRoom 类型,受套餐成员上限约束(标准版 2000 人),线上已触发 10038 拒绝加群;全局群与区域群先后都迁移到 AVChatRoom 类型的 v2 群(成员无上限、免审批)。
- `imgroup.GlobalBroadcastGroupID*` / `RegionBroadcastGroupID*` 只生成 v2 IDv1 ID 仅能由 `Legacy*` 变体生成,用于过渡期双发和旧群成员移除。
- 解析层对 v1/v2 返回同一个 Kind调用方不感知版本reconciler 只 ensure v2 群v1 旧群不再维护。
- 过渡期 outbox 投递成功后会向对应 v1 旧群补发一份 `<event_id>_legacy` 副本,覆盖尚未重新拉取 join 列表的存量会话;老客户端全部换代后可移除双发。
规则: 规则:
@ -114,7 +107,7 @@ func Parse(groupID string) ParsedGroup
| 红包资金事实 | `wallet-service` 或独立 red-packet domain | IM 只携带入口和展示字段 | | 红包资金事实 | `wallet-service` 或独立 red-packet domain | IM 只携带入口和展示字段 |
| 客户端 IM 长连接 | Tencent IM SDK | 本仓库不自建 WebSocket | | 客户端 IM 长连接 | Tencent IM SDK | 本仓库不自建 WebSocket |
`room-service` 只需要继续产出 `RoomGiftSent``RoomRocketIgnited` 等 outbox 事实。贵重礼物和火箭倒计时是否需要区域/全局播报,由 `activity-service` 通过 direct gRPC 或 RocketMQ `hyapp_room_outbox` consumer group 消费后判断,这样不会把区域播报策略塞进 Room Cell 主链路。 `room-service` 只需要继续产出 `RoomGiftSent``RoomTreasureCountdownStarted` 等 outbox 事实。贵重礼物和宝箱倒计时是否需要区域/全局播报,由 `activity-service` 通过 direct gRPC 或 RocketMQ `hyapp_room_outbox` consumer group 消费后判断,这样不会把区域播报策略塞进 Room Cell 主链路。
## Client Flow ## Client Flow
@ -130,8 +123,8 @@ sequenceDiagram
U-->>G: user.region_id, profile_completed U-->>G: user.region_id, profile_completed
G-->>C: UserSig + join_groups G-->>C: UserSig + join_groups
C->>IM: login(user_id, user_sig) C->>IM: login(user_id, user_sig)
C->>IM: joinGroup(hy_<app>_bc_g_v2) C->>IM: joinGroup(hy_<app>_bc_g)
C->>IM: joinGroup(hy_<app>_bc_r_v2_<region_id>) C->>IM: joinGroup(hy_<app>_bc_r_<region_id>)
``` ```
`/api/v1/im/usersig` response 增加 `join_groups`,客户端必须按返回列表加入播报群: `/api/v1/im/usersig` response 增加 `join_groups`,客户端必须按返回列表加入播报群:
@ -144,11 +137,11 @@ sequenceDiagram
"expire_at_ms": 1770000000000, "expire_at_ms": 1770000000000,
"join_groups": [ "join_groups": [
{ {
"group_id": "hy_lalu_bc_g_v2", "group_id": "hy_lalu_bc_g",
"type": "global_broadcast" "type": "global_broadcast"
}, },
{ {
"group_id": "hy_lalu_bc_r_v2_1001", "group_id": "hy_lalu_bc_r_1001",
"type": "region_broadcast", "type": "region_broadcast",
"region_id": 1001 "region_id": 1001
} }
@ -411,9 +404,9 @@ sequenceDiagram
如果现有事件字段不足,应优先扩展 protobuf event而不是让 activity-service 反查多个服务拼凑事实。 如果现有事件字段不足,应优先扩展 protobuf event而不是让 activity-service 反查多个服务拼凑事实。
## Room Rocket Broadcast ## Room Treasure Broadcast
火箭燃料满进入倒计时时Room Cell 写 `RoomRocketIgnited` outbox。activity-service 消费该事实后按后台配置决定发区域播报还是全局播报;发射到点、奖励结算和火箭状态推进仍由 room-service 负责。 宝箱满能量进入倒计时时Room Cell 写 `RoomTreasureCountdownStarted` outbox。activity-service 消费该事实后按后台配置决定发区域播报还是全局播报;开箱到点、奖励结算和宝箱状态推进仍由 room-service 负责。
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@ -428,12 +421,11 @@ sequenceDiagram
C->>G: POST /api/v1/rooms/gift/send C->>G: POST /api/v1/rooms/gift/send
G->>R: SendGift G->>R: SendGift
R->>W: DebitGift R->>W: DebitGift
W-->>R: receipt + heat_value W-->>R: receipt + heat/gift point
R->>R: add effective rocket fuel R->>R: add effective treasure energy
R->>R: append pending_launch when threshold reached R->>R: status=countdown when threshold reached
R->>R: start next level immediately
R-->>G: SendGiftResponse R-->>G: SendGiftResponse
R->>MQ: RoomRocketIgnited R->>MQ: RoomTreasureCountdownStarted
MQ->>A: consume by hyapp-activity-room-outbox MQ->>A: consume by hyapp-activity-room-outbox
A->>IM: send_group_msg(region/global broadcast group) A->>IM: send_group_msg(region/global broadcast group)
``` ```
@ -442,22 +434,15 @@ sequenceDiagram
```json ```json
{ {
"event_id": "room_rocket:lalu_room_xxx:rocket_1", "event_id": "room_treasure:lalu_room_xxx:box_1",
"broadcast_type": "room_rocket", "broadcast_type": "room_treasure",
"scope": "region", "scope": "region",
"app_code": "lalu", "app_code": "lalu",
"region_id": 1001, "region_id": 1001,
"room_id": "lalu_room_xxx", "room_id": "lalu_room_xxx",
"rocket_id": "rocket_1", "box_id": "box_1",
"rocket_level": 3, "level": 3,
"rocket_cover_url": "https://cdn.example/rocket-l3.png", "open_at_ms": 1770000000000,
"launch_at_ms": 1770000000000,
"igniter": {
"user_id": "10001",
"short_id": "80001",
"nickname": "Alice",
"avatar": "https://cdn.example/avatar.png"
},
"action": { "action": {
"type": "enter_room", "type": "enter_room",
"room_id": "lalu_room_xxx" "room_id": "lalu_room_xxx"
@ -465,7 +450,7 @@ sequenceDiagram
} }
``` ```
当前等级满阈值后room-service 立刻把该火箭加入 `pending_launches`并把下一等级作为当前进度继续累计单次送礼溢出的燃料作废不顺延。activity-service 只按 `RoomRocketIgnited` 做飘屏,不自行推导火箭进度。 倒计时期间继续送礼不会累加到下一宝箱,溢出能量也不会跨级;这些规则由 room-service 在 `RoomTreasureProgressChanged``RoomTreasureCountdownStarted` 事实中表达activity-service 不能自行推导宝箱进度。
## Red Packet Broadcast ## Red Packet Broadcast
@ -479,7 +464,7 @@ sequenceDiagram
| 过期退款 | wallet/red-packet domain + cron | | 过期退款 | wallet/red-packet domain + cron |
| IM 播报 | activity-service broadcast | | IM 播报 | activity-service broadcast |
红包创建成功后,红包领域写 `RedPacketCreated` 事件activity-service 转成区域/全局播报。客户端点击播报只发射红包入口,领取时必须调用后端接口,不能信任 IM payload 中的金额。 红包创建成功后,红包领域写 `RedPacketCreated` 事件activity-service 转成区域/全局播报。客户端点击播报只打开红包入口,领取时必须调用后端接口,不能信任 IM payload 中的金额。
## App Code And Region Safety ## App Code And Region Safety
@ -511,7 +496,7 @@ sequenceDiagram
5. `activity-service` 增加 broadcast group reconciler启动时确保全局和 active 区域群。 5. `activity-service` 增加 broadcast group reconciler启动时确保全局和 active 区域群。
6. `activity-service` 增加 `PublishRegionBroadcast/PublishGlobalBroadcast` 和 broadcast outbox。 6. `activity-service` 增加 `PublishRegionBroadcast/PublishGlobalBroadcast` 和 broadcast outbox。
7. 扩展 `RoomGiftSent` 事件字段activity-service 消费后按阈值发 `super_gift` 区域播报。 7. 扩展 `RoomGiftSent` 事件字段activity-service 消费后按阈值发 `super_gift` 区域播报。
8. 消费 `RoomRocketIgnited`,按后台配置发 `room_rocket` 区域或全局播报。 8. 消费 `RoomTreasureCountdownStarted`,按后台配置发 `room_treasure` 区域或全局播报。
9. 红包领域落账务事实后,复用同一播报能力发送 `red_packet` 消息。 9. 红包领域落账务事实后,复用同一播报能力发送 `red_packet` 消息。
## Test Expectations ## Test Expectations
@ -528,5 +513,5 @@ sequenceDiagram
| group reconciler | 已存在群视为成功,失败可重试 | | group reconciler | 已存在群视为成功,失败可重试 |
| broadcast outbox | 同一 `event_id` 幂等,失败后 retry | | broadcast outbox | 同一 `event_id` 幂等,失败后 retry |
| super gift | 未达阈值不播报,达阈值发送到用户房间所在区域群 | | super gift | 未达阈值不播报,达阈值发送到用户房间所在区域群 |
| room rocket | 火箭燃料满后只按 `RoomRocketIgnited` 播报一次;区域/全局 scope 来自后台配置 | | room treasure | 宝箱满能量后只按 `RoomTreasureCountdownStarted` 播报一次;区域/全局 scope 来自后台配置 |
| red packet | IM payload 不影响领取接口的后端金额校验 | | red packet | IM payload 不影响领取接口的后端金额校验 |

View File

@ -1,366 +0,0 @@
# Outbox 统一治理说明
更新时间2026-07-11
## 背景
生产 MySQL CPU 打满的直接触发点是多个 worker 高频轮询 outbox 表,尤其是 `wallet_outbox` 数据量已经到百万级后,部分查询没有完整匹配索引、带 `OR` 分支、缺少 `app_code` 过滤或由下游服务直扫 owner 表,导致 MySQL 需要持续扫描大量历史 outbox 行。
这次治理目标是:
- owner service 只允许自己扫描自己的 outbox并发布 MQ。
- 下游 projection 不再直接扫描 owner outbox。
- 所有 outbox claim 查询必须带 `app_code + status/event_type + retry/lock + created_at` 匹配索引。
- 所有 `OR` claim 分支拆成独立查询。
- `delivered/done` 历史 outbox 建立归档机制。
## 修改前
### 运行方式
- `wallet-service``wallet_outbox` 发布 `hyapp_wallet_outbox` MQ。
- `room-service``room_outbox` 发布 `hyapp_room_outbox` MQ。
- `activity-service``game-service``user-service` 也各自有 outbox worker。
- `notice-service` 仍保留直扫 `wallet_outbox``room_outbox` 的历史 worker 能力。
- `wallet-service` 内部的礼物墙和 badge projection worker 也在轮询扫描 `wallet_outbox`
- 部分 claim SQL 把 `pending/retryable/locked expired` 写在同一个 `OR` 条件里。
- 部分 outbox 索引缺少 `app_code``next_retry_at_ms``lock_until_ms/locked_until_ms``event_id` 这样的匹配列。
- `game_outbox``user_outbox` 缺少完整的 worker lock/retry 字段。
- 历史 `delivered/done` 数据没有统一归档脚本。
### 主要风险
- outbox 表越大,高频轮询越容易退化成大范围扫描。
- 下游服务直接读 owner outbox会把 owner 表变成跨服务共享查询热点。
- `OR` 查询很容易让优化器放弃最优索引路径。
- delivered 历史数据留在热表中,会持续抬高索引体积和扫描成本。
- projection、通知、MQ 发布混用 outbox 状态,后续排障边界不清晰。
## 修改后
### 运行方式
- owner service 继续负责自己的 outbox claim 和 MQ 发布:
- `wallet-service` 发布 `hyapp_wallet_outbox`
- `room-service` 发布 `hyapp_room_outbox`
- `lucky-gift-service` 在钱包返奖和本地 granted 收敛后发布 `hyapp_lucky_gift_outbox`
- `activity-service` 发布 activity / broadcast 相关 MQ
- `game-service` 发布 game 相关 MQ
- `user-service` 发布 `hyapp_user_outbox`
- `notice-service` 不再启动直扫 `wallet_outbox` / `room_outbox` 的 worker改为消费 MQ。
- `wallet-service` 内部礼物墙和 badge projection 不再轮询扫描 `wallet_outbox`,改为消费 `hyapp_wallet_outbox` MQ。
- claim 查询拆成多段:
- `pending`
- `retryable`
- `delivering/running expired lock`
- claim 查询全部显式带 `app_code`,并匹配对应索引。
- delivered/done 历史 outbox 通过归档脚本搬到 archive 表。
- wallet/room topic 使用可灰度的 typed Tagconsumer 先部署
`legacy || relevant event_type`producer 再从 `legacy` 切到 `event_type`
- 同步 MQ publisher 每次只 claim 一条 outbox`batch_size` 表示单轮最多处理
条数,不再表示一次提前锁住的行数,避免批尾 lease 在真正发送前过期。
## Typed Tag 无损切换
Typed Tag 只降低无关 consumer group 的投递、反序列化和 handler 调用量,不会
减少 Broker ingress 或存储。礼物消息总量必须在写入 owner outbox 前通过送礼微批
收敛;多人礼物仍按 target 保留各自 `WalletGiftDebited``RoomGiftSent` 事实,且
每条事实携带聚合后的 `gift_count`
切换顺序必须严格执行:
1. 先滚动发布所有 wallet consumer确认订阅表达式包含
`wallet_outbox_event || <相关 event_type>`,且同一 group 所有副本表达式一致。
2. 确认旧 consumer 实例归零、消费错误率稳定,再仅把 wallet-service
`rocketmq.wallet_event_tag_mode``legacy` 改为 `event_type`
3. 保留 consumer 的 legacy 分支直到切换前积压、retry 和计划内 replay 窗口全部
排空;不能在 producer 切换同一批发布中移除 legacy。
4. wallet 稳定后,以同样顺序先发布 activity/statistics/notice/user 等 room
consumer再把 room-service `rocketmq.room_event_tag_mode``legacy` 改为
`event_type`
5. 如需回滚 producer只改回 `legacy`;兼容 consumer 无需回滚。只有确认 Broker
已无 legacy 消息且不再重放旧 outbox 后,才可以另行删除 legacy selector。
推荐验证:
```text
Phase A: producer tag=legacy每个 consumer group 仍能消费原有事实
Phase B: gift typed tag 只进入需要 gift 的 group首充/累充/邀请等 group lag 不再随礼物增长
Rollback: producer 改回 legacy 后所有兼容 consumer 继续消费,无消息不可见窗口
```
Topic 拆分不是降低消息总量的手段。完成微批和 typed Tag 后,只有在礼物流量仍
持续挤压非礼物 SLO或确实需要独立 retention/retry/ACL/顺序策略时才拆高频 Topic
流量占比只能作为评审信号,不能把固定百分比当作行业标准。
### 历史礼物消费修复
新代码只阻止新增的 `WalletBalanceChanged(direct_gift_debit)``RoomGiftSent`
双计;已经落入 `stat_app_day_country.consumed_coin` 的历史窗口不能直接拍脑袋减值。
上线后使用现有 `replay-stat-tz` 从四个 owner outbox 重建受影响窗口,并分别执行
UTC 与 Asia/Shanghai 两套口径。先不带 `--apply` 核对 source_rows再在停止该
窗口实时写入或只选择已完成自然日时执行 apply
```bash
go run ./services/statistics-service/cmd/replay-stat-tz \
--app lalu --timezone UTC --start-day 2026-07-01 --end-day 2026-07-10
go run ./services/statistics-service/cmd/replay-stat-tz \
--app lalu --timezone Asia/Shanghai --start-day 2026-07-01 --end-day 2026-07-10
```
确认四类 owner source_rows 非零且窗口正确后,为同一命令补充 `--apply` 和运行时
DSN 环境变量。每个 App 独立执行;完成后校验 `consumed_coin` 中礼物部分等于
`RoomGiftSent.coin_spent`,钱包礼物扣款只继续影响 `coin_total`
## 修复掉的问题
### 1. 下游直扫 owner outbox
之前:
- `notice-service` 可以直接扫描 `wallet_outbox``room_outbox`
- `wallet-service` 内部 projection worker 轮询扫描 `wallet_outbox` 做礼物墙和 badge 投影。
现在:
- `notice-service` runtime 配置禁止直扫 wallet/room outbox只消费 MQ。
- `wallet-service` 的礼物墙和 badge projection 改为消费 `hyapp_wallet_outbox` MQ。
- 生产验证中,`wallet_projection_events / idx_wallet_outbox_event_created / idx_wallet_outbox_asset_event_created` 相关直扫进程数为 `0`
实现位置:
- `services/notice-service/internal/config/config.go`
- `services/notice-service/internal/app/app.go`
- `services/notice-service/internal/modules/walletnotice/service.go`
- `services/notice-service/internal/modules/walletnotice/mysql_repository.go`
- `services/wallet-service/internal/app/app.go`
- `services/wallet-service/internal/service/wallet/service.go`
- `services/wallet-service/internal/storage/mysql/gift_wall_projection.go`
- `services/wallet-service/internal/storage/mysql/badge_projection_relay.go`
### 2. claim 查询中 `OR` 导致索引不可控
之前:
- 多个 outbox claim SQL 用一个查询覆盖 pending、retryable、锁过期重试。
- SQL 里有 `OR` 分支,优化器在大表上不稳定,容易扫描过多行。
现在:
- claim 查询按状态拆成独立分支。
- 每个分支只走自己的索引条件。
- 必要位置使用 `FORCE INDEX` 固定索引路径。
实现位置:
- `services/wallet-service/internal/storage/mysql/repository.go`
- `services/room-service/internal/storage/mysql/repository.go`
- `services/activity-service/internal/storage/mysql/broadcast_repository.go`
- `services/game-service/internal/storage/mysql/repository.go`
- `services/user-service/internal/storage/mysql/user_outbox.go`
### 3. 缺少 app_code 和匹配索引
之前:
- 部分 claim 查询没有完整 `app_code + status + retry/lock + created_at` 索引。
- 部分索引没有 `event_id` 尾列,排序和稳定分页不够完整。
现在:
- wallet、room、activity、game、user、notice 相关 outbox 都补了 claim/retention 索引。
- 生产库已验证目标索引存在,关键 `EXPLAIN` 都命中指定索引。
关键索引形态:
```sql
app_code, status, next_retry_at_ms, created_at_ms, event_id
app_code, status, lock_until_ms, created_at_ms, event_id
app_code, status, updated_at_ms, event_id
```
部分表字段名不同:
- `im_broadcast_outbox` 使用 `locked_until_ms`
- `notice_delivery_events` 使用 `source_name, app_code, channel, status, next_retry_at_ms/lock_until_ms, updated_at_ms`
### 4. game/user outbox 缺少标准锁字段
之前:
- `game_outbox``user_outbox` 没有完整的 `worker_id``lock_until_ms``retry_count``next_retry_at_ms``last_error` 字段。
- worker 失败和过期接管能力不统一。
现在:
- 两张表都补齐标准 worker claim 字段。
- claim 逻辑统一为 pending/retryable/running expired 三段。
实现位置:
- `services/game-service/internal/storage/mysql/repository.go`
- `services/user-service/internal/storage/mysql/user_outbox.go`
- `services/game-service/internal/storage/mysql/repository.go` 的 schema/migrate 逻辑
- `services/user-service/deploy/mysql/initdb/001_user_service.sql`
### 5. delivered/done 热表历史数据无归档
之前:
- delivered/done 历史 outbox 一直留在业务热表。
- 表越大,索引越大,后续即使查询优化了也会逐步增加维护成本。
现在:
- 新增统一归档脚本:
`scripts/mysql/archive_delivered_outbox.sql`
- 归档目标是同库 archive 表:
- `hyapp_wallet.wallet_outbox_archive`
- `hyapp_room.room_outbox_archive`
- `hyapp_activity.activity_outbox_archive`
- `hyapp_activity.im_broadcast_outbox_archive`
- `hyapp_game.game_outbox_archive`
- `hyapp_game.game_level_event_outbox_archive`
- `hyapp_user.user_outbox_archive`
- `hyapp_notice.notice_delivery_events_archive`
## 归档方式
归档脚本逻辑:
1. 默认归档 30 天以前的数据:
```sql
SET @outbox_archive_cutoff_ms := IFNULL(
@outbox_archive_cutoff_ms,
UNIX_TIMESTAMP(DATE_SUB(UTC_TIMESTAMP(3), INTERVAL 30 DAY)) * 1000
);
```
2. 每张表先创建同结构 archive 表:
```sql
CREATE TABLE IF NOT EXISTS hyapp_wallet.wallet_outbox_archive LIKE hyapp_wallet.wallet_outbox;
```
3. 先 `INSERT IGNORE` 到 archive 表:
```sql
INSERT IGNORE INTO hyapp_wallet.wallet_outbox_archive
SELECT * FROM hyapp_wallet.wallet_outbox
WHERE status IN ('delivered', 'done') AND updated_at_ms < @outbox_archive_cutoff_ms
ORDER BY updated_at_ms ASC
LIMIT 10000;
```
4. 确认 archive 表存在对应记录后,再删除源表:
```sql
DELETE FROM hyapp_wallet.wallet_outbox
WHERE status IN ('delivered', 'done') AND updated_at_ms < @outbox_archive_cutoff_ms
AND EXISTS (
SELECT 1 FROM hyapp_wallet.wallet_outbox_archive a
WHERE a.app_code = wallet_outbox.app_code AND a.event_id = wallet_outbox.event_id
)
ORDER BY updated_at_ms ASC
LIMIT 10000;
```
每张表每次最多处理 `10000` 行,避免一次归档造成长事务和复制压力。
生产执行结果:
- 脚本已执行。
- 本次 30 天前 `delivered/done` 候选行为 `0`
- archive 表已通过脚本创建。
## 本次提交
- `60b7277 fix: govern outbox polling and archiving`
- `bd0ed78 fix: add game outbox retention index`
- `0aef70e fix: consume wallet projections from mq`
生产部署:
- `wallet-service`: `main-0aef70e-outbox-governance`
- `room-service/activity-service/game-service/user-service/notice-service`: `main-bd0ed78-outbox-governance`
## 当前方案的优点
- 明确服务边界owner outbox 只由 owner service 扫描和发布 MQ。
- 降低 MySQL CPU 风险:下游 projection 不再对 owner 大表做高频轮询。
- 查询路径稳定claim SQL 拆分后,索引选择更可控。
- 支持多实例并发:继续使用 lock 字段和 `FOR UPDATE SKIP LOCKED`
- 支持失败重试:通过 `retryable``next_retry_at_ms``lock_until_ms` 做指数退避和过期接管。
- 历史数据可控delivered/done 可以按批归档,热表不会无限增长。
- 保留幂等语义:下游消费通过各自投影表或 delivery 表做去重,不依赖 MQ exactly-once。
## 当前方案的缺点
- MQ 成为关键依赖MQ 故障时,下游 projection 会延迟。
- 归档现在是脚本,不是定时任务;需要接入 cron 或运维调度。
- 仍保留部分离线/测试用直扫方法,虽然 runtime 不启动,但后续要避免误开配置。
- range 查询因为 `next_retry_at_ms <= now``lock_until_ms <= now``EXPLAIN` 里仍可能出现 `Using filesort`;目前扫描行数已被前缀索引压住,但极端积压下仍要关注。
- MQ message 只覆盖被成功发布后的事件;如果历史 projection 已有缺口,需要单独跑一次离线修复或重放。
- archive 表目前与源表同库同实例,能降低热表成本,但不能降低整个 MySQL 实例总存储压力。
## 还需要补足
### 1. 固化归档调度
建议把 `scripts/mysql/archive_delivered_outbox.sql` 接入定时任务:
- 每天或每小时执行一次。
- 每次每表限制 `10000` 行。
- 归档窗口默认 30 天,可通过 `@outbox_archive_cutoff_ms` 覆盖。
- 执行前后记录每张表归档行数。
### 2. 增加 outbox 指标和告警
建议增加以下指标:
- 每张 outbox 各状态数量:`pending/retryable/delivering/running/delivered/dead`
- 最老 pending 时间
- retryable 积压数量
- dead 数量
- MQ 发布失败数
- MQ 消费失败数
- projection duplicate / skipped / failed 数量
### 3. 增加配置保护
现在 notice 已经禁止 runtime 直扫 wallet/room outbox。后续建议继续收紧
- 对所有下游服务保留的 legacy direct scan worker 增加启动保护。
- 生产配置中直接删除或禁用直扫配置。
- CI 增加 SQL lint检查 outbox claim 是否缺 `app_code` 或含 `OR`
### 4. 历史 projection 缺口修复
对于已经 `delivered` 但下游 projection 没处理到的历史事件,建议单独做一次离线 backfill
- 从 owner outbox 按时间窗口导出缺口事件。
- 写入对应 projection/delivery 幂等表。
- 或重新投递 MQ但要控制速率。
### 5. 更彻底的数据生命周期治理
中长期可以考虑:
- outbox 热表只保留未完成和近期完成数据。
- delivered/done 进入 archive 库或对象存储,而不是同库 archive 表。
- 按 `created_at_ms``updated_at_ms` 做分区表,归档时按分区迁移或 drop partition。
- 大流量 outbox 拆成 pending 表和 history 表,避免热冷数据混在同一张表。
## 最终状态
当前生产已验证:
- 目标服务均健康。
- outbox claim 索引已存在且 `EXPLAIN` 命中指定索引。
- notice 不再直扫 wallet/room outbox。
- wallet 内部 projection 不再轮询扫描 `wallet_outbox`
- MySQL processlist 中 wallet projection 相关 outbox 直扫检查结果为 `0`

View File

@ -1,194 +1,462 @@
# 可配置 VIP 系统 # VIP 系统架构设计
## 目标与边界 ## 目标
VIP 是 `wallet-service``app_code` 隔离的会员与权益域。每个 App 通过一份 `vip_program_configs` 选择等级数量、购买有效期策略、后台发放模式和体验卡开关gateway、room、admin 和客户端只能消费这份显式配置,不得通过 `app_code` 写第二套状态机 VIP 是用户可直接用金币购买的限时会员权益。每个 VIP 等级都可以单独购买,单次购买有效期固定 7 天,不同等级价格不同。购买成功后,系统根据后台配置的资源组发放头像框、座驾、气泡、徽章、礼物等权益
- `wallet-service`:会员、体验卡、用户功能开关、权益矩阵、购买扣费、每日返现、资源发放、历史和 outbox 的 owner。 首版只做当前业务真实需要的能力:
- `gateway-service`App HTTP 鉴权、协议转换和钱包 gRPC 编排。
- `room-service`:只执行房间权限并通过腾讯云 IM 投递当前房间事件,不能拥有会员状态。
- `server/admin`:配置 program、等级和每级完整权益发放真实 VIP 或体验卡。
- Flutter展示后端状态、操作背包佩戴并处理客户端生命周期效果不参与购买规则计算。
全链路业务时间使用 UTC epoch milliseconds。每日结算或刷新按 UTC 0 点切日;用户本地时间只用于展示。 - 用户可以购买任意有效 VIP 等级。
- 用户可以从低等级升级到高等级。
- 用户当前 VIP 未过期时,不能购买比当前等级低的 VIP。
- 同等级重复购买按续期处理。
- VIP 扣金币、会员状态变更、资源组发放必须在 `wallet-service` 内形成一致账务闭环。
- 后台只配置 VIP 等级、金币价格、有效期和赠送资源组;客户端不能传价格或资源明细。
## 当前 App 策略 ## 服务边界
| 配置 | Lalu | Fami | VIP 属于 `wallet-service` 的账务和权益域,不放在 `user-service`
| --- | --- | --- |
| `program_type` | `legacy_timed` | `tiered_privilege_v1` |
| `level_count` | 10 | 9 |
| 同级购买 | 从当前截止时间续期 | 从当前截止时间续期 |
| 高级升级 | 保留剩余时间后追加新时长 | 立即替换为高级,从购买时刻重新计时,旧等级剩余时间丢弃 |
| 低级购买 | 拒绝 | 拒绝 |
| 后台发放 | `direct_membership`,直接写真实 VIP | `trial_card`,发到背包后由用户佩戴 |
| 体验卡 | 关闭 | 开启 |
| 权益集合 | 每级显式保存 | 每级显式保存完整集合,不按 `unlock_level` 自动继承 |
Fami 的 VIP1VIP9 初始全部为 `disabled`。30 天只是不产生零时长脏配置的占位值;价格和资源组保持 0运营完成配置并启用前不可购买或发卡。 原因:
## 数据模型 - 购买 VIP 必须先扣金币,扣费成功才允许生成 VIP 状态和权益。
- 购买 VIP 会发放后台资源组,资源组、资源权益和钱包资产已经归 `wallet-service` 管。
- 如果 VIP 状态放在 `user-service`,扣费、会员状态、资源发放会跨服务,需要分布式事务或补偿,复杂度和失败窗口都更大。
### App 级 program 服务职责:
`vip_program_configs` 是行为规则的唯一来源: - `gateway-service`:提供 App HTTP API做鉴权、参数校验、生成 `request_id`、透传客户端 `command_id`,调用 `wallet-service`
- `wallet-service`:拥有 VIP 等级配置、用户 VIP 状态、VIP 购买订单、金币扣费、资源组发放、VIP 事件 outbox。
- `user-service`:只在用户资料聚合时读取 VIP 摘要,不拥有 VIP 状态。
- `room-service`:需要展示 VIP 标识时读取用户资料/VIP 投影,不参与 VIP 购买主链路。
- `server/admin`:后台配置 VIP 等级和资源组,具体后台接口单独设计。
- `program_type` ## 核心模型
- `level_count`
- `same_level_expiry_policy`
- `upgrade_expiry_policy`
- `downgrade_purchase_policy`
- `benefit_inheritance_policy`,当前固定 `target_only`
- `grant_mode`
- `trial_card_enabled`
- `status`
- `config_version`
后台修改 program、等级或权益时递增 `config_version`。会员、订单和体验卡保留创建时的 program/version 快照,当前权限判断使用最新有效 program 和等级显式权益集合。 ### VIP 等级
### 等级和权益 `vip_levels`
- `vip_levels`:等级名称、状态、金币价格、时长、可选资源组和排序。 | 字段 | 说明 |
- `vip_level_benefits`:以 `(app_code, level, benefit_code)` 保存该等级最终完整权益集合。 | -------------------------- | -------------------------------------------------- |
| `id` | 内部主键 |
| `level` | VIP 等级,数字越大等级越高 |
| `name` | 展示名称,例如 VIP1、VIP2 |
| `coin_price` | 购买该等级需要消耗的金币数 |
| `duration_ms` | 有效期,首版固定 `7 * 24 * 60 * 60 * 1000` |
| `reward_resource_group_id` | 购买成功后发放的资源组 |
| `status` | `draft` / `active` / `disabled` |
| `sort_order` | App 展示排序 |
| `display_config_json` | 展示配置快照,例如颜色、权益说明、角标,不参与结算 |
| `created_at_ms` | 创建时间 |
| `updated_at_ms` | 更新时间 |
`unlock_level` 只记录 P1 初始化来源,运行时和后台保存都不能据此给高等级自动补权益。纯功能权益的 `resource_id=0`;装扮权益可以绑定同 App 的实际资源。 约束:
### 付费会员 - `level` 唯一。
- `coin_price > 0`
- `duration_ms` 首版必须等于 7 天,不允许后台临时改成其它值。
- `active` 状态必须绑定有效资源组。
`user_vip_memberships` 保存用户付费会员事实。有效条件是 `level > 0 && expires_at_ms > now_ms`;不依赖异步过期任务才能失效。 ### 用户 VIP 当前状态
购买事务同时完成: `user_vip_memberships`
1. 锁定 program、目标等级和当前会员 | 字段 | 说明 |
2. 根据 program 计算有效期,拒绝降级; | --------------- | ---------------------------- |
3. 锁定 COIN 账户并扣费; | `user_id` | 用户 ID唯一 |
4. 发放目标资源组; | `level` | 当前等级 |
5. 更新会员,写订单、历史和 wallet outbox | `status` | `active` / `expired` |
6. 一次事务提交。 | `started_at_ms` | 当前等级周期开始时间 |
| `expires_at_ms` | 当前 VIP 到期时间 |
| `last_order_id` | 最近一次购买订单 |
| `version` | 乐观版本,配合行锁防并发覆盖 |
| `created_at_ms` | 创建时间 |
| `updated_at_ms` | 更新时间 |
`command_id` 是购买幂等键,`request_id` 仅用于链路追踪。 读取时以 `expires_at_ms > now` 判断是否有效,`status` 只作为后台检索和事件处理的辅助状态。这样即使过期任务延迟App 也不会把过期 VIP 当成有效权益
`vip_command_locks` 在任何会员、账户或资源业务锁之前串行化 `(app_code, command_id)`。购买、直接会员赠送和体验卡发放共享该守卫空间;同命令并发重试会等待原事务提交后读取稳定回执,三种业务也不能交叉复用同一命令号。 ### VIP 购买订单
### 体验卡 `vip_purchase_orders`
体验卡使用三层事实: | 字段 | 说明 |
| ---------------------------- | --------------------------------- |
| `order_id` | VIP 订单号 |
| `command_id` | 客户端生成的幂等命令 ID |
| `user_id` | 购买用户 |
| `target_level` | 目标 VIP 等级 |
| `coin_price_snapshot` | 下单时价格快照 |
| `duration_ms_snapshot` | 下单时有效期快照 |
| `resource_group_id_snapshot` | 下单时资源组快照 |
| `previous_level` | 购买前有效等级,无有效 VIP 则为 0 |
| `previous_expires_at_ms` | 购买前到期时间 |
| `new_level` | 购买后等级 |
| `new_expires_at_ms` | 购买后到期时间 |
| `wallet_transaction_id` | 金币扣费交易 ID |
| `resource_grant_id` | 资源组发放记录 ID |
| `status` | `succeeded` / `failed` |
| `failure_reason` | 失败原因 |
| `request_hash` | 幂等请求摘要 |
| `created_at_ms` | 创建时间 |
| `updated_at_ms` | 更新时间 |
- `resources(resource_type=vip_trial_card)`:卡面与等级元数据; 约束:
- `user_resource_entitlements`:背包实例和绝对有效期;
- `user_resource_equipment`:当前单选佩戴指针;
- `user_vip_trial_cards`:等级、来源、时长和 program/version 快照。
发卡时即写入 `effective_at_ms``expires_at_ms`,但不自动佩戴。切换卡片只替换 `user_resource_equipment` 指针,不修改、删除、暂停或延长任一卡片。 - `(user_id, command_id)` 唯一。
- 相同 `command_id` + 相同 `request_hash` 返回原订单结果。
- 相同 `command_id` + 不同 `request_hash` 返回幂等冲突。
例如 A=14 天、B=20 天、C=30 天:佩戴 A 后立刻切 BB 生效A 仍保留原绝对到期时间;之后只要 A 未过期即可重新佩戴。体验卡等级不受当前付费 VIP 或上一张体验卡等级限制。 ### VIP 变更历史
### 用户功能开关 `user_vip_history`
`user_vip_settings``(app_code,user_id)` 保存 5 个用户偏好: 记录每次购买、续期、升级、过期,供客服、后台账单、风控排查使用。历史表不参与 App 主链路强查询。
- `room_entry_notice_enabled`:进房 VIP 高亮通知; 核心字段:
- `online_global_notice_enabled`:上线 VIP 全服飘屏;
- `hide_profile_data_enabled`:隐藏个人数据;
- `anonymous_profile_visit_enabled`:匿名访问主页;
- `leaderboard_invisible_enabled`:榜单隐身。
无记录时 5 项默认为 `true`,首次修改才写行。偏好不授予权益:无 VIP 或对应等级没有权益时,即使开关为开也不会放行。用户可在无 VIP、VIP 过期或切换体验卡时保留偏好Fami 与 Lalu 互不影响。 - `history_id`
- `user_id`
- `event_type`: `purchase` / `renew` / `upgrade` / `expire`
- `from_level`
- `to_level`
- `from_expires_at_ms`
- `to_expires_at_ms`
- `order_id`
- `created_at_ms`
### 权益展示投影 ## 购买和升级规则
`vip_level_benefits.metadata_json.presentation` 是后台保存的扩展配置wallet 在读取时解析并校验,跨服务和 HTTP 统一投影为结构化 `presentation`。它包含说明文案、多项预览、`image/animation` 媒体类型、`none/user_avatar_center/user_avatar_waveform` 合成方式,以及 `once/daily/monthly` 数值奖励。Flutter 只消费结构化字段,不按 `benefit_code` 解释原始 JSON未知扩展因此不会变成客户端隐形配置表。 定义:
### 每日金币返现 - 当前有效 VIP`membership.expires_at_ms > now && membership.level > 0`
- 目标等级:用户本次购买的 `vip_level`
`daily_coin_rebate``metadata_json.coin_amount` 是该等级每日可领金币正整数。只有启用的付费 VIP 参与,体验卡即使历史配置误开 `trial_enabled` 也会被 wallet 二次排除。 规则:
每个 App 每个 UTC 日期只有一个 `vip_daily_coin_rebate_runs` | 场景 | 处理 |
| ---------------------- | ------------------------------------------------------------------------ |
| 无有效 VIP | 可以购买任意 active 等级,`expires_at_ms = now_ms + 7d_ms` |
| 有效 VIP购买同等级 | 续期,`expires_at_ms = current_expires_at_ms + 7d_ms` |
| 有效 VIP购买更高等级 | 立即升级,`level = target_level``expires_at_ms = current_expires_at_ms + 7d_ms` |
| 有效 VIP购买更低等级 | 拒绝,返回 `VIP_DOWNGRADE_NOT_ALLOWED` |
| VIP 已过期 | 按无有效 VIP 处理,可以购买任意 active 等级 |
1. 首个成功批次固化当时的 `config_version` 和等级金额矩阵;后续分页不重读新配置。 首版升级采用“保留剩余时间并升级”的策略:如果用户 VIP1 还剩 3 天,又购买 VIP3则 VIP3 到期时间为当前 VIP1 到期时间再加 7 天。这个规则实现简单、用户感知清晰,也避免升级时损失已付费时长。
2. wallet 根据会员历史还原 UTC 0 点时的付费 VIP0 点后才购买或升级的状态从次日起参与。
3. 生成 `vip_daily_coin_rebates` 资格,领取窗口固定为 `[UTC 当日0点,UTC 次日0点)`
4. 同事务写 `VipDailyCoinRebateAvailable` outboxactivity-service 幂等投影为“VIP金币返现”系统消息。
5. 用户手动领取时返现行锁、COIN 余额、交易、分录和 `WalletBalanceChanged` outbox 在一个事务内提交。
“日切资格”严格按 UTC 0 点;“金额配置”是当日首个成功 run 的快照,不伪造系统尚未保存的 0 点历史配置。正常情况下 cron 每分钟触发;如零点后 wallet/cron 长时间不可用,恢复前的后台金额修改会进入当日快照。运营若要保证修改次日生效,应在当日 run 已创建后修改 如果后续业务希望“补差价升级”或“升级只生效 7 天”,只需要扩展 VIP 等级的升级策略,不改变主链路表结构。
## 最终生效状态 ## 资源组发放规则
所有调用方统一消费 `VipState` 购买 VIP 成功后,`wallet-service` 根据 `vip_levels.reward_resource_group_id` 发放资源组。
- `paid_vip`:真实购买会员; 资源组继续复用现有资源组能力:
- `equipped_trial_card`:当前有效且已佩戴的体验卡;
- `effective_vip`:最终展示和权限使用的 VIP
- `effective_source``paid``trial``none`
- `effective_benefits`:最终身份拥有的权益资格;可关闭的通知类权益还要与 `user_settings` 做 AND
- `evaluated_at_ms`:本次状态合并的服务端时间;
- `program_config`:当前 App 规则;
- `user_settings`:当前 App 下完整的 5 项功能偏好。
`GET /vip/me``GET /vip/packages`、购买、佩戴体验卡和卸下体验卡返回完全同构的 `VipState`。写接口成功响应不是 patchFlutter 必须原子替换 store。灰度期间如旧节点返回缺字段状态客户端应丢弃该局部对象并重拉 `/vip/me` - 头像框:`avatar_frame`
- 资料卡:`profile_card`
- 座驾:`vehicle`
- 聊天气泡:`chat_bubble`
- 徽章:`badge`
- 飘屏:`floating_screen`
- 礼物:`gift`
- 金币等钱包资产:仅在明确配置时发放,默认不建议 VIP 购买返金币,避免账务绕圈。
program 启用时的优先级固定为:`trial_card_enabled=true` 且体验卡有效并已佩戴时覆盖展示;否则使用有效付费会员;二者都无效则为 `none`。关闭体验卡开关只停止它参与最终权限合并,背包和佩戴原始事实仍保留;关闭整个 program 时付费与卡片原始事实仍可查询,但最终身份为 `none`、不下发权益。 VIP 资源组需要增加一个明确约束:
钱包只读取 `effective_vip.level` 对应的显式权益行。来源为 `trial` 时,再移除 `trial_enabled=false` 的权益;当前 `daily_coin_rebate` 不对体验卡开放。owner service 必须按 `benefit_code` 判断,不能改回 `level >= N`;进房和上线通知的执行方必须使用 wallet 已合并用户开关的结果。 - VIP 专属装扮类资源默认与 VIP 到期时间对齐。
- 资源组 item 如果配置了独立 `duration_ms`,后台必须能看到它和 VIP 7 天周期的差异。
- 首版推荐新增 `duration_policy = inherit_vip_expiry`,购买 VIP 时将资源权益到期时间直接设置为 `new_expires_at_ms`
## P1 权益编码 续期和升级时:
| 等级 | 本级新增权益 | - 同一资源重复发放时,使用资源权益的 `extend_expiry``align_to_expiry` 策略,避免重复插入导致用户出现多个同资源有效权益。
| --- | --- | - 升级后只发放目标等级配置的资源组;低等级已获得的资源不立即删除,到期后自然失效。
| VIP1 | `vip_badge_identity``vip_medal``avatar_frame``vip_title` | - App 展示和自动佩戴时按用户当前有效资源、资源等级和佩戴状态决定,不从 VIP 等级反推资源。
| VIP2 | `chat_bubble``vip_gift``entry_effect``visitor_history` |
| VIP3 | `voice_wave``profile_card``custom_room_background``room_image_message` |
| VIP4 | `animated_avatar``animated_room_cover``mic_skin``room_border``vehicle` |
| VIP5 | `colored_room_name``colored_nickname``colored_id``gift_tray_skin` |
| VIP6 | `hide_profile_data``custom_avatar_frame``leaderboard_invisible``daily_coin_rebate` |
| VIP7 | `custom_profile_card``anonymous_profile_visit``custom_gift` |
| VIP8 | `room_entry_notice``custom_pretty_id``custom_vehicle` |
| VIP9 | `anti_kick``anti_mute``online_global_notice` |
初始化时高等级行已展开为完整集合,但这是 seed 结果,不是运行时继承规则。后台可以显式移除任一等级的某项权益。 ## 主链路流程
## 房间权限和 IM ```mermaid
sequenceDiagram
autonumber
participant App
participant Gateway as gateway-service
participant Wallet as wallet-service
participant DB as Wallet MySQL
participant Outbox as Wallet Outbox
`room-service` 在需要房间权限时读取 wallet 的 `GetMyVip.state` App->>Gateway: POST /api/v1/vip/purchase {level, command_id}
Gateway->>Wallet: PurchaseVip(user_id, level, command_id, request_meta)
Wallet->>DB: BEGIN
Wallet->>DB: 查询并锁定 vip_levels(level)
Wallet->>DB: 查询并锁定 user_vip_memberships(user_id)
Wallet->>DB: 校验不能降级、计算 new_expires_at_ms
Wallet->>DB: 锁定用户 COIN 钱包账户
Wallet->>DB: 扣减金币,写 wallet_transaction / wallet_entries
Wallet->>DB: 发放 VIP 资源组,写 resource_entitlements
Wallet->>DB: upsert user_vip_memberships
Wallet->>DB: 写 vip_purchase_orders / user_vip_history
Wallet->>Outbox: 写 VipPurchased / VipUpgraded / VipRenewed
Wallet->>DB: COMMIT
Wallet-->>Gateway: 返回当前 VIP、扣费、权益摘要
Gateway-->>App: {code,message,request_id,data}
```
- `custom_room_background`P1 program 的房主保存或切换自定义背景前强校验Lalu 保持原逻辑。 关键点:
- `anti_kick`:普通房主/管理员踢人时拒绝操作。
- `anti_mute`:普通房主/管理员新增禁言时拒绝操作。
- 平台封禁、风控、超级管理员使用独立系统治理入口,不经过上述普通房管守卫。
- `room_entry_notice`:权益有效且用户开关开启时,随 `JoinRoomResponse``RoomUserJoined` 和腾讯云 IM 当前房间系统消息携带;只在用户进入的当前房间展示,不做全服广播。
`online_global_notice``POST /vip/online-notice` 触发gateway 先调用 wallet `CheckVipBenefit`,同时校验权益资格和用户开关;然后从 user-service 取服务端用户资料,最后写 activity-service 全局播报 outbox。腾讯云 IM 使用 `TIMCustomElem``Ext=im_broadcast``Desc=vip_online_notice``broadcast_type=vip_online_notice``scope=global`。Flutter 在进程内按 UTC 日控制触发:同一进程每天最多触发一次,不持久化该标记;杀进程重启后允许再次触发。服务端不做用户日级去重,但相同 `command_id` 的 HTTP 重试映射到同一 `event_id`。Flutter 不能直接向全服群发消息。 - 金币扣费、VIP 状态、资源组发放必须在同一个 `wallet-service` 数据库事务中完成。
- 客户端只传 `level``command_id`,价格和资源组完全由服务端配置决定。
- 扣费成功但资源发放失败不能提交事务;否则用户会付费但拿不到权益。
- 资源发放成功但 VIP 状态更新失败也不能提交事务;否则权益和会员状态会不一致。
## App HTTP API ## App 一级接口
统一前缀 `/api/v1`,响应使用 `{code,message,request_id,data}` ### 查询 VIP 等级
- `GET /vip/packages`program、可购买等级、每级权益和完整 state。 `GET /api/v1/vip/levels`
- `GET /vip/me`:付费、体验卡、最终 VIP、权益资格和用户功能开关。
- `POST /vip/purchase`:请求 `{command_id,level}`;价格、时长和资源由服务端读取;响应 `coin_balance` 是带 `version` 的账后快照,旧 `coin_balance_after` 仅兼容历史客户端。
- `GET|PATCH /vip/settings`:读取或局部修改 5 项 VIP 功能偏好;空 PATCH 拒绝。
- `POST /vip/online-notice`:请求 `{command_id}`,服务端校验权益、开关和资料后写全服播报 outbox。
- `GET /vip/coin-rebates/current`:返回当前 UTC 日返现资格;无资格或 cron 尚未生成时 `found=false`
- `POST /vip/coin-rebates/statuses`:按最多 100 个 `rebate_ids` 恢复系统消息真实状态,或用受限分页查历史。
- `POST /vip/coin-rebates/{rebate_id}/claim`:请求 `{command_id}`,金额、归属、过期和入账全由 wallet 裁决。
- `GET /users/me/resources?resource_type=vip_trial_card&active_only=true`:查询背包体验卡资源实例。
- `POST /vip/trial-cards/{entitlement_id}/equip`:佩戴指定背包实例。
- `DELETE /vip/trial-cards/equipped`:卸下当前体验卡。
- 历史通用背包装备/卸下接口命中 `vip_trial_card` 时由 wallet service 委托上述专用状态机禁止仓储层直接改装备槽Flutter VIP 流程仍使用专用接口以一次取得完整 `VipState`
- `POST /manager-center/vip-grants`gateway 按当前 program 自动选择直接会员或 30 天体验卡发放Fami 发卡不自动佩戴。
后台接口 返回:
- `GET|PUT /v1/admin/activity/vip-program` ```json
- `GET|PUT /v1/admin/activity/vip-levels` {
- `POST /v1/admin/activity/vip-trial-card-grants` "current_vip": {
- `POST /v1/admin/activity/vip-grants`,仅用于 `direct_membership` program "level": 2,
"name": "VIP2",
"expires_at_ms": 1760000000000,
"remaining_ms": 604800000
},
"levels": [
{
"level": 1,
"name": "VIP1",
"coin_price": 1000,
"duration_ms": 604800000,
"can_purchase": true,
"purchase_block_reason": "",
"benefits": [
{
"resource_type": "avatar_frame",
"resource_id": "vip1_frame",
"name": "VIP1 Avatar Frame",
"duration_policy": "inherit_vip_expiry"
}
]
}
]
}
```
App 业务错误通过 xerr 目录稳定映射:购买使用 `VIP_PROGRAM_INACTIVE``VIP_PACKAGE_NOT_PURCHASABLE``VIP_DOWNGRADE_NOT_ALLOWED``VIP_RECHARGE_REQUIRED``VIP_INSUFFICIENT_COIN`;体验卡使用 `VIP_TRIAL_CARD_NOT_FOUND``VIP_TRIAL_CARD_EXPIRED`;权益拒绝使用 `VIP_BENEFIT_REQUIRED`,普通房管防踢/防禁言分别使用 `VIP_ANTI_KICK``VIP_ANTI_MUTE`;返现使用 `VIP_COIN_REBATE_NOT_FOUND``VIP_COIN_REBATE_EXPIRED`。客户端不得解析 message。返现重复领取沿用幂等成功回执不额外定义 already-claimed 错误。 说明:
## 权益落地边界 - `can_purchase=false` 通常表示当前有效 VIP 等级高于该等级。
- App 可以展示所有等级,但购买按钮状态以服务端返回为准。
当前服务端已强制执行购买有效期、体验卡状态合并、用户通知开关、每日金币返现资格/消息/手动领取入账、自定义房间背景、防踢、防禁言、房内进场通知和上线全服通知。装扮类权益由已绑定的钱包资源与 Flutter 展示链路执行。 ### 查询我的 VIP
以下权益仍只有稳定编码和资格下发,不能仅凭本架构宣称业务已完整闭环:访客匿名/隐藏、榜单隐身、房间发图、动态媒体、彩色样式和礼物托盘等。各 owner 实现时必须调用 `CheckVipBenefit` 或消费 `effective_benefits`,并补自己的存储、审计、风控和边界测试。 `GET /api/v1/vip/me`
返回:
```json
{
"level": 2,
"name": "VIP2",
"status": "active",
"expires_at_ms": 1760000000000,
"remaining_ms": 604800000,
"renewable": true,
"upgradeable_levels": [3, 4, 5]
}
```
说明:
- 底部“我的”页面只需要拿 VIP 摘要,不要拉完整资源列表。
- 完整装扮资源仍从资源/背包接口查询。
### 购买 VIP
`POST /api/v1/vip/purchase`
请求:
```json
{
"level": 3,
"command_id": "client-generated-id"
}
```
返回:
```json
{
"order_id": "vip_order_123",
"event_type": "upgrade",
"coin_spent": 3000,
"vip": {
"level": 3,
"name": "VIP3",
"expires_at_ms": 1760604800000
},
"granted_resources": [
{
"resource_type": "vehicle",
"resource_id": "vip3_vehicle",
"expires_at_ms": 1760604800000
}
]
}
```
## gRPC 契约建议
新增 `wallet.v1.VipService`
```protobuf
service VipService {
rpc ListVipLevels(ListVipLevelsRequest) returns (ListVipLevelsResponse);
rpc GetMyVip(GetMyVipRequest) returns (GetMyVipResponse);
rpc PurchaseVip(PurchaseVipRequest) returns (PurchaseVipResponse);
}
```
核心字段:
- `RequestMeta meta`
- `int64 user_id`
- `int32 level`
- `string command_id`
- `int64 now_ms` 不由客户端传,由服务端生成;测试可通过 clock 注入。
错误码建议纳入 `pkg/xerr` catalog
- `VIP_LEVEL_NOT_FOUND`
- `VIP_LEVEL_DISABLED`
- `VIP_DOWNGRADE_NOT_ALLOWED`
- `VIP_PRICE_CHANGED`,仅在未来支持客户端价格确认时使用
- `WALLET_INSUFFICIENT_BALANCE`
- `IDEMPOTENCY_CONFLICT`
gRPC status 仍保持少量通用 code
- 参数错误:`InvalidArgument`
- 余额不足、不能降级:`FailedPrecondition`
- 等级不存在或禁用:`NotFound` / `FailedPrecondition`
- 幂等冲突:`AlreadyExists`
- 内部事务失败:`Internal`
业务错误放在 `google.rpc.ErrorInfo.reason`
## 并发和幂等
同一用户可能快速点击多次购买或同时升级,必须按用户串行化处理。
事务内锁顺序固定:
1. 锁 `vip_levels(level)`,读取配置快照。
2. 锁 `user_vip_memberships(user_id)`
3. 锁用户 `COIN` 钱包账户。
4. 写订单、流水、权益、会员状态。
如果 `user_vip_memberships` 不存在,先插入一行占位再锁,或者用用户级 advisory lock。不要只依赖应用内 mutex因为多实例部署时无效。
幂等规则:
- `command_id` 是购买命令幂等键,不使用 `request_id`
- 已成功订单重复请求直接返回原结果。
- 已失败订单是否允许重试取决于失败类型:余额不足可以用新 `command_id` 重试;系统错误如果事务未提交,可以继续用原 `command_id` 重试。
- 订单内保存价格、资源组、有效期快照,后台改配置不影响已成功订单解释。
## VIP 过期
VIP 是否有效以 `expires_at_ms` 为准。过期处理分两层:
- App 查询时实时判断,过期立即返回 `status=expired`
- 后台定时任务扫描过期会员,写 `user_vip_history(expire)``VipExpired` outbox用于消息通知、缓存清理和搜索投影更新。
资源权益也以自己的 `expires_at_ms` 判断有效性。如果使用 `inherit_vip_expiry`VIP 到期后头像框、座驾等权益自然失效,不需要额外删除。
## Outbox 事件
`wallet-service` 写出以下事件:
- `VipPurchased`
- `VipRenewed`
- `VipUpgraded`
- `VipExpired`
事件内容:
- `event_id`
- `user_id`
- `order_id`
- `from_level`
- `to_level`
- `expires_at_ms`
- `coin_spent`
- `resource_group_id`
- `occurred_at_ms`
消费者:
- `user-service` 可消费后更新用户资料投影,便于列表页快速展示 VIP 标识。
- `room-service` 如需房间内展示 VIP 角标,可消费投影事件或通过用户资料聚合读取,不接入购买主链路。
- `activity-service` 可用于 VIP 活动任务统计。
## 缓存策略
VIP 配置可以缓存,用户 VIP 状态不要只放 Redis。
- `vip_levels`:本地内存缓存 + 短 TTL后台改配置后通过版本号或事件刷新。
- `user_vip_memberships`MySQL 为准Redis 只做读优化。
- 购买成功后删除或刷新 `vip:me:{user_id}` 缓存。
- 过期时间短且强相关权益展示,缓存 TTL 不能超过 `min(60s, expires_at_ms-now)`
## 后台配置约束
后台配置 VIP 等级时必须校验:
- 等级唯一且大于 0。
- 金币价格大于 0。
- 有效期首版固定 7 天。
- 绑定资源组必须存在且可用。
- 资源组里的装扮资源建议使用 `inherit_vip_expiry`
- 禁用等级只影响新购买,不影响已购买用户的剩余有效期。
- 修改价格只影响修改后的新订单,不回改历史订单。
后台可以修改资源组,但已成功订单使用订单快照解释;用户已经拿到的权益不因为后台换资源组被自动替换。
## 数据一致性原则
- VIP 购买不是普通“资源组发放”,它必须先扣金币。
- 不允许 gateway 先扣金币再单独调用资源发放。
- 不允许 user-service 写 VIP 状态。
- 不允许客户端传资源 ID、资源组 ID、价格。
- 不允许用 Redis 作为 VIP 当前状态唯一来源。
- 不允许后台直接改用户 VIP 状态来修单,修单必须走后台补单/补偿命令,并写历史和审计。
## 实现顺序
1. 扩展 protobuf新增 `VipService`、VIP 等级、我的 VIP、购买 VIP 响应结构。
2. 增加 MySQL 表:`vip_levels``user_vip_memberships``vip_purchase_orders``user_vip_history`
3. 在 `wallet-service` 实现 VIP repository先完成等级查询和用户 VIP 查询。
4. 实现 `PurchaseVip` 事务:等级校验、幂等、行锁、扣金币、资源组发放、会员状态更新、历史和 outbox。
5. 在 `gateway-service` 增加 App HTTP API`/api/v1/vip/levels``/api/v1/vip/me``/api/v1/vip/purchase`
6. 接入用户资料聚合:底部“我的”和房间用户信息只展示 VIP 摘要。
7. 增加过期扫描任务和 outbox 消费投影。
8. 再做后台 VIP 配置页面和后台补单/审计能力。
## 测试重点
必须覆盖:
- 无 VIP 购买 VIP1 成功。
- VIP1 未过期时续费 VIP1到期时间追加 7 天。
- VIP1 未过期时购买 VIP3等级变 VIP3到期时间追加 7 天。
- VIP3 未过期时购买 VIP1 被拒绝。
- VIP 过期后可以购买任意等级。
- 金币不足时不写 VIP 状态、不发资源。
- 资源组发放失败时金币扣费回滚。
- 同一 `command_id` 重试返回同一订单。
- 同一用户并发购买时只产生符合顺序的一条最终状态。
- 后台禁用等级后不能新购买,但不影响已购买用户有效期。

View File

@ -1,296 +0,0 @@
# Agency 资料与房间 Flutter 对接
## 基础约定
本地开发地址:
```text
http://127.0.0.1:13000
```
所有接口都是登录接口,需要带登录 token。
请求头:
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Authorization` | 是 | `Bearer <access_token>`。 |
| `X-App-Code` | 否 | App 编码,默认 `lalu`;登录态接口最终以 token 内 app_code 为准。 |
成功和失败都使用统一 envelope
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {}
}
```
客户端分支只判断 `code`,排查问题记录 `request_id`
## 1. 查询用户所属 Agency 资料
地址:
```http
GET /api/v1/agencies/profile
```
参数:
| 参数 | 位置 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- | --- |
| `user_id` | query | 是 | string/int64 | 被查看的 host 用户 ID。 |
示例:
```http
GET /api/v1/agencies/profile?user_id=10001
Authorization: Bearer <access_token>
X-App-Code: lalu
```
返回值:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"agency": {
"agency_id": "7001",
"owner_user_id": "99",
"region_id": 30,
"parent_bd_user_id": "501",
"name": "Yumi Star Agency",
"avatar": "https://cdn.example/agency.png",
"host_count": 20,
"status": "active",
"join_enabled": true,
"created_by_user_id": "1",
"created_at_ms": 1710000000000,
"updated_at_ms": 1710000001000
}
}
}
```
没有 active host 身份,或没有 active agency 归属时:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"agency": null
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `agency_id` | Agency ID字符串。 |
| `owner_user_id` | Agency 会长用户 ID字符串。 |
| `name` | Agency 自己的名称,不再读 owner 用户昵称。 |
| `avatar` | Agency 自己的头像,不再读 owner 用户头像。 |
| `host_count` | 当前 active host 数量。 |
| `status` | 当前只展示 `active`。 |
| `join_enabled` | 是否允许用户申请加入。 |
| `created_at_ms` / `updated_at_ms` | Unix epoch milliseconds。 |
## 2. 查询 Agency 下所有 host 的房间
地址:
```http
GET /api/v1/agencies/rooms
```
参数:
| 参数 | 位置 | 必填 | 类型 | 默认 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `user_id` | query | 是 | string/int64 | 无 | 被查看的 host 用户 ID。 |
| `limit` | query | 否 | int | 20 | 每页数量,最大 50。 |
| `cursor` | query | 否 | string | 空 | 下一页 cursor。 |
| `tab` | query | 否 | string | `hot` | `hot``new`。 |
| `query` | query | 否 | string | 空 | 搜索房间标题、房间 ID 或短号。 |
示例:
```http
GET /api/v1/agencies/rooms?user_id=10001&tab=hot&limit=20
Authorization: Bearer <access_token>
X-App-Code: lalu
```
返回值:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"agency_id": "7001",
"rooms": [
{
"room_id": "room_101",
"im_group_id": "room_101",
"owner_user_id": "101",
"title": "Agency Room",
"cover_url": "https://cdn.example/room.png",
"mode": "voice",
"status": "active",
"locked": false,
"heat": 88,
"online_count": 12,
"seat_count": 8,
"occupied_seat_count": 3,
"visible_region_id": 30,
"app_code": "lalu",
"room_short_id": "M20",
"country_code": "AE",
"country_flag": "🇦🇪"
}
],
"next_cursor": "cursor_xxx"
}
}
```
没有 active host 身份,或没有 active agency 归属时:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"agency_id": "",
"rooms": [],
"next_cursor": ""
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `agency_id` | 当前查询到的 Agency ID无归属时为空字符串。 |
| `rooms` | Agency 下所有 active host 的 active 房间。 |
| `next_cursor` | 下一页 cursor为空表示没有下一页。 |
| `room_id` | 房间 ID。 |
| `im_group_id` | 腾讯云 IM 房间群 ID进群使用这个字段。 |
| `owner_user_id` | 房主用户 ID字符串。 |
| `title` | 房间名。 |
| `cover_url` | 房间封面。 |
| `locked` | 是否锁房。 |
| `heat` | 房间热度。 |
| `online_count` | 在线人数。 |
| `seat_count` | 麦位数量。 |
| `occupied_seat_count` | 已占用麦位数量。 |
| `room_short_id` | 房间短号。 |
| `country_code` / `country_flag` | 房主国家展示字段。 |
## 3. 修改 Agency 资料
地址:
```http
POST /api/v1/agencies/profile/update
```
权限:
只有当前登录用户是 active Agency owner 时可以修改。普通 host、Agency 成员、BD、非 active owner 都不能修改。
参数:
| 参数 | 位置 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- | --- |
| `command_id` | body | 是 | string | 客户端生成的幂等 ID。 |
| `name` | body | 否 | string | 新 Agency 名称。 |
| `avatar` | body | 否 | string | 新 Agency 头像 URL。 |
`name``avatar` 至少传一个;传了就不能是空字符串。修改 Agency 资料不会修改 owner 用户昵称和头像。
示例:
```http
POST /api/v1/agencies/profile/update
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
{
"command_id": "cmd_agency_profile_001",
"name": "New Agency Name",
"avatar": "https://cdn.example/new-agency.png"
}
```
返回值:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"agency": {
"agency_id": "7001",
"owner_user_id": "42",
"region_id": 30,
"name": "New Agency Name",
"avatar": "https://cdn.example/new-agency.png",
"host_count": 20,
"status": "active",
"join_enabled": true,
"created_at_ms": 1710000000000,
"updated_at_ms": 1710000005000
}
}
}
```
## 错误处理
失败返回:
```json
{
"code": "INVALID_ARGUMENT",
"message": "invalid argument",
"request_id": "req_abc"
}
```
| HTTP 状态码 | `code` | 处理方式 |
| --- | --- | --- |
| `400` | `INVALID_ARGUMENT` | 参数错误,例如 `user_id` 非法、`tab` 非法、`limit` 非法、缺少 `command_id``name/avatar` 都没传或传空。 |
| `401` | `UNAUTHORIZED` | token 缺失、无效或过期,重新登录。 |
| `403` | `PROFILE_REQUIRED` | 用户资料未完成,跳转资料补全。 |
| `403` | `PERMISSION_DENIED` | 修改接口中当前用户不是 active Agency owner不允许修改。 |
| `404` | `NOT_FOUND` | 修改接口中 Agency 事实不存在;刷新当前用户身份后再进入页面。 |
| `409` | `CONFLICT` | `command_id` 与已有不同命令冲突;重新生成新的 `command_id` 再提交。 |
| `502` | `UPSTREAM_ERROR` | 服务暂不可用,可以提示稍后重试。 |
客户端处理规则:
1. 查询资料时,`agency == null` 是正常空状态,不弹错误。
2. 查询房间时,`rooms == []` 是正常空列表,不弹错误。
3. 修改资料成功后,用返回的 `data.agency` 刷新页面,不要用 owner 用户资料覆盖。
4. 修改失败时保留本地未保存状态,让用户可以重试。
5. 所有失败日志记录 `request_id`
## 相关 IM
无。Agency 资料修改和 Agency 房间列表不会下发新的 IM房间卡片里的 `im_group_id` 只用于进入房间时加入腾讯云 IM 房间群。

View File

@ -1,108 +0,0 @@
# App 埋点 Flutter 对接
## 地址
```http
POST /api/v1/app/events
Content-Type: application/json
```
可登录也可未登录调用。已登录时建议带:
```http
Authorization: Bearer <access_token>
```
未登录时必须传 `device_id` 或 Header
```http
X-Device-ID: <device_id>
```
## 参数
Body
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `events` | array | 是 | 埋点列表,单次最多 50 条。 |
`events[]`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `event_id` | string | 是 | 客户端生成的唯一事件 ID。同一事件重试必须复用同一个值。 |
| `event_name` | string | 是 | 事件名,例如 `banner_view``banner_click``page_open`。 |
| `event_type` | string | 否 | 事件类型,例如 `view``click``submit`。 |
| `screen` | string | 否 | 页面或模块,例如 `home``room``me`。 |
| `target_type` | string | 否 | 目标类型,例如 `banner``button``room`。 |
| `target_id` | string | 否 | 目标 ID例如 banner ID、房间 ID。 |
| `device_id` | string | 未登录必填 | 设备 ID。也可以放到 `X-Device-ID`。 |
| `session_id` | string | 否 | 客户端本地会话 ID。登录态下服务端优先使用 token 里的 session。 |
| `platform` | string | 否 | `android``ios`。不传时读取 `X-App-Platform` / `X-Platform`。 |
| `app_version` | string | 否 | App 版本。不传时读取 `X-App-Version` / `X-Client-Version`。 |
| `language` | string | 否 | 语言。不传时读取 `X-App-Language` / `X-Language` / `Accept-Language`。 |
| `timezone` | string | 否 | 客户端时区,只做属性记录,不参与业务切日。 |
| `duration_ms` | int64 | 否 | 客户端记录的耗时毫秒。 |
| `success` | bool | 否 | 动作是否成功。 |
| `error_code` | string | 否 | 失败时的客户端错误码。 |
| `properties` | object | 否 | 扩展属性,最大 4KB。 |
| `occurred_at_ms` | int64 | 否 | 事件发生时间UTC epoch milliseconds。不传时使用服务端接收时间。 |
服务端会自己识别 `app_code``user_id``country_id``region_id`,客户端不要传这些字段。
示例:
```json
{
"events": [
{
"event_id": "evt_20260703_100001_banner_17_view",
"event_name": "banner_view",
"event_type": "view",
"screen": "home",
"target_type": "banner",
"target_id": "17",
"device_id": "device_xxx",
"platform": "android",
"app_version": "1.2.3",
"language": "en-US",
"timezone": "Asia/Shanghai",
"success": true,
"properties": {
"position": "top"
},
"occurred_at_ms": 1778000005000
}
]
}
```
## 返回值
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"accepted": true,
"received": 1,
"stored": 1,
"duplicated": 0,
"server_time_ms": 1778000005000
}
}
```
| 字段 | 说明 |
| --- | --- |
| `data.accepted` | 是否接受本次上报。 |
| `data.received` | 本次收到的事件数。 |
| `data.stored` | 本次新写入的事件数。 |
| `data.duplicated` | 已存在的事件数。通常是重试导致的重复 `event_id`。 |
| `data.server_time_ms` | 服务端时间UTC epoch milliseconds。 |
## 相关 IM
无。

View File

@ -1,522 +0,0 @@
# CP关系Flutter对接
本文只写 App 需要对接的地址、参数、返回值和相关 IM。CP 关系包含 `cp``brother``sister` 三种类型CP 关系每个用户最多 1 个,兄弟和姐妹数量由后台 CP 配置控制;不需要佩戴接口。
## 1. 礼物面板CP Tab
地址:
```http
GET /api/v1/rooms/{room_id}/gift-panel
Authorization: Bearer <access_token>
X-App-Code: lalu
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID |
返回值:
`tabs` 里会有 `gift_type_code=cp` 的 Tab。CP 礼物会带:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `gift_id` | string | 送礼时使用 |
| `gift_type_code` | string | CP 礼物为 `cp` |
| `cp_relation_type` | string | `cp``brother``sister` |
| `name` | string | 礼物名 |
| `coin_price` | int64 | 展示价格,扣费以后端为准 |
| `resource.asset_url` | string | 礼物 icon |
| `resource.animation_url` | string | 礼物动效,可为空 |
Flutter 在 CP 礼物 Tab 下按 `cp_relation_type` 分成 CP、兄弟、姐妹三个二级 Tab。
## 2. 发送CP礼物
地址:
```http
POST /api/v1/rooms/gift/send
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID |
| `command_id` | string | 是 | 本次送礼幂等键,重试必须复用 |
| `target_type` | string | 否 | 当前传 `user` |
| `target_user_ids` | int64[] | 是 | CP 礼物只传 1 个收礼人 |
| `gift_id` | string | 是 | CP 礼物 ID |
| `gift_count` | int32 | 是 | 必须大于 0 |
示例:
```json
{
"room_id": "room_1001",
"command_id": "cp_gift_1779259000000_10001_10002",
"target_type": "user",
"target_user_ids": [10002],
"gift_id": "cp_ring",
"gift_count": 1
}
```
返回值:
沿用普通送礼响应。关系申请通过 IM 到达,送礼响应不保证返回申请 ID。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `result.applied` | bool | 本次命令是否落地 |
| `billing_receipt_id` | string | 扣费回执 |
| `room_heat` | int64 | 最新房间热度 |
| `gift_rank` | array | 房间礼物榜 |
| `room` | object | 房间快照 |
| `lucky_gift` | object | 幸运礼物结果,普通 CP 礼物为空 |
## 3. 查询我的申请
地址:
```http
GET /api/v1/cp/applications?direction=incoming&status=pending&page=1&page_size=20
Authorization: Bearer <access_token>
X-App-Code: lalu
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `direction` | string | 否 | `incoming` 我收到的;`outgoing` 我发出的 |
| `status` | string | 否 | `pending``accepted``rejected``expired``blocked` |
| `page` | int32 | 否 | 默认 1 |
| `page_size` | int32 | 否 | 默认 20 |
返回值:
```json
{
"applications": [
{
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"status": "pending",
"requester": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"target": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"room_id": "room_1001",
"room_region_id": 86,
"gift": {
"gift_id": "cp_ring",
"gift_name": "CP Ring",
"gift_icon_url": "https://cdn.example/cp-ring.png",
"gift_animation_url": "",
"gift_count": 1,
"gift_value": 520,
"billing_receipt_id": "gift_tx_1"
},
"created_at_ms": 1779259000000,
"updated_at_ms": 1779259000000,
"expires_at_ms": 1779345400000,
"decided_at_ms": 0
}
],
"total": 1,
"server_time_ms": 1779259001000
}
```
## 4. 同意申请
地址:
```http
POST /api/v1/cp/applications/{application_id}/accept
Authorization: Bearer <access_token>
X-App-Code: lalu
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `application_id` | string | 是 | 申请 ID |
返回值:
```json
{
"application": {
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"status": "accepted",
"decided_at_ms": 1779259100000
},
"relationship": {
"relationship_id": "cp_rel_10001_10002",
"relation_type": "cp",
"status": "active",
"me": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"partner": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"intimacy_value": 0,
"level": 1,
"formed_at_ms": 1779259100000,
"updated_at_ms": 1779259100000
}
}
```
错误处理:
| code | 处理 |
| --- | --- |
| `CONFLICT` | A/B 任一方已经有 active 关系,或申请已过期;刷新申请和关系列表 |
| `NOT_FOUND` | 申请不存在,刷新申请列表 |
| `PERMISSION_DENIED` | 当前用户不是申请目标用户 |
## 5. 拒绝申请
地址:
```http
POST /api/v1/cp/applications/{application_id}/reject
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `application_id` | string | 是 | 申请 ID |
| `reason` | string | 否 | 拒绝原因 |
返回值:
```json
{
"application": {
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"status": "rejected",
"decided_at_ms": 1779259100000
}
}
```
## 6. 查询我的关系
地址:
```http
GET /api/v1/cp/relationships?relation_type=cp&page=1&page_size=20
Authorization: Bearer <access_token>
X-App-Code: lalu
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `relation_type` | string | 否 | `cp``brother``sister`;不传返回全部 |
| `page` | int32 | 否 | 默认 1 |
| `page_size` | int32 | 否 | 默认 20 |
返回值:
```json
{
"relationships": [
{
"relationship_id": "cp_rel_10001_10002",
"relation_type": "cp",
"status": "active",
"me": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"partner": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"intimacy_value": 1888,
"level": 2,
"current_level_threshold": 1000,
"next_level_threshold": 3000,
"needed_for_next_level": 1112,
"level_progress_percent": 44,
"max_level": false,
"breakup_cost_coins": 88,
"formed_at_ms": 1779259100000,
"ended_at_ms": 0,
"updated_at_ms": 1779259200000
}
],
"total": 1
}
```
说明:
- CP 关系每个用户最多 1 个;兄弟和姐妹可拥有数量以后台配置为准。
- A/B 两个用户之间同一时间只能存在一种 active 关系,不能同时是 CP、兄弟或姐妹。
- 不需要佩戴接口,资料卡直接展示当前 active 关系。
- 资料卡弹窗只使用后端返回的 `intimacy_value``level``current_level_threshold``next_level_threshold``needed_for_next_level``level_progress_percent``max_level`
- `max_level=true``next_level_threshold=0` 时表示没有下一等级,隐藏“距离下一级”和进度条。
- Flutter 不要用本地常量、礼物价格或后台配置接口自行推算进度条和“距离下一级”。
- `breakup_cost_coins` 是解除当前关系需要支付的金币数,弹窗和二次确认直接使用该值。
## 7. 解除关系
地址:
```http
POST /api/v1/cp/relationships/{relationship_id}/break
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
{
"command_id": "break_cp_rel_10001_10002_1779259300000"
}
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `relationship_id` | string | 是 | URL path关系 ID |
| `command_id` | string | 是 | 解除关系幂等键,同一次重试必须复用 |
返回值:
```json
{
"relationship": {
"relationship_id": "cp_rel_10001_10002",
"relation_type": "cp",
"status": "ended",
"breakup_cost_coins": 88,
"formed_at_ms": 1779259100000,
"ended_at_ms": 1779259300000,
"updated_at_ms": 1779259300000
},
"breakup_cost_coins": 88,
"wallet_transaction_id": "wallet_tx_break_1",
"coin_balance_after": 912
}
```
说明:
- 解除前先展示 `/api/v1/cp/relationships` 返回的 `breakup_cost_coins`
- 金币不足时接口返回 `INSUFFICIENT_BALANCE`,关系仍保持 active。
- 解除成功不发 C2C、房间 IM 或区域 IMFlutter 使用接口返回刷新资料卡和关系列表。
## 8. 相关IM
所有 IM 都是 Tencent `TIMCustomElem``MsgContent.Desc` 等于下表事件名,`MsgContent.Data` 是 JSON 字符串。客户端按 `event_id` 去重。
### 8.1 收到关系申请
房间 IM 和 C2C IM 都会收到:
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `cp_application_created` |
| `Data.event_type` | `cp_application_created` |
Data
```json
{
"event_id": "cp_application:created:10002:1",
"event_type": "cp_application_created",
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"status": "pending",
"requester": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"target": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"room_id": "room_1001",
"room_region_id": 86,
"gift": {
"gift_id": "cp_ring",
"gift_name": "CP Ring",
"gift_icon_url": "https://cdn.example/cp-ring.png",
"gift_animation_url": "",
"gift_count": 1,
"gift_value": 520,
"billing_receipt_id": "gift_tx_1"
},
"expires_at_ms": 1779345400000,
"decided_at_ms": 0,
"source_created_at_ms": 1779259000000,
"recipient_user_id": "10002"
}
```
处理:
- 房间 IM 只有 `recipient_user_id` 等于当前用户时显示同意/拒绝按钮。
- C2C IM 在私聊里显示同意/拒绝按钮。
- `expires_at_ms <= server_time_ms` 时按钮置灰并刷新申请列表。
### 8.2 申请被同意
C2C 会收到双方互发的两条消息,房间 IM 仍然只给申请发起人 A 展示:
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `cp_application_accepted` |
| `Data.event_type` | `cp_application_accepted` |
Data 字段和申请消息一致,差异是:
| 字段 | 值 |
| --- | --- |
| `status` | `accepted` |
| `decided_at_ms` | 处理时间 |
| `recipient_user_id` | 房间 IM 中为 A 的 user_id |
C2C 差异:
| 方向 | `c2c_notice_role` | `message_text` |
| --- | --- | --- |
| B -> A | `accepted_to_requester` | `我同意了你` |
| A -> B | `accepted_to_acceptor` | `对方已同意你的申请` |
处理:
- A 收到后刷新 `/api/v1/cp/relationships`
- B 操作成功后也刷新关系列表。
- 同一对用户其他 pending 申请需要置灰。
### 8.3 申请被拒绝
C2C 和房间 IM 都会收到:
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `cp_application_rejected` |
| `Data.event_type` | `cp_application_rejected` |
Data 字段和申请消息一致,差异是:
| 字段 | 值 |
| --- | --- |
| `status` | `rejected` |
| `reason` | 拒绝原因,可为空 |
| `decided_at_ms` | 处理时间 |
| `recipient_user_id` | 房间 IM 中为 A 的 user_id |
### 8.4 区域关系成立播报
用户组成任意关系后,会发送区域 IM
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `cp_relationship_created` |
| `Data.event_type` | `cp_relationship_created` |
Data
```json
{
"event_id": "cp_relationship:created:10002:1",
"event_type": "cp_relationship_created",
"relationship_id": "cp_rel_10001_10002",
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"requester": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"target": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"gift": {
"gift_id": "cp_ring",
"gift_name": "CP Ring",
"gift_icon_url": "https://cdn.example/cp-ring.png",
"gift_animation_url": "",
"gift_count": 1,
"gift_value": 520,
"billing_receipt_id": "gift_tx_1"
},
"room_id": "room_1001",
"room_region_id": 86,
"group_id": "hy_lalu_bc_r_86",
"source_created_at_ms": 1779259100000
}
```
处理:
- 只在区域播报位展示,不当作私聊消息。
- 点击可进入 `room_id` 对应房间;房间不存在或进房失败时只关闭播报。
## 8. 客户端错误提示
| code | 建议提示 |
| --- | --- |
| `CONFLICT` | `你或对方已经有关系`;如果申请已过期则提示 `申请已过期` |
| `NOT_FOUND` | `申请不存在或已失效` |
| `PERMISSION_DENIED` | `只能处理发给你的申请` |
## 9. 本地状态规则
- 不要靠送礼成功直接创建关系。
- 不要靠 C2C 或房间按钮本地改关系,必须调用同意或拒绝接口。
- 同一个 `event_id` 只处理一次。
- 同一个 `application_id` 在房间和 C2C 里可能都出现,按钮状态要共享。
- 申请过期时间以 `expires_at_ms` 为准。
- 亲密值和等级以后端关系接口返回为准,不在 Flutter 里按礼物价格推算。
- 进度条和距离下一级只使用关系接口返回的阈值字段;字段缺失时不展示,不要伪造 `next_level_threshold``needed_for_next_level` 或进度百分比。

View File

@ -138,7 +138,7 @@ GET /api/v1/wallet/me/balances
示例: 示例:
```text ```text
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 GET /api/v1/wallet/me/balances?asset_type=COIN&asset_type=DIAMOND&asset_type=USD_BALANCE
``` ```
### 返回值 `data` ### 返回值 `data`
@ -158,7 +158,7 @@ GET /api/v1/wallet/me/balances?asset_type=COIN&asset_type=DIAMOND&asset_type=HOS
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| ------------------ | ------ | ------------------------------------------------- | | ------------------ | ------ | ------------------------------------------------- |
| `asset_type` | string | `COIN` / `DIAMOND` / `HOST_SALARY_USD` / `AGENCY_SALARY_USD` / `BD_SALARY_USD` / `ADMIN_SALARY_USD` / `GIFT_POINT` | | `asset_type` | string | `COIN` / `DIAMOND` / `USD_BALANCE` / `GIFT_POINT` |
| `available_amount` | int64 | 可用余额 | | `available_amount` | int64 | 可用余额 |
| `frozen_amount` | int64 | 冻结余额 | | `frozen_amount` | int64 | 冻结余额 |
| `version` | int64 | 该资产余额版本,用于和 IM 通知去重/防乱序 | | `version` | int64 | 该资产余额版本,用于和 IM 通知去重/防乱序 |
@ -201,7 +201,7 @@ GET /api/v1/wallet/me/overview
"version": 4 "version": 4
}, },
{ {
"asset_type": "HOST_SALARY_USD", "asset_type": "USD_BALANCE",
"available_amount": 1500, "available_amount": 1500,
"frozen_amount": 0, "frozen_amount": 0,
"version": 2 "version": 2
@ -209,7 +209,8 @@ GET /api/v1/wallet/me/overview
], ],
"feature_flags": { "feature_flags": {
"recharge_enabled": true, "recharge_enabled": true,
"diamond_exchange_enabled": true "diamond_exchange_enabled": true,
"withdraw_enabled": true
} }
} }
``` ```
@ -219,6 +220,7 @@ GET /api/v1/wallet/me/overview
| `balances` | array | 钱包首页需要展示的资产余额 | | `balances` | array | 钱包首页需要展示的资产余额 |
| `feature_flags.recharge_enabled` | bool | 是否允许充值 | | `feature_flags.recharge_enabled` | bool | 是否允许充值 |
| `feature_flags.diamond_exchange_enabled` | bool | 是否允许钻石兑换 | | `feature_flags.diamond_exchange_enabled` | bool | 是否允许钻石兑换 |
| `feature_flags.withdraw_enabled` | bool | 是否允许提现 |
### 可能错误 ### 可能错误

View File

@ -1,303 +0,0 @@
# Google 三方注册 Flutter 对接
## 边界
Google 登录后Flutter 只暂存 Firebase ID token 和 Google 基本信息,不写入 `AppAuthSession`
资料页允许 pending third-party auth 进入,不要求 `_authService.isSignedIn`。用户点击完成后Flutter 调注册接口;后端校验 Firebase ID token、创建或补齐用户资料、返回 access/refresh token。Flutter 收到 token 后再写入现有 `AppAuthSession`
旧接口 `POST /api/v1/auth/third-party/login` 仍保留兼容。新 Google 流程只能在检查接口返回 `registered=true` 后调用它拿 session不要在 `registered=false` 时调用它,避免旧接口创建 pending 用户。
## 调用流程
1. Flutter 调 Google Sign-In / Firebase Auth拿到 Firebase ID token。
2. Flutter 暂存:
- `provider=firebase`
- `credential=<firebase_id_token>`
- Google 返回的昵称、头像等基础信息
- `device_id``platform``language``timezone`
3. 调注册检查接口。
4. 如果 `registered=true`,调用旧三方登录接口拿 session写入 `AppAuthSession` 后进首页。
5. 如果 `registered=false`,进入资料页,用户补齐 `username``avatar``gender``country``invite_code`
6. 如果用户选择本地头像,先调用注册头像上传接口拿 URL。
7. 调 `POST /api/v1/auth/third-party/register`
8. 成功后把返回的 token 写入 `AppAuthSession`
## 注册检查
地址:
```http
POST /api/v1/auth/third-party/registered/check
```
Headers
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Content-Type` | 是 | `application/json` |
| `X-App-Code` | 否 | App 编码,默认 `lalu` |
| `X-App-Package` | 否 | 包名;后端可按包名解析 app_code。 |
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `provider` | string | 是 | 固定传 `firebase`。 |
| `credential` | string | 是 | Firebase ID token。 |
请求示例:
```json
{
"provider": "firebase",
"credential": "firebase_id_token"
}
```
返回值:
```json
{
"code": 0,
"message": "ok",
"request_id": "req_xxx",
"data": {
"registered": true
}
}
```
Flutter 处理:
| `registered` | 处理 |
| --- | --- |
| `true` | 用户已完成注册;再调用 `POST /api/v1/auth/third-party/login` 获取 session。 |
| `false` | 用户未完成注册,或者只有旧流程留下的 pending 用户;进入资料页,最后调用注册接口。 |
## 注册头像上传
地址:
```http
POST /api/v1/files/registration/avatar/upload
```
Headers
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Content-Type` | 是 | `multipart/form-data` |
| `X-App-Code` | 否 | App 编码,默认 `lalu` |
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `file` | file | 是 | 头像文件;也支持字段名 `avatar`。 |
限制:
| 项 | 说明 |
| --- | --- |
| 登录态 | 不需要 `Authorization`。 |
| 大小 | 最大 5MB。 |
| 格式 | `jpg``jpeg``png``webp`。 |
| 校验 | 后端按文件头识别图片类型,不信任客户端传入的 Content-Type。 |
返回值:
```json
{
"code": 0,
"message": "ok",
"request_id": "req_xxx",
"data": {
"url": "https://media.example.com/app/avatars/registration/20260626/avatar_xxx.png",
"object_key": "app/avatars/registration/20260626/avatar_xxx.png",
"content_type": "image/png",
"size_bytes": 204800
}
}
```
Flutter 使用 `data.url` 作为注册接口的 `avatar`
## 三方注册
地址:
```http
POST /api/v1/auth/third-party/register
```
Headers
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Content-Type` | 是 | `application/json` |
| `X-App-Code` | 否 | App 编码,默认 `lalu` |
| `X-App-Package` | 否 | 包名;后端可按包名解析 app_code。 |
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `provider` | string | 是 | 固定传 `firebase`。 |
| `credential` | string | 是 | Firebase ID token。 |
| `device_id` | string | 是 | 设备 ID用于 refresh session 绑定和注册风控。 |
| `username` | string | 是 | 用户昵称。 |
| `avatar` | string | 是 | HTTP(S) 头像 URL可以是上传接口返回的 `url`。 |
| `gender` | string | 是 | 性别值,后端只保存原值。 |
| `country` | string | 是 | 国家码,例如 `SG``CN`。 |
| `invite_code` | string | 否 | 邀请码。 |
| `device` | string | 否 | 设备型号。 |
| `os_version` | string | 否 | 系统版本。 |
| `birth` | string | 否 | 生日,格式 `yyyy-mm-dd`。 |
| `app_version` | string | 否 | App 版本。 |
| `build_number` | string | 否 | 构建号。 |
| `source` | string | 否 | 注册来源。 |
| `install_channel` | string | 否 | 安装渠道。 |
| `campaign` | string | 否 | 投放活动。 |
| `platform` | string | 是 | `android``ios`。 |
| `language` | string | 否 | 客户端语言,例如 `en-US`。 |
| `timezone` | string | 否 | IANA 时区,例如 `Asia/Shanghai`。 |
请求示例:
```json
{
"provider": "firebase",
"credential": "firebase_id_token",
"device_id": "ios_device_001",
"username": "Alice",
"avatar": "https://media.example.com/app/avatars/registration/20260626/avatar_xxx.png",
"gender": "female",
"country": "SG",
"invite_code": "",
"device": "iPhone 15",
"os_version": "iOS 18.1",
"birth": "2000-01-02",
"app_version": "1.2.3",
"build_number": "123",
"source": "google",
"install_channel": "app_store",
"campaign": "",
"platform": "ios",
"language": "en-US",
"timezone": "Asia/Shanghai"
}
```
返回值:
```json
{
"code": 0,
"message": "ok",
"request_id": "req_xxx",
"data": {
"app_code": "lalu",
"user_id": "10001",
"display_user_id": "163001",
"default_display_user_id": "163001",
"display_user_id_kind": "default",
"display_user_id_expires_at_ms": 0,
"session_id": "sess_xxx",
"access_token": "access_token",
"refresh_token": "refresh_token",
"expires_in_sec": 604800,
"token_type": "Bearer",
"is_new_user": true,
"profile_completed": true,
"onboarding_status": "completed"
}
}
```
Flutter 成功处理:
| 字段 | 处理 |
| --- | --- |
| `access_token` | 写入现有 `AppAuthSession.accessToken`。 |
| `refresh_token` | 写入现有 `AppAuthSession.refreshToken`。 |
| `session_id` | 写入现有 session 标识。 |
| `user_id` | 字符串保存,不要转 JS number。 |
| `display_user_id` | 展示号。 |
| `profile_completed` | 必须为 `true`;否则不要进入首页。 |
| `onboarding_status` | 成功应为 `completed`。 |
## 重试和兼容
同一个 Firebase subject 如果之前已经被旧流程创建成 pending 用户,新注册接口会补齐这个用户并返回 token不会再创建第二个用户。
同一个 Firebase subject 如果已经完成注册,再调该接口会按已有用户登录返回 token不会覆盖用户后续改过的资料。
## 错误处理
| HTTP | code | 场景 | Flutter 处理 |
| --- | --- | --- | --- |
| `400` | `INVALID_ARGUMENT` | 缺必填字段、国家格式错误、头像 URL 非 HTTP(S)、生日格式错误。 | 停留资料页,提示用户修正。 |
| `400` | `INVALID_INVITE_CODE` | 邀请码格式错误或不可用。 | 提示邀请码无效,可允许清空后重试。 |
| `401` | `AUTH_FAILED` | Firebase ID token 无效、过期或 provider 不允许。 | 回到 Google 登录重新拿 token。 |
| `429` | `RATE_LIMITED` | 登录注册入口限流。 | 稍后重试。 |
| `409` | `DEVICE_ALREADY_REGISTERED` | 当前设备已达到注册账号上限。 | 提示当前设备无法继续注册。 |
| `502` | `UPSTREAM_ERROR` | user-service 或对象存储不可用。 | 稍后重试。 |
## Dart 调用示例
```dart
Future<AppAuthSession> registerFirebaseUser({
required AppNetworkService network,
required String firebaseIdToken,
required String deviceId,
required String username,
required String avatar,
required String gender,
required String country,
String inviteCode = '',
}) async {
final envelope = await network.postEnvelope(
'/api/v1/auth/third-party/register',
data: {
'provider': 'firebase',
'credential': firebaseIdToken,
'device_id': deviceId,
'username': username,
'avatar': avatar,
'gender': gender,
'country': country,
'invite_code': inviteCode,
'platform': Platform.isIOS ? 'ios' : 'android',
'language': PlatformDispatcher.instance.locale.toLanguageTag(),
'timezone': await currentIanaTimezone(),
},
requiresAuthorization: false,
);
if (envelope.code != 0) {
throw ApiException(envelope.code, envelope.message);
}
final data = envelope.data as Map<String, dynamic>;
if (data['profile_completed'] != true ||
data['onboarding_status'] != 'completed') {
throw StateError('third-party register did not complete profile');
}
return AppAuthSession(
appCode: data['app_code'] as String? ?? '',
userId: data['user_id'] as String? ?? '',
displayUserId: data['display_user_id'] as String? ?? '',
sessionId: data['session_id'] as String? ?? '',
accessToken: data['access_token'] as String? ?? '',
refreshToken: data['refresh_token'] as String? ?? '',
expiresInSec: (data['expires_in_sec'] as num? ?? 0).toInt(),
tokenType: data['token_type'] as String? ?? 'Bearer',
profileCompleted: data['profile_completed'] == true,
onboardingStatus: data['onboarding_status'] as String? ?? '',
);
}
```
相关 IM注册接口成功后后端会导入腾讯 IM 用户资料Flutter 不需要额外调用 IM 导入接口。

View File

@ -1,82 +0,0 @@
# Huwaa 首页房间筛选 Flutter 对接
## 地址
筛选目录:
```http
GET /api/v1/rooms/filters
Authorization: Bearer <access_token>
X-App-Code: huwaa
```
房间列表:
```http
GET /api/v1/rooms?tab=hot&region_id=1001
GET /api/v1/rooms?tab=hot&country_code=AE
```
目录接口要求用户已登录并完成资料。租户权威值是 access token claims 中的 `app_code``X-App-Code`/包名只参与请求入口解析,不能覆盖 token 或用来跨租户切换。Huwaa 返回本 App 的全部 active 区域和其中 enabled 国家;其他 App 只返回当前用户所在 active 区域及其国家。
## 参数
目录接口没有 query 参数。每个 option 都返回 `query_key``query_value`Flutter 按下面规则调用房间列表:
- All`query_key``query_value` 都为空,不传 `region_id``country_code`
- 区域:把 option 的 `query_key=region_id``query_value` 原样放进 query。
- 国家:把 option 的 `query_key=country_code``query_value` 原样放进 query。
- `region_id``country_code` 不能同时传;切换筛选项时清空旧 cursor再从第一页请求。
- 旧参数 `country` 继续兼容,新代码统一使用 `country_code`
`parent_region_id` 只用于把国家展示在所属区域下。筛选国家时只传 `country_code`,不要把 `parent_region_id` 同时作为 `region_id` 传入。
## 返回值
```json
{
"code": "OK",
"message": "ok",
"request_id": "req-room-filters",
"data": {
"all": {
"filter_type": "all",
"query_key": "",
"query_value": ""
},
"regions": [
{
"filter_type": "region",
"query_key": "region_id",
"query_value": "1001",
"region_id": 1001,
"region_code": "MIDDLE_EAST",
"region_name": "Middle East",
"sort_order": 10
}
],
"countries": [
{
"filter_type": "country",
"query_key": "country_code",
"query_value": "AE",
"parent_region_id": 1001,
"country_id": 971,
"country_code": "AE",
"country_name": "United Arab Emirates",
"country_display_name": "UAE",
"country_flag": "🇦🇪",
"sort_order": 10
}
]
}
}
```
目录的数组顺序已经使用后台 `sort_order`。国家只在“国家 enabled 且所属区域 active”时下发客户端不要自行拼接国家码或区域 ID。
`GET /api/v1/rooms` 响应中的旧 `data.countries` 字段继续保留供旧版当前区域国家筛选使用Huwaa 新版完整目录以本接口为准。
## 相关 IM/RTC
筛选目录和列表筛选不创建房间、不入腾讯云 IM 群,也不签发 RTC token。用户实际进入房间后继续按现有 JoinRoom 流程处理 IM/RTC。

View File

@ -1,103 +0,0 @@
# 个人主页背景图 Flutter 对接
个人主页背景图是用户资料字段 `profile_bg_img`,不是背包资源,不是 `profile_card`也不需要佩戴。App 先自行上传图片拿到 URL再调用后端保存 URL。
## 保存主页背景图
地址:
```http
POST /api/v1/users/me/profile-bg-img
```
Headers
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Authorization` | 是 | `Bearer <access_token>` |
| `Content-Type` | 是 | `application/json` |
| `X-App-Code` | 否 | App 编码,默认 `lalu` |
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `url` | string | 是 | App 已上传好的图片 URL必须是 `http``https` 绝对地址。 |
请求示例:
```json
{
"url": "https://cdn.example.com/profile-bg.png"
}
```
返回值:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"profile_bg_img": "https://cdn.example.com/profile-bg.png",
"profile": {
"user_id": "10001",
"display_user_id": "163001",
"username": "Alice",
"avatar": "https://cdn.example.com/avatar.png",
"profile_bg_img": "https://cdn.example.com/profile-bg.png",
"profile_completed": true,
"onboarding_status": "completed"
}
}
}
```
Flutter 处理:
| 场景 | 处理 |
| --- | --- |
| 保存成功 | 使用 `data.profile_bg_img` 刷新当前资料页背景图。 |
| 重新打开我的资料页 | 读取个人信息接口返回的 `profile_bg_img`。 |
| 查看别人资料卡 | 使用访问资料卡响应里的 `target_profile.profile_bg_img`。 |
## 个人信息返回字段
当前用户个人信息:
```http
GET /api/v1/users/me
```
我的页概览:
```http
GET /api/v1/users/me/overview
```
访问别人的资料卡:
```http
POST /api/v1/users/{user_id}/visit
```
返回字段:
| 字段 | 说明 |
| --- | --- |
| `profile_bg_img` | 用户主页背景图 URL未设置时为空字符串。 |
| `profile.profile_bg_img` | `/users/me/overview` 里的用户资料背景图。 |
| `target_profile.profile_bg_img` | 查看别人资料卡时,对方的主页背景图。 |
## 错误处理
| HTTP | code | 场景 |
| --- | --- | --- |
| `400` | `INVALID_ARGUMENT` | `url` 为空、不是 HTTP(S) 绝对地址或长度超过限制。 |
| `401` | `UNAUTHORIZED` | 未登录或 token 无效。 |
| `403` | `PROFILE_REQUIRED` | 当前用户资料未完成。 |
| `404` | `NOT_FOUND` | 当前用户不存在。 |
| `502` | `UPSTREAM_ERROR` | user-service 不可用。 |
相关 IM无。该接口只保存用户资料字段不发送 IM不触发背包或装扮刷新。

View File

@ -1,118 +0,0 @@
# 任务系统 Flutter 对接
## 获取任务页
地址:`GET /api/v1/tasks/tabs`
参数:无。需要登录态。
返回值:
```json
{
"sections": [
{
"section": "daily",
"server_time_ms": 1781097600000,
"next_refresh_at_ms": 1781136000000,
"items": [
{
"task_id": "task_xxx",
"task_type": "daily",
"category": "gift",
"audience_type": "all",
"metric_type": "gift_spend_coin",
"title": "普通礼物消耗金币",
"description": "",
"icon_key": "gift",
"icon_url": "",
"action_type": "gift_panel",
"action_param": "",
"action_payload": {},
"action_payload_json": "{}",
"dimension_filter_json": "{}",
"target_value": 10000,
"target_unit": "coin",
"progress_value": 1200,
"reward_coin_amount": 200,
"status": "in_progress",
"claimable": false,
"task_day": "2026-06-11",
"server_time_ms": 1781097600000,
"next_refresh_at_ms": 1781136000000,
"sort_order": 10,
"version": 1
}
]
},
{
"section": "exclusive",
"server_time_ms": 1781097600000,
"next_refresh_at_ms": 1781136000000,
"items": []
}
],
"server_time_ms": 1781097600000,
"next_refresh_at_ms": 1781136000000
}
```
说明:
- `section=daily` 是每日任务,按服务端 UTC 00:00 刷新。
- `section=exclusive` 是专属/新手任务,`task_day` 固定为 `lifetime`
- `claimable=true` 显示领取按钮。
- `status=claimed` 显示已领取。
- 其他状态显示去完成按钮,按 `action_type` 跳转。
## 领取任务奖励
地址:`POST /api/v1/tasks/claim`
参数:
```json
{
"task_id": "task_xxx",
"task_type": "daily",
"task_day": "2026-06-11",
"command_id": "app-generated-uuid"
}
```
返回值:
```json
{
"claim_id": "tclaim_xxx",
"task_id": "task_xxx",
"task_type": "daily",
"task_day": "2026-06-11",
"reward_coin_amount": 200,
"status": "granted",
"wallet_transaction_id": "wtx_xxx",
"granted_at_ms": 1781097600000,
"claimed": true
}
```
说明:
- `command_id` 由 App 每次点击领取时生成;同一次重试继续使用同一个 `command_id`
- 每日任务只能领取当天 `task_day`
- 专属任务领取时传列表里的 `task_day`,通常是 `lifetime`
## 跳转类型
| action_type | action_param |
| --- | --- |
| `none` | 不跳转 |
| `room_random` | 随机进入一个有人的房间;没有有人的房间时进入随机房间 |
| `room_random_game` | `game_id`,进入房间后打开指定游戏 |
| `wallet` | 打开钱包 |
| `gift_panel` | 进入房间后打开礼物面板 |
| `gift_panel_specific` | `gift_id`,进入房间后打开礼物面板并定位指定礼物 |
## 相关 IM
无。任务进度来自服务端事实事件,领取结果以接口返回和 `wallet_transaction_id` 为准。

View File

@ -1,212 +0,0 @@
# 公共跳转参数
## 地址
无。
这不是一个接口是后台运营配置、H5 bridge、IM custom data 共用的一套 App 跳转参数。
## 用在哪里
- Admin Banner`type``APP``param` 填下面的 JSON。
- Admin 开屏配置:`type``APP``param` 填下面的 JSON。
- Admin 弹窗配置:`跳转类型``APP`,使用公共跳转组件生成下面的 JSON。
- H5 bridge通过 `taskAction` 或其它业务 action 传下面的 JSON。
- 全服飘窗 IM`jump_type``jump_param``jump_payload`
## 参数
### 钱包
打开钱包页。
```json
{"type":"wallet"}
```
### 指定房间
进入指定房间。
```json
{"type":"room","room_id":"ROOM_ID"}
```
### 随机房间
进入一个随机公开房间。
```json
{"type":"room_random"}
```
### 指定房内拉起窗口
进入指定房间后打开房内窗口。
```json
{"type":"room_window","room_id":"ROOM_ID","window":"gift_panel"}
```
### 随机房内拉起窗口
进入随机公开房间后打开房内窗口。
```json
{"type":"room_random_window","window":"gift_panel"}
```
### 指定房内拉起游戏
进入指定房间后打开指定游戏。
```json
{"type":"room_game","room_id":"ROOM_ID","game_id":"GAME_ID"}
```
### 随机房内拉起游戏
进入随机公开房间后打开指定游戏。
```json
{"type":"room_random_game","game_id":"GAME_ID"}
```
### Explore 游戏 H5
App 先切到 Explore tab再打开对应的 H5。
```json
{"type":"explore_game","game_code":"game_dice"}
```
`game_code` 可填:
- `game_dice`:打开 Explore 的 Dice Battle H5对应 Flutter H5 key `game_dice`
- `game_rock`:打开 Explore 的 Rock Paper Scissors H5对应 Flutter H5 key `game_rock`
## window 可填值
### 游戏窗口
打开房内游戏窗口。带 `game_id` 时直接打开这个游戏;不带 `game_id` 时打开游戏菜单。
```json
{"type":"room_window","room_id":"ROOM_ID","window":"game_window","game_id":"GAME_ID"}
```
### 礼物面板
打开礼物面板。带 `gift_id` 时预选这个礼物。
```json
{"type":"room_window","room_id":"ROOM_ID","window":"gift_panel","gift_id":"GIFT_ID"}
```
### 领红包弹窗
打开领红包弹窗。带 `red_packet_no` 时打开指定红包。
```json
{"type":"room_window","room_id":"ROOM_ID","window":"red_packet_claim","red_packet_no":"RED_PACKET_NO"}
```
### 转盘窗口
打开房内转盘窗口。
```json
{"type":"room_window","room_id":"ROOM_ID","window":"wheel"}
```
## Admin 配置方式
后台公共跳转组件里:
- `跳转类型``APP`
- `APP 目标` 选需要的目标。
- 目标是 `Explore 游戏 H5` 时,继续选择 `Game Code`
- 保存后后台会生成 `param` JSON。
Explore 游戏 H5 的后台示例:
```json
{"type":"explore_game","game_code":"game_rock"}
```
## 返回值
无。
这是 App 内部跳转参数,没有服务端返回值。
## 相关 IM
全服飘窗使用 IM custom message。App 会解析 `jump_type``jump_param``jump_payload`
### 指定 jump_payload
```json
{
"broadcast_type": "game_win",
"room_id": "ROOM_ID",
"game_id": "GAME_ID",
"coins": 1000,
"jump_type": "room_window",
"jump_payload": {
"room_id": "ROOM_ID",
"window": "gift_panel",
"gift_id": "GIFT_ID"
}
}
```
### 跳钱包
```json
{
"broadcast_type": "game_win",
"coins": 1000,
"jump_type": "wallet",
"jump_payload": {}
}
```
### 随机房拉起游戏
```json
{
"broadcast_type": "game_win",
"coins": 1000,
"jump_type": "room_random_game",
"jump_payload": {
"game_id": "GAME_ID"
}
}
```
### 跳 Explore 游戏 H5
```json
{
"broadcast_type": "game_win",
"coins": 1000,
"jump_type": "explore_game",
"jump_payload": {
"game_code": "game_dice"
}
}
```
也可以把 `game_code` 放在 `jump_param`
```json
{
"broadcast_type": "game_win",
"coins": 1000,
"jump_type": "explore_game",
"jump_param": "game_rock"
}
```
没有 `jump_type`App 使用 IM 里的 `room_id``game_id` 作为默认跳转。

View File

@ -101,7 +101,7 @@ Content-Type: application/json
| `seat_count` | 否 | int | 不传或传 `0` 使用后台默认麦位数;正数必须命中后台启用座位数。 | | `seat_count` | 否 | int | 不传或传 `0` 使用后台默认麦位数;正数必须命中后台启用座位数。 |
| `mode` | 是 | string | 当前传 `voice`。 | | `mode` | 是 | string | 当前传 `voice`。 |
| `room_name` | 是 | string | 房间标题,最长 128 个字符。 | | `room_name` | 是 | string | 房间标题,最长 128 个字符。 |
| `room_avatar` | 是 | string | 房间封面 URL不传或空字符串会返回 `INVALID_ARGUMENT`。 | | `room_avatar` | 否 | string | 房间头像 URL为空时服务端写默认头像。 |
| `room_description` | 否 | string | 房间简介,最长 512 个字符。 | | `room_description` | 否 | string | 房间简介,最长 512 个字符。 |
成功响应: 成功响应:
@ -401,3 +401,4 @@ String newCommandId(String action) {
| `/rooms/join` 返回 `PERMISSION_DENIED` | 可能被 ban 或锁房密码错误;不调用 `joinGroup`。 | | `/rooms/join` 返回 `PERMISSION_DENIED` | 可能被 ban 或锁房密码错误;不调用 `joinGroup`。 |
| IM `joinGroup` 群不存在 | 当前不应出现;记录 `request_id``room_id``group_id` 上报。 | | IM `joinGroup` 群不存在 | 当前不应出现;记录 `request_id``room_id``group_id` 上报。 |
| IM 登录票据过期 | 重新调用 `/api/v1/im/usersig``login`。 | | IM 登录票据过期 | 重新调用 `/api/v1/im/usersig``login`。 |

View File

@ -1,330 +0,0 @@
# 幸运礼物 Flutter 对接
本文只保留 App 对接需要的信息地址、参数、返回值、IM 字段。服务端实现细节看 [幸运礼物实现文档](../幸运礼物实现文档.md),礼物面板通用字段看 [礼物 Tab Flutter 对接](礼物TabFlutter对接.md)。
## 1. 送礼前检查
地址:
```http
POST /api/v1/activities/lucky-gifts/check
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
请求参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID。 |
| `gift_id` | string | 是 | 礼物 ID。 |
| `pool_id` | string | 是 | 幸运礼物奖池 ID例如 `lucky``super_lucky` 或后台配置值。 |
请求示例:
```json
{
"room_id": "lalu_room_001",
"gift_id": "rose",
"pool_id": "super_lucky"
}
```
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `enabled` | bool | 是否可参与幸运礼物抽奖。 |
| `reason` | string | 不可参与原因;可为空。 |
| `room_id` | string | 房间 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `pool_id` | string | 奖池 ID。 |
| `gift_price` | int64 | 当前规则参考礼物价格,只展示用,扣费以后端送礼结果为准。 |
| `rule_version` | int64 | 命中的幸运礼物规则版本。 |
| `target_rtp_ppm` | int64 | 目标 RTPppm 口径;`950000` 表示 95%。 |
| `experience_pool` | string | 当前用户体验池,例如 `novice``normal``advanced`。 |
返回示例:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"enabled": true,
"reason": "",
"room_id": "lalu_room_001",
"gift_id": "rose",
"pool_id": "super_lucky",
"gift_price": 100,
"rule_version": 12,
"target_rtp_ppm": 950000,
"experience_pool": "novice"
}
}
```
## 2. 发送幸运礼物
地址:
```http
POST /api/v1/rooms/gift/send
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
请求参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID。 |
| `command_id` | string | 是 | 本次送礼幂等键;同一次重试必须复用。 |
| `target_type` | string | 否 | 当前固定传 `user`;为空后端按 `user`。 |
| `target_user_ids` | int64[] | 是 | 收礼用户列表;当前只传 1 个用户。 |
| `target_user_id` | int64 | 否 | 旧单目标兼容字段,新代码优先用 `target_user_ids`。 |
| `gift_id` | string | 是 | 礼物 ID。 |
| `gift_count` | int32 | 是 | 礼物数量,必须大于 0批量送礼会聚合返回一次中奖结果。 |
| `pool_id` | string | 是 | 幸运礼物奖池 ID普通礼物不传。 |
请求示例:
```json
{
"room_id": "lalu_room_001",
"command_id": "gift_1779259000000_10001_rose_01",
"target_type": "user",
"target_user_ids": [10002],
"gift_id": "rose",
"gift_count": 9,
"pool_id": "super_lucky"
}
```
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `result.applied` | bool | 本次命令是否实际落地;重复提交同一 `command_id` 可能为 `false`。 |
| `result.room_version` | int64 | 房间版本。 |
| `result.server_time_ms` | int64 | 服务端时间。 |
| `billing_receipt_id` | string | 送礼扣费回执。 |
| `room_heat` | int64 | 送礼后的房间热度。 |
| `gift_rank` | array | 当前房间礼物榜。 |
| `room` | object | 房间快照。 |
| `rocket` | object | 房间火箭状态。 |
| `lucky_gift` | object | 幸运礼物抽奖结果;普通礼物可为空。 |
`lucky_gift` 返回字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `enabled` | bool | 本次是否触发幸运礼物。 |
| `draw_id` | string | 聚合抽奖 ID。 |
| `command_id` | string | 本次送礼命令 ID。 |
| `pool_id` | string | 奖池 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `rule_version` | int64 | 规则版本。 |
| `experience_pool` | string | 用户体验池。 |
| `selected_tier_id` | string | 命中奖档 ID。 |
| `multiplier_ppm` | int64 | 倍率 ppm`2000000` 表示 2x。 |
| `base_reward_coins` | int64 | 基础奖池返奖金币。 |
| `room_atmosphere_reward_coins` | int64 | 房间气氛池返奖金币。 |
| `activity_subsidy_coins` | int64 | 活动补贴返奖金币。 |
| `effective_reward_coins` | int64 | 实际中奖金币0 表示未中奖。 |
| `reward_status` | string | `granted` 已入账;`pending`/`granting` 到账处理中;`failed` 需要人工处理。 |
| `stage_feedback` | bool | 是否有阶段反馈表现。 |
| `high_multiplier` | bool | 是否高倍率中奖。 |
| `created_at_ms` | int64 | 抽奖创建时间。 |
| `wallet_transaction_id` | string | 返奖钱包交易 ID已入账时有值。 |
| `coin_balance_after` | int64 | 返奖入账后的 COIN 可用余额。 |
返回示例:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"result": {
"applied": true,
"room_version": 14,
"server_time_ms": 1779259000000
},
"billing_receipt_id": "receipt-lucky",
"room_heat": 123456,
"gift_rank": [],
"room": {},
"rocket": {},
"lucky_gift": {
"enabled": true,
"draw_id": "lucky_draw_test",
"command_id": "gift_1779259000000_10001_rose_01",
"pool_id": "super_lucky",
"gift_id": "rose",
"rule_version": 12,
"experience_pool": "novice",
"selected_tier_id": "novice_2x",
"multiplier_ppm": 2000000,
"base_reward_coins": 1800,
"room_atmosphere_reward_coins": 0,
"activity_subsidy_coins": 0,
"effective_reward_coins": 1800,
"reward_status": "granted",
"stage_feedback": false,
"high_multiplier": false,
"created_at_ms": 1779259000000,
"wallet_transaction_id": "wallet_tx_lucky",
"coin_balance_after": 8800
}
}
}
```
客户端处理:
- `reward_status=granted``effective_reward_coins > 0`:立即展示中奖,并用 `coin_balance_after` 覆盖本地金币余额。
- `reward_status=pending``granting`:展示“到账处理中”,不要本地加金币,不要生成新 `command_id` 重发。
- `effective_reward_coins=0`:未中奖,只展示普通送礼表现。
- 同一用户同一房间连续发送幸运礼物时,客户端需要本地排队;重试必须复用原 `command_id`
## 3. IM 字段
### 3.1 送礼房间 IM
用途:房间内播放普通送礼动效、同步热度和礼物榜。幸运礼物也会先产生这条送礼 IM。
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `room_gift_sent` |
| `Data.event_type` | `room_gift_sent` |
`Data` 核心字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | string | 房间事件 ID用于去重。 |
| `room_id` | string | 房间 ID。 |
| `room_version` | int64 | 房间版本。 |
| `occurred_at_ms` | int64 | 事件发生时间。 |
| `actor_user_id` | string/int64 | 送礼用户 ID。 |
| `target_user_id` | string/int64 | 收礼用户 ID。 |
| `gift_value` | int64 | 本次送礼房间价值/热度口径。 |
| `room_heat` | int64 | 送礼后的房间热度。 |
| `attributes.gift_id` | string | 礼物 ID。 |
| `attributes.pool_id` | string | 幸运礼物奖池 ID普通礼物可为空。 |
| `attributes.gift_count` | string | 礼物数量,字符串格式。 |
| `attributes.billing_receipt_id` | string | 钱包扣费回执。 |
| `attributes.coin_spent` | string | 本次实际消耗金币,字符串格式。 |
| `attributes.gift_point_added` | string | 主播礼物积分增量,字符串格式。 |
| `attributes.price_version` | string | 价格版本。 |
示例:
```json
{
"event_type": "room_gift_sent",
"event_id": "room_event_xxx",
"room_id": "lalu_room_001",
"room_version": 14,
"occurred_at_ms": 1779259000000,
"actor_user_id": "10001",
"target_user_id": "10002",
"gift_value": 900,
"room_heat": 123456,
"attributes": {
"gift_id": "rose",
"pool_id": "super_lucky",
"gift_count": "9",
"billing_receipt_id": "receipt-lucky",
"coin_spent": "900",
"gift_point_added": "900",
"price_version": "v1"
}
}
```
### 3.2 幸运礼物开奖 IM
用途:房间公屏展示幸运礼物中奖结果、其他用户同步中奖表现。当前用户自己的余额以 HTTP 返回的 `coin_balance_after` 为准,不等这条 IM。
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `lucky_gift_drawn` |
| `Data.event_type` | `lucky_gift_drawn` |
`Data` 核心字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | string | 开奖事件 ID用于去重。 |
| `app_code` | string | App Code。 |
| `draw_id` | string | 聚合抽奖 ID。 |
| `command_id` | string | 送礼命令 ID。 |
| `pool_id` | string | 奖池 ID。 |
| `room_id` | string | 房间 ID。 |
| `gift_id` | string | 礼物 ID。 |
| `gift_count` | int32 | 礼物数量。 |
| `user_id` | int64 | 中奖用户 ID。 |
| `sender_user_id` | int64 | 送礼用户 ID。 |
| `target_user_id` | int64 | 收礼用户 ID。 |
| `visible_region_id` | int64 | 展示区域 ID。 |
| `coin_spent` | int64 | 本次送礼消耗金币。 |
| `rule_version` | int64 | 规则版本。 |
| `experience_pool` | string | 用户体验池。 |
| `selected_tier_id` | string | 命中奖档 ID。 |
| `multiplier_ppm` | int64 | 命中倍率 ppm。 |
| `base_reward_coins` | int64 | 基础奖池返奖金币。 |
| `room_atmosphere_reward_coins` | int64 | 房间气氛池返奖金币。 |
| `activity_subsidy_coins` | int64 | 活动补贴返奖金币。 |
| `effective_reward_coins` | int64 | 实际中奖金币。 |
| `stage_feedback` | bool | 是否有阶段反馈表现。 |
| `high_multiplier` | bool | 是否高倍率中奖。 |
| `created_at_ms` | int64 | 抽奖创建时间。 |
示例:
```json
{
"event_type": "lucky_gift_drawn",
"event_id": "lucky_gift_drawn:lucky_draw_test",
"app_code": "lalu",
"draw_id": "lucky_draw_test",
"command_id": "gift_1779259000000_10001_rose_01",
"pool_id": "super_lucky",
"room_id": "lalu_room_001",
"gift_id": "rose",
"gift_count": 9,
"user_id": 10001,
"sender_user_id": 10001,
"target_user_id": 10002,
"visible_region_id": 688,
"coin_spent": 900,
"rule_version": 12,
"experience_pool": "novice",
"selected_tier_id": "novice_2x",
"multiplier_ppm": 2000000,
"base_reward_coins": 1800,
"room_atmosphere_reward_coins": 0,
"activity_subsidy_coins": 0,
"effective_reward_coins": 1800,
"stage_feedback": false,
"high_multiplier": false,
"created_at_ms": 1779259000000
}
```
## 4. 状态处理
| 场景 | App 处理 |
| --- | --- |
| `lucky_gift` 为空 | 当普通礼物处理。 |
| `effective_reward_coins=0` | 未中奖,不更新中奖余额。 |
| `reward_status=granted` | 已入账;用 `coin_balance_after` 更新余额。 |
| `reward_status=pending` / `granting` | 到账处理中;等待钱包通知、刷新余额或后端补偿。 |
| `reward_status=failed` | 展示异常状态,记录 `request_id/draw_id/command_id` 方便排查。 |
| HTTP 超时 | 保留原 `command_id` 重试,不生成新命令。 |

View File

@ -1,69 +0,0 @@
# 开屏配置 Flutter 对接
## 地址
`GET /api/v1/app/splash-screens`
公开接口,不需要登录。返回后台 `APP配置 -> 开屏配置` 中当前有效的开屏列表。
## 参数
| 参数 | 位置 | 必填 | 说明 |
| --- | --- | --- | --- |
| `X-App-Code` | Header | 否 | App 编码,默认 `lalu`。 |
| `X-App-Package` | Header | 否 | 包名或 Bundle ID用于服务端解析 App。 |
| `platform` | Query | 否 | `android``ios`。不传时读取 `X-App-Platform` / `X-Platform`。 |
| `region_id` | Query | 否 | 当前用户区域 ID。 |
| `country` | Query | 否 | 当前用户国家码。不传时读取 `X-Country-Code` / `X-App-Country`。 |
示例:
```http
GET /api/v1/app/splash-screens?platform=android&region_id=1&country=CN
X-App-Code: lalu
X-App-Platform: android
```
## 返回值
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"items": [
{
"id": 12,
"cover_url": "https://cdn.example.com/splash.png",
"type": "h5",
"param": "https://h5.example.com/splash",
"platform": "android",
"sort_order": 1,
"region_id": 1,
"country_code": "CN",
"description": "活动开屏",
"starts_at_ms": 1700000000000,
"ends_at_ms": 1800000000000,
"updated_at_ms": 1700000003000
}
],
"total": 1
}
}
```
| 字段 | 说明 |
| --- | --- |
| `items` | 当前可展示的开屏配置列表,已按 `sort_order ASC, id DESC` 排序。 |
| `cover_url` | 开屏图片地址。 |
| `type` | `h5``app`。 |
| `param` | `type=h5` 时是 H5 链接;`type=app` 时由客户端解释。 |
| `platform` | `android``ios`。 |
| `region_id` | 配置限制的区域 ID为空或 0 表示全部区域。 |
| `country_code` | 配置限制的国家码;为空表示全部国家。 |
| `starts_at_ms` / `ends_at_ms` | 投放时间。0 表示不限。 |
## 相关 IM
无。

View File

@ -1,72 +0,0 @@
# 弹窗配置 Flutter 对接
## 地址
`GET /api/v1/app/popups`
公开接口,不需要登录。返回后台 `APP配置 -> 弹窗配置` 中当前有效的弹窗列表。
## 参数
| 参数 | 位置 | 必填 | 说明 |
| --- | --- | --- | --- |
| `X-App-Code` | Header | 否 | App 编码,默认 `lalu`。 |
| `X-App-Package` | Header | 否 | 包名或 Bundle ID用于服务端解析 App。 |
示例:
```http
GET /api/v1/app/popups
X-App-Code: lalu
```
## 返回值
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"items": [
{
"id": 21,
"code": "new-user-popup",
"name": "新人活动",
"image_url": "https://cdn.example.com/popup.png",
"jump_type": "h5",
"jump_url": "https://h5.example.com/new-user",
"display_period_days": 1,
"sort_order": 1,
"starts_at_ms": 1700000000000,
"ends_at_ms": 1800000000000,
"updated_at_ms": 1700000004000
}
],
"total": 1
}
}
```
| 字段 | 说明 |
| --- | --- |
| `items` | 当前可展示的弹窗配置列表,已按 `sort_order ASC, id DESC` 排序。 |
| `code` | 弹窗编码,同一个 App 下唯一,用于 Flutter 本地频控和埋点识别。 |
| `name` | 弹窗名字。 |
| `image_url` | 弹窗图片地址,可能为空。 |
| `jump_type` | `h5``app`。 |
| `jump_url` | `jump_type=h5` 时是 H5 链接;`jump_type=app` 时由 Flutter 解释。 |
| `display_period_days` | 展示周期。`0` 表示每次打开 App 都展示;`1` 表示 1 天展示一次;`2``3``4` 同理。 |
| `starts_at_ms` / `ends_at_ms` | 弹窗生效时间。`0` 表示不限。 |
Flutter 处理规则:
1. 调接口后按 `items` 顺序取第一个满足本地频控的弹窗。
2. 用 `popup_last_show:{code}:{updated_at_ms}` 记录本地最后展示时间。
3. `display_period_days=0` 不读本地间隔,每次打开都可以展示。
4. `display_period_days>0` 时,距离上次展示满对应天数后再展示。
5. 用户点击弹窗时,`jump_type=h5` 打开 H5`jump_type=app` 走 App 内路由。
## 相关 IM
无。

View File

@ -1,179 +0,0 @@
# Flutter 心跳接口对接
## 1. APP 心跳
### 接口地址
```http
POST /api/v1/app/heartbeat
Authorization: Bearer <access_token>
```
### 参数
Body 不需要传参数。
Header
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `Authorization` | 是 | 登录 token。 |
| `X-Device-ID` | 否 | 设备 ID有就传服务端只做会话审计。 |
### 返回值
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"accepted": true,
"heartbeat_at_ms": 1778000005000,
"server_time_ms": 1778000005000
}
}
```
| 字段 | 说明 |
| --- | --- |
| `data.accepted` | 是否接受本次 APP 心跳。 |
| `data.heartbeat_at_ms` | 本次 APP 心跳写入时间UTC epoch milliseconds。 |
| `data.server_time_ms` | 服务端时间UTC epoch milliseconds。 |
相关 IM无。
## 2. 房间心跳
### 接口地址
```http
POST /api/v1/rooms/heartbeat
Authorization: Bearer <access_token>
Content-Type: application/json
```
### 参数
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 房间 ID。 |
| `command_id` | string | 是 | 本次心跳命令 ID。同一次请求重试复用同一个值下一次心跳换新的值。 |
请求示例:
```json
{
"room_id": "lalu_xxx",
"command_id": "cmd_room_heartbeat_lalu_xxx_123_001"
}
```
### 返回值
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"result": {
"applied": true,
"room_version": 18,
"server_time_ms": 1778000005000
},
"user": {
"user_id": 123,
"role": "audience",
"joined_at_ms": 1778000000000,
"last_seen_at_ms": 1778000005000
},
"room": {
"room_id": "lalu_xxx",
"version": 18,
"online_users": []
}
}
}
```
| 字段 | 说明 |
| --- | --- |
| `data.result.applied` | 本次心跳是否写入成功。 |
| `data.result.room_version` | 房间版本。 |
| `data.user.last_seen_at_ms` | 当前用户房间 presence 最近刷新时间。 |
| `data.room` | 刷新后的房间快照。 |
相关 IM无。房间心跳只刷新 room-service presence不会广播独立 IM。
## 3. 麦位心跳
### 接口地址
```http
POST /api/v1/rooms/mic/heartbeat
Authorization: Bearer <access_token>
Content-Type: application/json
```
### 参数
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 房间 ID。 |
| `command_id` | string | 是 | 本次麦位心跳命令 ID。同一次请求重试复用同一个值下一次心跳换新的值。 |
| `mic_session_id` | string | 是 | 当前麦位会话 ID必须使用上麦返回的 `mic_session_id`。 |
| `target_user_id` | int64 | 否 | 传 `0` 或不传表示当前登录用户自己。 |
请求示例:
```json
{
"room_id": "lalu_xxx",
"command_id": "cmd_mic_heartbeat_lalu_xxx_123_001",
"mic_session_id": "mic_xxx",
"target_user_id": 0
}
```
### 返回值
```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
}
]
}
}
}
```
| 字段 | 说明 |
| --- | --- |
| `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` 表示当前会话已确认发流,可以继续麦位心跳。 |
相关 IM无独立心跳 IM。收到 `room_mic_down` 后停止麦位心跳。

View File

@ -1,322 +0,0 @@
# 房内猜拳 Flutter 对接
本文只记录房内猜拳 App 端 HTTP 接口和腾讯 IM 自定义消息字段。独立猜拳本阶段不接入。
## 1. 基本约定
- 所有 HTTP 地址都有 `/api/v1` 前缀。
- 所有接口都需要 `Authorization: Bearer <access_token>`
- 成功响应统一是 gateway envelope
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {}
}
```
- 所有时间字段都是 Unix epoch milliseconds。
- Flutter 模型里用户 ID、挑战 ID、礼物 ID 建议统一按 `String` 保存。
- HTTP 返回的发起人和应战人资料在 `challenge.initiator.user``challenge.challenger.user`
- IM 里的发起人和应战人也必须带 `nickname``avatar`;客户端不要为了基础展示再查用户资料。
## 2. 通用挑战单字段
`challenge` 字段在列表、详情、发起、应战接口中结构一致。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `challenge_id` | string | 挑战单 ID。 |
| `room_id` | string | 房间 ID。 |
| `region_id` | number | 用户语区 ID。 |
| `status` | string | `pending``matched``revealing``finished``timeout``settlement_failed``cancelled`。 |
| `stake_gift_id` | string | 下注礼物 IDFlutter 优先用这个字段。 |
| `stake_gift_id_number` | number | 下注礼物 ID 数字值。 |
| `stake_coin` | number | 本局礼物对应金币价值。 |
| `initiator` | object | 发起人。 |
| `challenger` | object | 应战人;`pending` 时可能为空用户。 |
| `winner_user_id` | string | 胜者用户 ID平局或未结算为空或 `0`。 |
| `winner_user_id_number` | number | 胜者用户 ID 数字值。 |
| `settlement_status` | string | 结算状态,例如 `pending``settled``failed`。 |
| `failure_reason` | string | 结算失败原因。 |
| `timeout_at_ms` | number | 无人应战自动过期时间。 |
| `reveal_at_ms` | number | 揭晓时间。 |
| `created_at_ms` | number | 创建时间。 |
| `matched_at_ms` | number | 应战成功时间。 |
| `settled_at_ms` | number | 结算时间。 |
| `updated_at_ms` | number | 更新时间。 |
`initiator``challenger` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `user_id` | string | 用户 IDFlutter 优先用这个字段。 |
| `user_id_number` | number | 用户 ID 数字值。 |
| `gesture` | string | `rock``paper``scissors`。 |
| `result` | string | `win``lose``draw`;未结算时为空。 |
| `balance_after` | number | 结算后的金币余额。 |
| `joined_at_ms` | number | 参与时间。 |
| `user.user_id` | string | 展示用户 ID。 |
| `user.display_user_id` | string | 短号或展示号。 |
| `user.nickname` | string | 昵称;发起人和应战人都要有。 |
| `user.avatar` | string | 头像;发起人和应战人都要有。 |
## 3. 查询配置
地址:
```http
GET /api/v1/games/room-rps/config
```
参数:无。
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `config.status` | string | `active``disabled`。 |
| `config.challenge_timeout_ms` | number | 发起后无人应战的超时时间,默认 10 分钟。 |
| `config.reveal_countdown_ms` | number | 双方出拳后的揭晓倒计时,默认 3 秒。 |
| `config.stake_gifts` | array | 礼物下注档位,固定 4 档。 |
| `server_time_ms` | number | 服务端时间。 |
`stake_gifts` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `gift_id` | string | 礼物 IDFlutter 优先用这个字段提交。 |
| `gift_id_number` | number | 礼物 ID 数字值。 |
| `gift_name` | string | 礼物名称。 |
| `gift_icon_url` | string | 礼物图标。 |
| `gift_price_coin` | number | 礼物价格金币。 |
| `enabled` | bool | 是否启用。 |
| `sort_order` | number | 排序。 |
相关 IM无。
## 4. 查询房间挑战单
地址:
```http
GET /api/v1/games/room-rps/challenges
```
参数:
| 字段 | 位置 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | query | 是 | 房间 ID。 |
| `status` | query | 否 | 挑战状态。 |
| `page_size` | query | 否 | 页大小,默认 20。 |
| `cursor` | query | 否 | 下一页游标。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `challenges` | array | 挑战单列表,结构见通用挑战单字段。 |
| `next_cursor` | string | 下一页游标。 |
| `page_size` | number | 本次页大小。 |
| `server_time_ms` | number | 服务端时间。 |
相关 IM无。客户端漏掉 IM 后,用这个接口补拉当前房间挑战单。
## 5. 发起挑战
地址:
```http
POST /api/v1/games/room-rps/challenges/create
Content-Type: application/json
```
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 房间 ID。 |
| `gift_id` | number | 是 | 后台配置的礼物档位 ID。 |
| `gesture` | string | 是 | `rock``paper``scissors`。 |
请求示例:
```json
{
"room_id": "room_1001",
"gift_id": 10001,
"gesture": "rock"
}
```
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `challenge` | object | 挑战单,结构见通用挑战单字段。 |
| `server_time_ms` | number | 服务端时间。 |
相关 IM`room_rps_challenge_created`
## 6. 应战
地址:
```http
POST /api/v1/games/room-rps/challenges/{challenge_id}/accept
Content-Type: application/json
```
参数:
| 字段 | 位置 | 必填 | 说明 |
| --- | --- | --- | --- |
| `challenge_id` | path | 是 | 挑战单 ID。 |
| `gesture` | body | 是 | `rock``paper``scissors`。 |
请求示例:
```json
{
"gesture": "paper"
}
```
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `challenge` | object | 应战后的挑战单,结构见通用挑战单字段。 |
| `server_time_ms` | number | 服务端时间。 |
相关 IM
- `room_rps_challenge_accepted`
- `room_rps_reveal_countdown`
- `room_rps_finished`
## 7. 查询挑战详情
地址:
```http
GET /api/v1/games/room-rps/challenges/{challenge_id}
```
参数:
| 字段 | 位置 | 必填 | 说明 |
| --- | --- | --- | --- |
| `challenge_id` | path | 是 | 挑战单 ID。 |
返回值:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `challenge` | object | 挑战单,结构见通用挑战单字段。 |
| `server_time_ms` | number | 服务端时间。 |
相关 IM无。客户端收到 IM 后,可以用这个接口刷新挑战事实。
## 8. 腾讯 IM 字段
房间群消息使用腾讯 IM `TIMCustomElem``data` 是 JSON 字符串。Flutter 只需要解析下面字段。
通用字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `event_id` | string | IM 事件 ID客户端用它去重。 |
| `event_type` | string | 事件类型。 |
| `room_id` | string | 房间 ID。 |
| `challenge_id` | string | 挑战单 ID。 |
| `server_time_ms` | number | 服务端时间。 |
| `challenge` | object | 挑战单快照,结构见通用挑战单字段。 |
`challenge.initiator.user` 字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `user_id` | string | 是 | 发起人用户 ID。 |
| `display_user_id` | string | 否 | 发起人展示号。 |
| `nickname` | string | 是 | 发起人昵称。 |
| `avatar` | string | 是 | 发起人头像。 |
`challenge.challenger.user` 字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `user_id` | string | 应战后是 | 应战人用户 ID。 |
| `display_user_id` | string | 否 | 应战人展示号。 |
| `nickname` | string | 应战后是 | 应战人昵称。 |
| `avatar` | string | 应战后是 | 应战人头像。 |
IM 事件:
| `event_type` | 触发时机 | Flutter 处理 |
| --- | --- | --- |
| `room_rps_challenge_created` | 用户发起挑战后 | 展示 `[PK]` 入口、发起人头像昵称、礼物和倒计时。此时 `challenger` 可以为空。 |
| `room_rps_challenge_accepted` | 有用户应战后 | 锁定挑战入口,展示双方头像昵称。 |
| `room_rps_reveal_countdown` | 双方出拳后 | 播放 3 秒揭晓动画,展示双方头像昵称。 |
| `room_rps_finished` | 结算完成或平局退款后 | 展示胜负、双方头像昵称、余额或失败提示。 |
IM 示例:
```json
{
"event_id": "room_rps_finished:rps_10001",
"event_type": "room_rps_finished",
"room_id": "room_1001",
"challenge_id": "rps_10001",
"server_time_ms": 1780000003000,
"challenge": {
"challenge_id": "rps_10001",
"room_id": "room_1001",
"status": "finished",
"stake_gift_id": "10001",
"stake_gift_id_number": 10001,
"stake_coin": 100,
"initiator": {
"user_id": "10001",
"user_id_number": 10001,
"gesture": "rock",
"result": "lose",
"balance_after": 900,
"joined_at_ms": 1780000000000,
"user": {
"user_id": "10001",
"display_user_id": "880001",
"nickname": "Alice",
"avatar": "https://cdn.example/alice.png"
}
},
"challenger": {
"user_id": "10002",
"user_id_number": 10002,
"gesture": "paper",
"result": "win",
"balance_after": 1200,
"joined_at_ms": 1780000001000,
"user": {
"user_id": "10002",
"display_user_id": "880002",
"nickname": "Bob",
"avatar": "https://cdn.example/bob.png"
}
},
"winner_user_id": "10002",
"winner_user_id_number": 10002,
"settlement_status": "settled",
"timeout_at_ms": 1780000600000,
"reveal_at_ms": 1780000003000,
"created_at_ms": 1780000000000,
"matched_at_ms": 1780000001000,
"settled_at_ms": 1780000003000,
"updated_at_ms": 1780000003000
}
}
```

View File

@ -1,277 +0,0 @@
# 房内贡献榜 Flutter 对接
本文描述语音房内 day/week/month 三个 tab 的贡献榜接口。客户端按当前 tab 独立请求,不再从进房、心跳或送礼接口里复用同一份榜单数据。
## 接口地址
本地开发地址:
```text
http://127.0.0.1:13000
```
```http
GET /api/v1/rooms/{room_id}/contribution-rank?period=day&limit=20
Authorization: Bearer <access_token>
X-App-Code: lalu
```
说明:
| 项 | 说明 |
| --- | --- |
| 鉴权 | 必须登录。gateway 使用 token 里的 `user_id` 作为 viewer。 |
| 房间校验 | viewer 必须仍在当前房间内;不在房间时返回 403。 |
| 响应外壳 | 统一使用 `{code,message,request_id,data}`。 |
| 时间单位 | 所有 `*_ms` 都是 Unix epoch milliseconds统计窗口按 UTC 计算。 |
## 参数
Path 参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 房间 ID。只支持字母、数字、下划线和中横线最长 48 位。 |
Query 参数:
| 字段 | 类型 | 必填 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| `period` | string | 否 | `day` | 榜单周期。可选值:`day``week``month`。 |
| `limit` | int32 | 否 | `20` | 返回条数。必须大于 0服务端最大返回 `100`。 |
周期含义:
| `period` | 统计窗口 |
| --- | --- |
| `day` | UTC 当日 00:00:00 到次日 00:00:00区间为 `[start_at_ms, end_at_ms)`。 |
| `week` | UTC 自然周,周一 00:00:00 到下周一 00:00:00区间为 `[start_at_ms, end_at_ms)`。 |
| `month` | UTC 自然月1 日 00:00:00 到下月 1 日 00:00:00区间为 `[start_at_ms, end_at_ms)`。 |
请求示例:
```http
GET /api/v1/rooms/room_123/contribution-rank?period=week&limit=20
Authorization: Bearer <access_token>
X-App-Code: lalu
```
## 返回值
统一外壳:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {}
}
```
成功返回示例:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_room_rank",
"data": {
"room_id": "room_123",
"period": "week",
"start_at_ms": 1780329600000,
"end_at_ms": 1780934400000,
"total": 2,
"contribution_rank": [
{
"rank": 1,
"user_id": "43",
"score": 900,
"gift_value": 900,
"updated_at_ms": 1780400001000,
"profile": {
"user_id": "43",
"username": "Luna",
"avatar": "https://cdn.example/avatar/43.png",
"display_user_id": "10043",
"pretty_id": "888888",
"pretty_display_user_id": "888888",
"gender": "female",
"age": 22,
"country": "US",
"country_name": "United States",
"country_display_name": "United States",
"country_flag": "https://cdn.example/flags/us.png",
"vip": {},
"level": {},
"level_badges": {},
"badges": [],
"avatar_frame": {},
"profile_card": {},
"vehicle": {},
"mic_seat_animation": {},
"charm": 0
}
},
{
"rank": 2,
"user_id": "44",
"score": 520,
"gift_value": 520,
"updated_at_ms": 1780390001000,
"profile": {
"user_id": "44",
"username": "Mia",
"avatar": "https://cdn.example/avatar/44.png",
"display_user_id": "10044",
"gender": "",
"age": 0,
"country": "",
"country_name": "",
"country_display_name": "",
"country_flag": "",
"vip": {},
"level": {},
"level_badges": {},
"badges": [],
"avatar_frame": {},
"profile_card": {},
"vehicle": {},
"mic_seat_animation": {},
"charm": 0
}
}
],
"profiles": [
{
"user_id": "43",
"username": "Luna",
"avatar": "https://cdn.example/avatar/43.png",
"display_user_id": "10043",
"gender": "female",
"age": 22,
"country": "US",
"country_name": "United States",
"country_display_name": "United States",
"country_flag": "https://cdn.example/flags/us.png",
"vip": {},
"level": {},
"level_badges": {},
"badges": [],
"avatar_frame": {},
"profile_card": {},
"vehicle": {},
"mic_seat_animation": {},
"charm": 0
},
{
"user_id": "44",
"username": "Mia",
"avatar": "https://cdn.example/avatar/44.png",
"display_user_id": "10044",
"gender": "",
"age": 0,
"country": "",
"country_name": "",
"country_display_name": "",
"country_flag": "",
"vip": {},
"level": {},
"level_badges": {},
"badges": [],
"avatar_frame": {},
"profile_card": {},
"vehicle": {},
"mic_seat_animation": {},
"charm": 0
}
],
"server_time_ms": 1780400005000
}
}
```
`data` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `room_id` | string | 房间 ID。 |
| `period` | string | 服务端归一化后的周期:`day``week``month`。 |
| `start_at_ms` | int64 | 本次榜单统计窗口开始时间UTC。 |
| `end_at_ms` | int64 | 本次榜单统计窗口结束时间UTC。 |
| `total` | int64 | 当前房间、当前周期内有贡献的总用户数。 |
| `contribution_rank` | array | 当前页榜单项。空数组表示当前周期暂无贡献数据。 |
| `profiles` | array | 榜单用户的展示资料列表,顺序和 `contribution_rank` 一致。 |
| `server_time_ms` | int64 | 服务端当前时间,可用于展示更新时间。 |
`contribution_rank[]` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `rank` | int64 | 排名,从 1 开始。 |
| `user_id` | string | 上榜用户 IDHTTP JSON 固定按字符串解析。 |
| `score` | int64 | 榜单分值;当前等于 `gift_value`。 |
| `gift_value` | int64 | 统计窗口内该用户在本房间送礼贡献总值。 |
| `updated_at_ms` | int64 | 该用户本周期贡献记录更新时间。 |
| `profile` | object? | 当前用户展示资料。资料服务异常或用户资料缺失时可能为空。 |
`profile` / `profiles[]` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `user_id` | string | 用户长 ID。 |
| `username` | string | 昵称。 |
| `avatar` | string | 头像 URL。 |
| `display_user_id` | string | 展示 ID。 |
| `pretty_id` | string | 靓号 ID没有时为空或不返回。 |
| `pretty_display_user_id` | string | 靓号展示 ID没有时为空或不返回。 |
| `gender` | string | 性别展示值。 |
| `age` | int32 | 年龄。 |
| `country` | string | 国家码。 |
| `country_name` | string | 国家名称。 |
| `country_display_name` | string | 国家展示名。 |
| `country_flag` | string | 国家旗帜。 |
| `vip` | object | VIP 展示资料。 |
| `level` | object | 等级展示资料。 |
| `level_badges` | object | 等级徽章资料。 |
| `badges` | array | 当前佩戴徽章。 |
| `avatar_frame` | object | 当前佩戴头像框。 |
| `profile_card` | object | 当前佩戴资料卡。 |
| `vehicle` | object | 当前佩戴座驾。 |
| `mic_seat_animation` | object | 当前佩戴麦位动画。 |
| `charm` | int64 | 魅力值。 |
## 错误码
| HTTP | `code` | 场景 |
| --- | --- | --- |
| 400 | `INVALID_ARGUMENT` | `room_id` 格式错误、`period` 不是 `day/week/month``limit<=0` 或不是数字。 |
| 401 | `UNAUTHORIZED` | 未登录或 token 无效。 |
| 403 | `PROFILE_REQUIRED` | 登录用户未完成资料流程gateway profile gate 拦截。 |
| 403 | `PERMISSION_DENIED` | viewer 当前不在该房间,不能读取房内贡献榜。 |
| 502 | `UPSTREAM_ERROR` | room-service 或 user-service 调用失败。 |
失败返回示例:
```json
{
"code": "INVALID_ARGUMENT",
"message": "invalid argument",
"request_id": "req_bad_period"
}
```
## Flutter 调用规则
| 场景 | 处理 |
| --- | --- |
| 进入房间后首次展示 | `POST /api/v1/rooms/join` 成功、房间页建立后,请求当前默认 tab例如 `period=day`。 |
| tab 切换 | 按 `room_id + period` 单独请求本接口;不要把 day 的数据复制给 week/month。 |
| 送礼成功后 | 刷新当前展示的 `period`,或收到本地送礼成功事件后延迟刷新;不要依赖送礼接口返回榜单。 |
| 心跳 | `POST /api/v1/rooms/heartbeat` 只维护在线状态,不用它刷新榜单。 |
| 进房首包 | 新版进房继续传 `response_mode=lite`;贡献榜由本接口独立拉取。 |
| 空榜 | `contribution_rank=[]``profiles=[]``total=0` 时展示空状态。 |
## 相关 IM
本接口没有独立 IM 消息。房间内送礼动效、公屏礼物消息仍按现有送礼链路和房间 IM 事件处理;本接口只用于 Flutter 在面板打开、tab 切换或送礼后重拉榜单快照。

View File

@ -1,45 +1,60 @@
# 房间锁房 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 ```text
http://127.0.0.1:13000 http://127.0.0.1:13000
``` ```
接口: ## 请求方法
| 方法 | 地址 | 说明 | | 方法 | 接口 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| `POST` | `/api/v1/rooms/password` | 房主设置或清空房间密码 | | `POST` | `/api/v1/rooms/password` | 房主设置或清空房间入房密码 |
| `POST` | `/api/v1/rooms/join` | 进入房间;锁房时必须传密码 | | `POST` | `/api/v1/rooms/join` | 进入房间;带锁房间需要传 `password` |
| `GET` | `/api/v1/rooms` | 房间列表和搜索,返回 `locked` | | `GET` | `/api/v1/rooms` | 公共房间列表和搜索;房间卡片返回 `locked` |
| `GET` | `/api/v1/rooms/feeds` | Mine 页房间流,返回 `locked` | | `GET` | `/api/v1/rooms/feeds` | Mine 页房间流;房间卡片返回 `locked` |
请求头 ## 请求头
| Header | 必填 | 说明 | | Header | 必填 | 模拟值 | 说明 |
| --- | --- | --- |
| `Authorization` | 是 | `Bearer <access_token>` |
| `X-App-Code` | 否 | App 编码,例如 `lalu` |
## 设置房间密码
`POST /api/v1/rooms/password`
参数:
| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `room_id` | 是 | string | 房间 ID | | `Authorization` | 是 | `Bearer <access_token>` | 登录 access token服务端从 token 读取当前用户 ID。 |
| `command_id` | 是 | string | 本次设置动作的幂等键,同一次重试复用 | | `X-App-Code` | 否 | `lalu` | App 编码;登录态接口最终以 token 内的 `app_code` 为准。 |
| `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", "room_id": "room_10001",
"command_id": "cmd_room_password_room_10001_1710000000", "command_id": "cmd_room_password_room_10001_1710000000",
@ -48,7 +63,22 @@ http://127.0.0.1:13000
} }
``` ```
返回: 清空密码请求示例:
```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 ```json
{ {
@ -84,56 +114,42 @@ http://127.0.0.1:13000
} }
``` ```
返回字段: 字段说明
| 字段 | 说明 | | 字段 | 类型 | 说明 |
| --- | --- | --- |
| `data.result.applied` | bool | 本次命令是否产生状态变更;重复提交相同命令可能为 `false`。 |
| `data.result.room_version` | int64 | 命令完成后房间版本。 |
| `data.room.locked` | bool | 最新锁房状态;`true` 表示后续新用户进房需要密码。 |
| `data.room.version` | int64 | 最新房间快照版本;客户端本地房间状态以更大版本覆盖旧状态。 |
| `data.seats` | array | 当前座位列表;锁房接口成功后可直接复用该房间快照刷新页面。 |
## 带密码进入房间
客户端进入房间前先看房间卡片或详情里的 `locked`
| `locked` | 客户端行为 |
| --- | --- | | --- | --- |
| `data.result.applied` | 本次命令是否改变房间状态 | | `false` | 直接调用 JoinRoom`password` 可不传或传空字符串。 |
| `data.result.room_version` | 房间最新版本 | | `true` | 先弹出密码输入,再调用 JoinRoom 并传 `password`。 |
| `data.room.locked` | 最新锁房状态 |
| `data.room.version` | 房间快照版本 |
| `data.seats` | 当前麦位列表 |
## 清空房间密码 请求体:
`POST /api/v1/rooms/password` | 字段 | 必填 | 类型 | 模拟值 | 说明 |
| --- | --- | --- | --- | --- |
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
| `command_id` | 是 | string | `cmd_join_room_10001_20001_1710000000` | 进房命令幂等键;同一次进房重试必须复用。 |
| `role` | 否 | string | `audience` | 当前固定传 `audience`。 |
| `password` | 锁房时必填 | string | `1234` | 只用于本次进房校验,服务端不保存明文。 |
参数: 请求示例
| 字段 | 必填 | 类型 | 说明 | ```http
| --- | --- | --- | --- | POST /api/v1/rooms/join
| `room_id` | 是 | string | 房间 ID | Authorization: Bearer <access_token>
| `command_id` | 是 | string | 本次清空动作的幂等键 | X-App-Code: lalu
| `locked` | 是 | bool | 固定传 `false` | Content-Type: application/json
请求:
```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", "room_id": "room_10001",
"command_id": "cmd_join_room_10001_20001_1710000000", "command_id": "cmd_join_room_10001_20001_1710000000",
@ -142,7 +158,7 @@ http://127.0.0.1:13000
} }
``` ```
返回 成功响应只展示锁房相关字段
```json ```json
{ {
@ -180,33 +196,35 @@ http://127.0.0.1:13000
} }
``` ```
返回字段:
| 字段 | 说明 |
| --- | --- |
| `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` 可不传 | | 房间未锁 | 不校验 `password`。 |
| 已锁房且密码正确 | JoinRoom 成功,再加入 IM 房间群 | | 房间已锁且密码正确 | JoinRoom 成功,客户端再调用腾讯 IM `joinGroup` 和 RTC `enterRoom`。 |
| 已锁且密码错误或缺失 | 返回 `403 PERMISSION_DENIED`,不要加入 IM 房间群 | | 房间已锁且密码错误或缺失 | 返回 `403 PERMISSION_DENIED`,不会创建 presence不会返回 RTC token。 |
| 房主进自己房间 | 不需要密码 | | 房主进入自己的房间 | 不需要密码。 |
| 已在房间内重复 JoinRoom | 非房主仍要按当前最新密码校验;密码错误返回 `403 PERMISSION_DENIED` | | 用户已经在房间内 | 再次 JoinRoom 按幂等恢复当前房间态,不会因为后续锁房被踢出。 |
## 房间列表 locked 字段 ## 房间列表和搜索 locked 字段
`GET /api/v1/rooms?tab=hot&query=100001` 公共列表和搜索使用同一个接口:
`GET /api/v1/rooms/feeds?tab=followed` ```http
GET /api/v1/rooms?tab=hot&query=100001
Authorization: Bearer <access_token>
X-App-Code: lalu
```
返回: Mine 页房间流:
```http
GET /api/v1/rooms/feeds?tab=followed
Authorization: Bearer <access_token>
X-App-Code: lalu
```
列表响应示例:
```json ```json
{ {
@ -218,10 +236,18 @@ http://127.0.0.1:13000
{ {
"room_id": "room_10001", "room_id": "room_10001",
"im_group_id": "room_10001", "im_group_id": "room_10001",
"owner_user_id": "10001",
"title": "Live Room", "title": "Live Room",
"cover_url": "https://cdn.example/room.png",
"mode": "voice",
"status": "active",
"locked": true, "locked": true,
"heat": 1000,
"online_count": 12, "online_count": 12,
"seat_count": 15,
"occupied_seat_count": 2,
"visible_region_id": 1, "visible_region_id": 1,
"app_code": "lalu",
"room_short_id": "100001" "room_short_id": "100001"
} }
], ],
@ -230,87 +256,17 @@ http://127.0.0.1:13000
} }
``` ```
字段: 字段说明
| 字段 | 说明 | | 字段 | 类型 | 说明 |
| --- | --- | | --- | --- | --- |
| `rooms[].locked` | 是否锁房;`true` 时进房前弹密码输入 | | `rooms[].locked` | bool | 房间是否需要入房密码;搜索结果和普通列表一致。 |
| `rooms[].im_group_id` | 房间 IM 群 ID只能在 JoinRoom 成功后使用 | | `rooms[].im_group_id` | string | 腾讯 IM 群 ID只用于 JoinRoom 成功后的 `joinGroup`,不能绕过 JoinRoom。 |
| `rooms[].visible_region_id` | 房间区域 | | `next_cursor` | string | 下一页游标;为空表示没有下一页。 |
## 相关 IM ## 错误处理
### JoinRoom 成功后加入房间群 失败返回仍使用统一 envelope
接口 `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 ```json
{ {
@ -320,13 +276,108 @@ Flutter 需要监听用户所在区域播报群的自定义消息:
} }
``` ```
常见错误: | 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` | 内部服务暂不可用。 | 可提示稍后重试。 |
| HTTP | code | 说明 | 幂等规则:
| --- | --- | --- |
| `400` | `INVALID_ARGUMENT` | 参数缺失、设置密码时密码为空或超过 64 字符 | | 场景 | 返回 |
| `401` | `UNAUTHORIZED` | token 无效或过期 | | --- | --- |
| `403` | `PERMISSION_DENIED` | 非房主设置密码、进房密码错误、用户被 ban | | 同一个 `command_id` 重试同一次设置密码 | 返回同一命令结果,不重复变更房间。 |
| `404` | `NOT_FOUND` | 房间不存在 | | 同一个 `command_id` 换另一个密码 | `409 CONFLICT`。 |
| `409` | `ROOM_CLOSED` | 房间已关闭 | | 重复清空已解锁房间 | `200 OK``data.room.locked=false`。 |
| `409` | `CONFLICT` | 同一个 `command_id` 被不同请求体复用 | | 同一个用户重复 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`

View File

@ -1,6 +1,6 @@
# 用户排行榜 Flutter 对接 # 用户排行榜 Flutter 对接
本文描述 Flutter App 对接财富榜、魅力榜、游戏榜和房间榜的 HTTP 接口。`sent/received` 来自钱包已提交的礼物事实,`game` 来自游戏成功扣币的 `game_spend_coin` 事实,`room` 来自 room-service 的房间收礼投影;客户端只展示 gateway 返回的榜单,不在本地重算排名或统计窗口。 本文描述 Flutter App 对接用户排行榜、送礼榜、收礼榜和房间榜的 HTTP 接口。榜单事实来自 `wallet-service` 已成功的礼物扣费流水;客户端只展示 gateway 返回的榜单投影,不在本地重算排名、礼物价值或统计窗口。
## 基础约定 ## 基础约定
@ -49,7 +49,7 @@ Query 参数:
| 字段 | 类型 | 必填 | 默认 | 说明 | | 字段 | 类型 | 必填 | 默认 | 说明 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| `board_type` | string | 否 | `sent` | 榜单类型。`sent` 是送礼财富榜;`received` 是收礼魅力榜;`game` 是游戏消费榜;`room` 是房间收礼榜。 | | `board_type` | string | 否 | `sent` | 榜单类型。`sent` 是送礼榜;`received` 是收礼榜;`room` 是房间收礼榜。 |
| `period` | string | 否 | `today` | 统计周期。`today``week``month`。 | | `period` | string | 否 | `today` | 统计周期。`today``week``month`。 |
| `page` | int32 | 否 | `1` | 页码,从 1 开始。必须大于 0。 | | `page` | int32 | 否 | `1` | 页码,从 1 开始。必须大于 0。 |
| `page_size` | int32 | 否 | `20` | 每页数量。必须大于 0服务端最大返回 `100`。 | | `page_size` | int32 | 否 | `20` | 每页数量。必须大于 0服务端最大返回 `100`。 |
@ -60,7 +60,6 @@ Query 参数:
| --- | --- | --- | | --- | --- | --- |
| `sent` | 空字符串、`send``sender``gift_sent``user_sent` | 按送礼用户聚合,展示用户送礼贡献。 | | `sent` | 空字符串、`send``sender``gift_sent``user_sent` | 按送礼用户聚合,展示用户送礼贡献。 |
| `received` | `receive``receiver``gift_received``user_received` | 按收礼用户聚合,展示用户收礼魅力。 | | `received` | `receive``receiver``gift_received``user_received` | 按收礼用户聚合,展示用户收礼魅力。 |
| `game` | `games``gaming` | 按用户聚合成功游戏扣币额;不累计派奖、退款或净输赢。 |
| `room` | `rooms``room_gift``room_gifts` | 按房间聚合,展示房间礼物流水。 | | `room` | `rooms``room_gift``room_gifts` | 按房间聚合,展示房间礼物流水。 |
`period` 兼容别名: `period` 兼容别名:
@ -71,7 +70,7 @@ Query 参数:
| `week` | `weekly` | UTC 本周周一 00:00:00 到当前服务端时间。 | | `week` | `weekly` | UTC 本周周一 00:00:00 到当前服务端时间。 |
| `month` | `monthly` | UTC 本月 1 日 00:00:00 到当前服务端时间。 | | `month` | `monthly` | UTC 本月 1 日 00:00:00 到当前服务端时间。 |
服务端实际查询范围是 `[start_at_ms, end_at_ms)`,其中 `end_at_ms` 是本次请求的当前服务端时间。用户榜按窗口累计值倒序;`game` 的累计值是 `game_spend_coin`,为兼容既有 JSON 暂时仍通过 `gift_value` 返回。房间榜由 room-service 按同一周期返回 服务端实际查询范围是 `[start_at_ms, end_at_ms)`,其中 `end_at_ms` 是本次请求的当前服务端时间。排序固定为 `gift_value DESC, last_gift_at_ms DESC, subject_id ASC`;礼物价值相同的情况下,最近送礼时间更晚的排名更靠前,再用用户 ID 或房间 ID 保证稳定顺序
### 送礼榜 ### 送礼榜
@ -93,16 +92,6 @@ X-App-Code: lalu
收礼榜按 `target_user_id` 聚合。返回项里的 `user_id``user` 表示收礼用户。 收礼榜按 `target_user_id` 聚合。返回项里的 `user_id``user` 表示收礼用户。
### 游戏榜
```http
GET /api/v1/activities/user-leaderboards?board_type=game&period=today&page=1&page_size=20
Authorization: Bearer <access_token>
X-App-Code: huwaa
```
游戏榜按成功扣币订单的 `user_id` 聚合 `game_spend_coin`。只有 `op_type=debit` 且扣币成功、金额大于 0 的订单进入榜单;派奖、退款和净输赢不会改变游戏榜积分。返回项中的 `gift_value` 是兼容字段,游戏榜场景表示累计游戏扣币额。
### 房间榜 ### 房间榜
```http ```http
@ -115,7 +104,7 @@ X-App-Code: lalu
## 房间内贡献榜边界 ## 房间内贡献榜边界
本接口是全局活动榜,按 `board_type + period` 查询对应的用户或房间聚合。房间页里的当前房间送礼贡献榜不是通过本接口读取: 本接口是全局活动榜,按 `board_type + period` 查询成功送礼流水。房间页里的当前房间送礼贡献榜不是通过本接口读取:
| 场景 | 接口 | 榜单字段 | 说明 | | 场景 | 接口 | 榜单字段 | 说明 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
@ -145,10 +134,7 @@ X-App-Code: lalu
"user_id": "10001", "user_id": "10001",
"display_user_id": "888888", "display_user_id": "888888",
"username": "Luna", "username": "Luna",
"avatar": "https://cdn.example/avatar/10001.png", "avatar": "https://cdn.example/avatar/10001.png"
"short_badge_cover_urls": [
"https://cdn.example/badges/short-1.png"
]
} }
} }
], ],
@ -178,8 +164,6 @@ X-App-Code: lalu
} }
``` ```
`user.short_badge_cover_urls` 只包含用户当前佩戴的普通短徽章静态封面,长徽章和等级徽章不会混入。用户没有短徽章时字段省略,客户端按空数组处理即可;房间榜没有用户徽章字段。
房间榜返回示例: 房间榜返回示例:
```json ```json
@ -221,24 +205,24 @@ X-App-Code: lalu
| `total` | int64 | 当前榜单和周期下的总上榜主体数。 | | `total` | int64 | 当前榜单和周期下的总上榜主体数。 |
| `page` | int32 | 服务端解析后的页码。 | | `page` | int32 | 服务端解析后的页码。 |
| `page_size` | int32 | 服务端实际使用的每页数量;请求超过 100 时返回 100。 | | `page_size` | int32 | 服务端实际使用的每页数量;请求超过 100 时返回 100。 |
| `board_type` | string | 服务端归一化后的榜单类型:`sent``received``game`、`room`。 | | `board_type` | string | 服务端归一化后的榜单类型:`sent``received``room`。 |
| `period` | string | 服务端归一化后的统计周期:`today``week``month`。 | | `period` | string | 服务端归一化后的统计周期:`today``week``month`。 |
| `start_at_ms` | int64 | 本次统计窗口开始时间UTC。 | | `start_at_ms` | int64 | 本次统计窗口开始时间UTC。 |
| `end_at_ms` | int64 | 本次统计窗口结束时间,等于请求时的当前服务端时间。 | | `end_at_ms` | int64 | 本次统计窗口结束时间,等于请求时的当前服务端时间。 |
| `server_time_ms` | int64 | 服务端当前时间,可用于前端展示“更新时间”。 | | `server_time_ms` | int64 | 服务端当前时间,可用于前端展示“更新时间”。 |
| `my_rank` | object? | 当前登录用户在送礼榜、收礼榜或游戏榜中的排名;未上榜或 `board_type=room` 时不返回。 | | `my_rank` | object? | 当前登录用户在送礼榜或收礼榜中的排名;未上榜或 `board_type=room` 时不返回。 |
`items[]` / `my_rank` 字段: `items[]` / `my_rank` 字段:
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| `rank` | int64 | 排名,从 1 开始。 | | `rank` | int64 | 排名,从 1 开始。 |
| `user_id` | string | 用户榜单主体 ID`sent``received``game` 才有。 | | `user_id` | string | 用户榜单主体 ID`sent``received` 才有。 |
| `room_id` | string | 房间榜单主体 ID`room` 才有。 | | `room_id` | string | 房间榜单主体 ID`room` 才有。 |
| `gift_value` | int64 | 排名累计值。`sent/received/room` 表示礼物价值;`game` 表示成功游戏扣币额。 | | `gift_value` | int64 | 统计窗口内礼物价值总和,用于排名和主数值展示。 |
| `gift_count` | int64 | 礼物榜的礼物数量总和;`game` 固定为 0。 | | `gift_count` | int64 | 统计窗口内礼物数量总和。 |
| `transaction_count` | int64 | 用户榜的成功事实数;`game` 表示成功扣币订单数。 | | `transaction_count` | int64 | 统计窗口内成功送礼扣费流水数。 |
| `last_gift_at_ms` | int64 | 最近一次计榜事实时间;`game` 表示最近一次成功游戏扣币时间。字段名为兼容既有协议保留。 | | `last_gift_at_ms` | int64 | 该主体最近一次成功送礼流水时间。 |
| `user` | object? | 用户资料;正常会补充展示 ID、昵称和头像本地未注入用户资料 client 时至少返回 `user_id`。 | | `user` | object? | 用户资料;正常会补充展示 ID、昵称和头像本地未注入用户资料 client 时至少返回 `user_id`。 |
| `room` | object? | 房间基础投影;房间榜返回 `room_id``room_short_id``title``cover_url``room_avatar`。 | | `room` | object? | 房间基础投影;房间榜返回 `room_id``room_short_id``title``cover_url``room_avatar`。 |
@ -374,14 +358,12 @@ class LeaderboardUser {
required this.displayUserId, required this.displayUserId,
required this.username, required this.username,
required this.avatar, required this.avatar,
required this.shortBadgeCoverUrls,
}); });
final String userId; final String userId;
final String displayUserId; final String displayUserId;
final String username; final String username;
final String avatar; final String avatar;
final List<String> shortBadgeCoverUrls;
factory LeaderboardUser.fromJson(Map<String, dynamic> json) { factory LeaderboardUser.fromJson(Map<String, dynamic> json) {
return LeaderboardUser( return LeaderboardUser(
@ -389,9 +371,6 @@ class LeaderboardUser {
displayUserId: json['display_user_id'] as String? ?? '', displayUserId: json['display_user_id'] as String? ?? '',
username: json['username'] as String? ?? '', username: json['username'] as String? ?? '',
avatar: json['avatar'] as String? ?? '', avatar: json['avatar'] as String? ?? '',
shortBadgeCoverUrls: (json['short_badge_cover_urls'] as List<dynamic>? ?? const [])
.whereType<String>()
.toList(),
); );
} }
} }
@ -426,7 +405,7 @@ class LeaderboardRoom {
请求封装示例: 请求封装示例:
```dart ```dart
enum LeaderboardType { sent, received, game, room } enum LeaderboardType { sent, received, room }
enum LeaderboardPeriod { today, week, month } enum LeaderboardPeriod { today, week, month }
extension on LeaderboardType { extension on LeaderboardType {
@ -504,15 +483,15 @@ class ApiException implements Exception {
| `401` | `SESSION_REVOKED` | 会话已被服务端撤销。 | 清理本地登录态并重新登录。 | | `401` | `SESSION_REVOKED` | 会话已被服务端撤销。 | 清理本地登录态并重新登录。 |
| `403` | `PROFILE_REQUIRED` | 当前 token 对应用户未完成资料。 | 跳资料补全流程。 | | `403` | `PROFILE_REQUIRED` | 当前 token 对应用户未完成资料。 | 跳资料补全流程。 |
| `405` | `INVALID_ARGUMENT` | 使用了非 GET 方法。 | 修正客户端请求方法。 | | `405` | `INVALID_ARGUMENT` | 使用了非 GET 方法。 | 修正客户端请求方法。 |
| `502` | `UPSTREAM_ERROR` | 榜单 Redis、用户资料、钱包装扮或房间上游依赖异常。 | 展示通用失败,可保留旧榜单并允许下拉重试;记录 `request_id`。 | | `502` | `UPSTREAM_ERROR` | 钱包库、用户资料服务或上游依赖异常。 | 展示通用失败,可保留旧榜单并允许下拉重试;记录 `request_id`。 |
| `503` | `UPSTREAM_ERROR` | gateway 未配置榜单 Redis 读模型。 | 展示通用失败,记录 `request_id`。 | | `503` | `UPSTREAM_ERROR` | gateway 未配置榜单钱包只读库。 | 展示通用失败,记录 `request_id`。 |
## 客户端展示规则 ## 客户端展示规则
榜单页建议用 `board_type + period + page + page_size` 做缓存 key。用户切换榜单类型或周期时重新请求第一页上拉加载下一页时用返回的 `total` 判断是否还有更多:`page * page_size < total` 榜单页建议用 `board_type + period + page + page_size` 做缓存 key。用户切换榜单类型或周期时重新请求第一页上拉加载下一页时用返回的 `total` 判断是否还有更多:`page * page_size < total`
`my_rank` 只表示当前登录用户在当前用户榜单中的排名。送礼榜里是“我的送礼排名”,收礼榜里是“我的收礼排名”,游戏榜里是“我的游戏消费排名”;房间榜没有个人排名。未上榜时 `my_rank` 不存在,客户端展示“未上榜”即可,不要自行用 `items` 推断。 `my_rank` 只表示当前登录用户在当前用户榜单中的排名。送礼榜里是“我的送礼排名”,收礼榜里是“我的收礼排名”;房间榜没有个人排名。未上榜时 `my_rank` 不存在,客户端展示“未上榜”即可,不要自行用 `items` 推断。
时间展示用 `server_time_ms``end_at_ms` 标注“更新于”。`today``week``month` 的统计口径固定为 UTC展示文案如果需要本地化只能改时间格式不能改变请求周期含义。 时间展示用 `server_time_ms``end_at_ms` 标注“更新于”。`today``week``month` 的统计口径固定为 UTC展示文案如果需要本地化只能改时间格式不能改变请求周期含义。
榜单数据当前没有单独 IM 推送。送礼或游戏扣币后如果当前页面正在展示对应榜单,可以延迟刷新第一页或由用户手动下拉刷新;不要依赖房间公屏 IM 来本地累加全局榜单。 榜单数据当前没有单独 IM 推送。用户送礼后如果当前页面正在展示对应榜单,可以延迟刷新第一页或由用户手动下拉刷新;不要依赖房间公屏 IM 来本地累加全局榜单。

View File

@ -10,23 +10,22 @@
- 房间送礼面板入口是 `GET /api/v1/rooms/{room_id}/gift-panel`,返回当前房间可送礼物、可收礼人、金币余额和房间内可展示 Tab。 - 房间送礼面板入口是 `GET /api/v1/rooms/{room_id}/gift-panel`,返回当前房间可送礼物、可收礼人、金币余额和房间内可展示 Tab。
- `GET /api/v1/gift-tabs` 不能替代 `GET /api/v1/rooms/{room_id}/gift-panel`App 真正送礼前必须以房间面板返回的数据为准。 - `GET /api/v1/gift-tabs` 不能替代 `GET /api/v1/rooms/{room_id}/gift-panel`App 真正送礼前必须以房间面板返回的数据为准。
- 送礼只调用 `POST /api/v1/rooms/gift/send` - 送礼只调用 `POST /api/v1/rooms/gift/send`
- 金币余额以礼物面板、送礼响应里的 `lucky_gift.coin_balance_after`钱包通知或 `GET /api/v1/wallet/me/balances`准;普通礼物和未中奖场景仍以后续钱包事实校准。 - 金币余额以礼物面板、送礼响应后的钱包通知或 `GET /api/v1/wallet/me/balances` 为准。
- 房间内其他用户看到的礼物表现以腾讯云 IM 自定义消息 `room_gift_sent` 为准。 - 房间内其他用户看到的礼物表现以腾讯云 IM 自定义消息 `room_gift_sent` 为准。
- `command_id` 是送礼幂等键;同一次用户点击重试必须复用同一个值。 - `command_id` 是送礼幂等键;同一次用户点击重试必须复用同一个值。
- 当前 App 送礼主流程只按单个用户收礼处理:`target_type=user``target_user_ids` 只传 1 个用户。 - 当前 App 送礼主流程只按单个用户收礼处理:`target_type=user``target_user_ids` 只传 1 个用户。
- 幸运礼物的专门接入规则见 [幸运礼物 Flutter 对接](幸运礼物Flutter对接.md)。
## 交互流程 ## 交互流程
1. 用户进入房间并完成 `JoinRoom` 1. 用户进入房间并完成 `JoinRoom`
2. App 可在启动或进入房间前调用 `GET /api/v1/gift-tabs` 预加载 Tab 配置。 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` 渲染收礼人。 4. 用 `tabs` 渲染当前房间分类,用 `gifts` 渲染礼物网格,用 `recipients` 渲染收礼人。
5. 用户选择收礼人、礼物和数量。 5. 用户选择收礼人、礼物和数量。
6. 点击发送时生成稳定 `command_id`,调用 `POST /api/v1/rooms/gift/send` 6. 点击发送时生成稳定 `command_id`,调用 `POST /api/v1/rooms/gift/send`
7. 发送成功后用响应里的 `room_heat``gift_rank``rocket` 更新房间 UI如果有 `lucky_gift` 且已入账,用 `coin_balance_after` 立即刷新金币余额 7. 发送成功后用响应里的 `room_heat``gift_rank``treasure` 更新房间 UI
8. 监听房间 IM `room_gift_sent`,同步播放礼物动效。 8. 监听房间 IM `room_gift_sent`,同步播放礼物动效。
9. 普通礼物和未中奖余额仍监听私有钱包通知或主动刷新余额,不本地扣减余额作为最终值。 9. 监听私有钱包通知或主动刷新余额,不本地扣减余额作为最终值。
## 获取礼物 Tab 配置 ## 获取礼物 Tab 配置
@ -202,7 +201,7 @@ X-App-Code: lalu
| 字段 | 说明 | | 字段 | 说明 |
| --- | --- | | --- | --- |
| `coin_balance` | 当前登录用户 COIN 可用余额快照。只用于发射面板时展示,最终余额仍以后续钱包事实为准。 | | `coin_balance` | 当前登录用户 COIN 可用余额快照。只用于打开面板时展示,最终余额仍以后续钱包事实为准。 |
| `recipients` | 当前房间可收礼人。当前主流程只启用 `target_type=user` 的单个用户。 | | `recipients` | 当前房间可收礼人。当前主流程只启用 `target_type=user` 的单个用户。 |
| `tabs` | 礼物分类。`key=all` 是全量分类;其他 tab 通常对应 `gift_type_code`。 | | `tabs` | 礼物分类。`key=all` 是全量分类;其他 tab 通常对应 `gift_type_code`。 |
| `gifts` | 当前房间国家维度可用的 active 礼物列表。 | | `gifts` | 当前房间国家维度可用的 active 礼物列表。 |
@ -295,25 +294,7 @@ Content-Type: application/json
} }
], ],
"room": {}, "room": {},
"rocket": {}, "treasure": {}
"lucky_gift": {
"enabled": true,
"draw_id": "lucky_draw_test",
"command_id": "gift_1779259000000_10001_rose_01",
"pool_id": "super_lucky",
"gift_id": "rose",
"rule_version": 12,
"experience_pool": "novice",
"selected_tier_id": "novice_2x",
"multiplier_ppm": 2000000,
"effective_reward_coins": 1800,
"reward_status": "granted",
"stage_feedback": false,
"high_multiplier": false,
"created_at_ms": 1779259000000,
"wallet_transaction_id": "wallet_tx_lucky",
"coin_balance_after": 8800
}
} }
} }
``` ```
@ -329,17 +310,14 @@ Content-Type: application/json
| `room_heat` | 最新房间热度。 | | `room_heat` | 最新房间热度。 |
| `gift_rank` | 最新房间礼物榜。 | | `gift_rank` | 最新房间礼物榜。 |
| `room` | 房间快照字段较大App 可按已有房间快照解析逻辑局部更新。 | | `room` | 房间快照字段较大App 可按已有房间快照解析逻辑局部更新。 |
| `rocket` | 房间火箭状态;火箭 UI 以该字段或火箭 IM 事件更新。 | | `treasure` | 房间宝箱状态;宝箱 UI 以该字段或宝箱 IM 事件更新。 |
| `lucky_gift` | 幸运礼物抽奖结果;普通礼物可为空。`reward_status=granted` 且中奖时,`coin_balance_after` 是当前用户返奖入账后的余额。 |
发送后处理: 发送后处理:
- 成功后立即关闭 loading保留礼物面板或按产品交互关闭。 - 成功后立即关闭 loading保留礼物面板或按产品交互关闭。
- 用 `room_heat``gift_rank` 更新房间顶部和贡献榜。 - 用 `room_heat``gift_rank` 更新房间顶部和贡献榜。
- 播放当前用户送礼动效时按 `command_id` 去重,避免 HTTP 响应和 IM 各播放一次。 - 播放当前用户送礼动效时按 `command_id` 去重,避免 HTTP 响应和 IM 各播放一次。
- 如果 `lucky_gift.reward_status=granted``lucky_gift.effective_reward_coins > 0`,立即用 `lucky_gift.coin_balance_after` 覆盖本地金币余额。 - 不要本地直接写最终余额;等待 `WalletBalanceChanged` 私有 IM 或主动刷新余额。
- 如果 `lucky_gift.reward_status=pending`,展示中奖到账处理中,不要生成新 `command_id` 重送;补偿 worker 会继续使用同一个 draw 发放。
- 普通礼物和未中奖场景不要本地直接写最终余额;等待 `WalletBalanceChanged` 私有 IM 或主动刷新余额。
## 房间 IM 同步 ## 房间 IM 同步

View File

@ -1,17 +1,12 @@
# Flutter 礼物墙对接 # Flutter 礼物墙对接
本文描述 Flutter App 查询用户礼物墙的 HTTP 接口。礼物墙事实由 `wallet-service` 在送礼扣费成功后通过异步投影聚合,客户端只展示 gateway 返回的当前投影,不从房间消息或本地缓存推算收礼数量。 本文描述 Flutter App 查询当前登录用户礼物墙的 HTTP 接口。礼物墙事实由 `wallet-service` 在送礼扣费成功后通过异步投影聚合,客户端只展示 gateway 返回的当前投影,不从房间消息或本地缓存推算收礼数量。
## 接口 ## 接口
`GET /api/v1/users/me/gift-wall` `GET /api/v1/users/me/gift-wall`
`GET /api/v1/users/{user_id}/gift-wall` 登录接口,需要 `Authorization: Bearer <access_token>`。接口固定查询当前登录用户,不接受客户端传 `user_id`
登录接口,需要 `Authorization: Bearer <access_token>`
- `GET /api/v1/users/me/gift-wall` 查询当前登录用户。
- `GET /api/v1/users/{user_id}/gift-wall` 查询资料卡目标用户,`user_id` 是路径参数,必须是正整数。
一致性边界:礼物墙是最终一致读模型,送礼成功后允许延迟数秒到几十秒才出现在接口结果中。客户端不要用该接口判断刚刚送礼是否成功;送礼成功状态仍以送礼接口响应和房间 IM 消息为准。 一致性边界:礼物墙是最终一致读模型,送礼成功后允许延迟数秒到几十秒才出现在接口结果中。客户端不要用该接口判断刚刚送礼是否成功;送礼成功状态仍以送礼接口响应和房间 IM 消息为准。
@ -30,12 +25,6 @@ Authorization: Bearer <access_token>
X-App-Code: lalu X-App-Code: lalu
``` ```
```http
GET /api/v1/users/10002/gift-wall
Authorization: Bearer <access_token>
X-App-Code: lalu
```
成功响应: 成功响应:
```json ```json

View File

@ -82,7 +82,7 @@ Query
| `gift` | 礼物权益 | 否 | | `gift` | 礼物权益 | 否 |
| `floating_screen` | 飘屏 | 否 | | `floating_screen` | 飘屏 | 否 |
| `mic_seat_icon` | 麦位图标 | 否 | | `mic_seat_icon` | 麦位图标 | 否 |
| `mic_seat_animation` | 麦位动画 | | | `mic_seat_animation` | 麦位动画 | |
| `emoji_pack` | 表情包 | 否 | | `emoji_pack` | 表情包 | 否 |
接口只返回当前有效权益:`status=active`、已到生效时间、未过期、`remaining_quantity > 0``expires_at_ms = 0` 表示永久有效。 接口只返回当前有效权益:`status=active`、已到生效时间、未过期、`remaining_quantity > 0``expires_at_ms = 0` 表示永久有效。
@ -219,13 +219,11 @@ Body
| 规则 | 说明 | | 规则 | 说明 |
| --- | --- | | --- | --- |
| 可佩戴类型 | `avatar_frame``profile_card``vehicle``chat_bubble``badge``mic_seat_animation`。 | | 可佩戴类型 | `avatar_frame``profile_card``vehicle``chat_bubble``badge`。 |
| 不可佩戴类型 | `gift``floating_screen``mic_seat_icon``emoji_pack`。 | | 不可佩戴类型 | `gift``floating_screen``mic_seat_icon``mic_seat_animation`、`emoji_pack`。 |
| 同类替换 | 同一个 `resource_type` 只有一个当前佩戴项,新佩戴会替换旧佩戴。 | | 同类替换 | 同一个 `resource_type` 只有一个当前佩戴项,新佩戴会替换旧佩戴。 |
| 权益校验 | 必须是当前登录用户自己的有效权益;过期、禁用、剩余数量为 0 都不能佩戴。 | | 权益校验 | 必须是当前登录用户自己的有效权益;过期、禁用、剩余数量为 0 都不能佩戴。 |
| 发放默认佩戴 | `badge``mic_seat_animation` 发放成功后默认佩戴。 | | 徽章展示 | 背包佩戴的 `badge` 是单装备槽;资料页多徽章展示仍使用 `/api/v1/badges/me``/api/v1/badges/display`。 |
| 徽章展示 | `badge` 是拥有即佩戴的多装备资源;资料页多徽章展示仍使用 `/api/v1/badges/me``/api/v1/badges/display`。 |
| 资料卡背景 | `profile_card` 只用于装扮资料卡;个人主页背景图使用用户资料字段 `profile_bg_img`,不走背包佩戴。 |
常见错误: 常见错误:

View File

@ -0,0 +1,611 @@
# 语音房宝箱 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

@ -1,295 +0,0 @@
# 语音房火箭接口对接
## 规则
- 火箭最高 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` | 点火人奖励候选 |
| `levels[].*_rewards[].resource_group_id` | 奖励资源组 ID |
| `levels[].*_rewards[].resource_group` | 奖励资源组详情 |
| `levels[].*_rewards[].resource_group.items` | 资源组明细 |
| `levels[].*_rewards[].resource_group.items[].item_type` | `wallet_asset``resource` |
| `levels[].*_rewards[].resource_group.items[].wallet_asset_type` | 金币等奖励资产类型,例如 `COIN` |
| `levels[].*_rewards[].resource_group.items[].wallet_asset_amount` | 金币等奖励数量 |
| `levels[].*_rewards[].resource_group.items[].resource` | 聊天气泡、麦位特效等资源素材 |
| `levels[].*_rewards[].resource_group.items[].resource.resource_type` | 资源类型,例如 `chat_bubble``mic_effect` |
| `levels[].*_rewards[].resource_group.items[].resource.asset_url` | 资源主素材 |
| `levels[].*_rewards[].resource_group.items[].resource.preview_url` | 资源预览图 |
| `levels[].*_rewards[].resource_group.items[].resource.animation_url` | 资源动效 |
| `levels[].*_rewards[].resource_group.items[].duration_ms` | 资源有效期毫秒,`0` 表示永久 |
| `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` | 最近一次发射奖励结果 |
| `state.last_rewards[].resource_group_id` | 本次中奖资源组 ID |
| `state.last_rewards[].resource_group` | 本次中奖资源组详情,字段同上 |
| `server_time_ms` | 服务端 UTC 毫秒 |
`levels[].*_rewards` 表示 `in_room_rewards``top1_rewards``igniter_rewards` 三个奖励池。客户端展示金币数量、奖励天数、聊天气泡、麦位特效时,直接读 `resource_group.items`
## 送礼
地址:`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` | 发奖状态 |
奖励 IM 用于通知本次哪些人中奖。top1 和点火人命中后会发完整配置池,同一用户可能同时收到多条 `top1``igniter` 和 1 条在房随机奖励。需要渲染金币数量、奖励天数、聊天气泡或麦位特效素材时,收到 IM 后调用 `GET /api/v1/rooms/{room_id}/rocket`,读取 `state.last_rewards[].resource_group`
## 客户端处理
- 进房先调 `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

@ -1,624 +0,0 @@
# 语音房红包 Flutter 对接
本文按当前 gateway、wallet-service 和 activity-service 实现描述 Flutter 需要对接的房内红包 HTTP 接口和腾讯 IM 自定义消息。红包金额、份额、可抢时间、过期退款、领取幂等和房间在线态校验全部以后端为准Flutter 只负责展示、提交用户动作和处理实时刷新。
## 1. 基本约定
- 所有 HTTP 路径都有 `/api/v1` 前缀。
- 所有接口都需要 `Authorization: Bearer <access_token>`,并且用户必须已完成资料。
- 成功响应统一是 gateway envelope
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {}
}
```
- 失败响应也使用同一个 envelopeFlutter 只按 HTTP 状态和 `code` 分支,`message` 只做兜底展示或日志。
- 所有时间字段都是 Unix epoch milliseconds。Flutter 不用本地时区参与业务判断,只做展示格式化。
- 红包主键统一使用 `packetNo`。IM 里兼容带 `packetId`,值和 `packetNo` 相同。
- Go 返回的部分 ID 是 numberFlutter 模型里建议统一转成 `String` 保存,避免大整数精度和 UI 拼接问题。
## 2. 接入流程
进入语音房后:
1. 走现有进房流程,加入房间 IM 群。
2. 调用 `GET /api/v1/app/voice-room/red-packet/active?roomId=<roomId>` 初始化侧边栏红包列表。
3. 监听腾讯 IM 房间群 `TIMCustomElem`,解析 `ROOM_RED_PACKET`
4. 如果需要接收延时红包同语区飘窗,调用 `GET /api/v1/app/voice-room/red-packet/im-group` 获取当前语区群并加入。
5. 用户点击 IM 红包弹窗或侧边栏红包时,直接用 payload 里的 `packetNo` 调领取或详情接口。
收到 `CREATED` IM 后payload 已带发送者头像昵称、金额、份数和剩余份数。Flutter 可以直接弹窗或显示飘窗,不需要为了基础展示再立刻拉 active/detail。断线重连、进房初始化、详情页打开和二级领取记录页仍然走 HTTP 查询。
## 3. 当前语区 IM 群
```http
GET /api/v1/app/voice-room/red-packet/im-group
```
返回:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"regionId": 310,
"regionCode": "AR",
"regionName": "Arabic",
"groupId": "hy_lalu_bc_r_310",
"groupName": "hy_lalu_bc_r_310"
}
}
```
Flutter 用途:
| 字段 | 用途 |
| --- | --- |
| `groupId` | 腾讯 IM 区域广播群 ID加入后接收延时红包同语区飘窗。 |
| `regionId` | 当前用户所在语区 ID。 |
| `regionCode` | UI 或埋点使用。 |
如果返回 `voice_room_region_missing`,说明用户资料没有有效语区,隐藏区域红包飘窗入口。
## 4. 红包配置
```http
GET /api/v1/app/voice-room/red-packet/config
```
当前该接口沿用 wallet 配置 DTO字段是 snake_case
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"config": {
"app_code": "lalu",
"enabled": true,
"count_tiers": [5, 10],
"amount_tiers": [15000, 30000],
"delayed_open_seconds": 300,
"expire_seconds": 86400,
"daily_send_limit": 10,
"rule_url": "https://example.com/red-packet-rule",
"created_at_ms": 1780000000000,
"updated_at_ms": 1780000000000
},
"server_time_ms": 1780000000000
}
}
```
Flutter 用途:
| 字段 | 用途 |
| --- | --- |
| `enabled` | false 时隐藏发送入口,已收到的历史红包仍可按接口结果展示。 |
| `count_tiers` | 发送红包份数选项。 |
| `amount_tiers` | 发送红包总金币金额选项。 |
| `delayed_open_seconds` | 延时红包等待秒数Flutter 不提交该值。 |
| `expire_seconds` | 过期秒数;当前为 86400。 |
| `daily_send_limit` | 每人每日发送上限提示。 |
| `rule_url` | 规则说明 H5。 |
## 5. 发送红包
```http
POST /api/v1/app/voice-room/red-packet/send
Content-Type: application/json
```
请求:
```json
{
"roomId": "9001",
"packetMode": "DELAYED",
"totalAmount": 15000,
"totalCount": 5,
"requestId": "client-uuid-or-action-id"
}
```
字段:
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `roomId` | 是 | 当前语音房 ID。 |
| `packetMode` | 是 | `IMMEDIATE``DELAYED`。后端也兼容 `normal/delayed`。 |
| `totalAmount` | 是 | 红包总金币,必须命中 `amount_tiers`。 |
| `totalCount` | 是 | 红包份数,必须命中 `count_tiers`。 |
| `requestId` | 是 | 客户端动作幂等 ID。同一用户同一 `requestId` 重试返回同一红包。 |
返回:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"packet": {
"packetNo": "VRRP2026052712340001",
"roomId": "9001",
"regionId": 310,
"senderUserId": 10001,
"senderAccount": "10001",
"senderNickname": "Tom",
"senderAvatar": "https://example.com/avatar1.png",
"packetMode": "DELAYED",
"totalAmount": 15000,
"totalCount": 5,
"remainingAmount": 15000,
"remainingCount": 5,
"status": "WAITING_OPEN",
"claimStartTime": 1780000000000,
"expireTime": 1780086400000,
"createdAt": 1779999700000,
"claimedByMe": false,
"myClaimAmount": 0,
"refundAmount": 0,
"refundStatus": "",
"refundedAt": 0,
"claims": []
},
"balanceAfter": 985000,
"serverTime": 1779999700000
}
}
```
发送成功后服务端会异步发送 IM
- 当前房间群:`ROOM_RED_PACKET / CREATED / scope=ROOM`
- 延时红包额外发送当前语区广播群:`ROOM_RED_PACKET / CREATED / scope=REGION`
- 延时红包到点后当前房间群:`ROOM_RED_PACKET / OPEN_DUE / scope=ROOM`
Flutter 发送成功后可以立即把返回的 `packet` 放入本地红包列表;后续 IM 可能也会收到同一个红包,需要按 `packetNo + event + scope` 做去重或覆盖更新。
## 6. 房间有效红包列表
```http
GET /api/v1/app/voice-room/red-packet/active?roomId=9001
```
返回:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"packets": [
{
"packetNo": "VRRP2026052712340001",
"roomId": "9001",
"regionId": 310,
"senderUserId": 10001,
"senderAccount": "10001",
"senderNickname": "Tom",
"senderAvatar": "https://example.com/avatar1.png",
"packetMode": "DELAYED",
"totalAmount": 15000,
"totalCount": 5,
"remainingAmount": 15000,
"remainingCount": 5,
"status": "WAITING_OPEN",
"claimStartTime": 1780000000000,
"expireTime": 1780086400000,
"createdAt": 1779999700000,
"claimedByMe": false,
"myClaimAmount": 0
}
],
"serverTime": 1779999700000
}
}
```
Flutter 用途:
- 进房或重连后初始化侧边栏列表。
- 侧边栏未领取数量可以用 `packets.where((p) => !p.claimedByMe).length`
- 该接口只返回仍处于 `ACTIVE``WAITING_OPEN` 的红包。抢完、已退款或过期后的最终状态通过详情或 IM `STATUS_CHANGED` 刷新。
## 7. 抢红包
```http
POST /api/v1/app/voice-room/red-packet/claim
Content-Type: application/json
```
请求:
```json
{
"packetNo": "VRRP2026052712340001"
}
```
返回:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"claim": {
"claimId": "claim_xxx",
"packetNo": "VRRP2026052712340001",
"userId": 10002,
"account": "10002",
"nickname": "Jerry",
"avatar": "https://example.com/avatar2.png",
"amount": 3000,
"status": "claimed",
"createdAt": 1780000010000
},
"packet": {
"packetNo": "VRRP2026052712340001",
"roomId": "9001",
"regionId": 310,
"senderUserId": 10001,
"senderAccount": "10001",
"senderNickname": "Tom",
"senderAvatar": "https://example.com/avatar1.png",
"remainingAmount": 12000,
"remainingCount": 4,
"status": "ACTIVE",
"claimedByMe": true,
"myClaimAmount": 3000
},
"balanceAfter": 1003000,
"serverTime": 1780000010000
}
}
```
处理规则:
- 领取成功响应里的 `packet` 结构和详情接口一致,示例只保留弹窗刷新需要的关键字段。
- Flutter 只提交 `packetNo`。当前服务端按 `packetNo + 当前用户` 保证同一用户重复领取返回同一结果。
- 如果返回 `voice_room_red_packet_room_presence_required`,提示用户需要在房间内操作,并刷新房间在线态。
- 如果返回 `voice_room_red_packet_not_started`,展示倒计时,不要重试抢。
- 如果返回 `voice_room_red_packet_finished``voice_room_red_packet_expired`,刷新侧边栏和详情。
- 如果返回 `claim_processing`,展示处理中,可让用户稍后重试同一个红包或打开详情。
## 8. 红包详情
```http
GET /api/v1/app/voice-room/red-packet/detail?packetNo=VRRP2026052712340001
```
返回:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"packet": {
"packetNo": "VRRP2026052712340001",
"roomId": "9001",
"regionId": 310,
"senderUserId": 10001,
"senderAccount": "10001",
"senderNickname": "Tom",
"senderAvatar": "https://example.com/avatar1.png",
"packetMode": "DELAYED",
"totalAmount": 15000,
"totalCount": 5,
"remainingAmount": 12000,
"remainingCount": 4,
"status": "ACTIVE",
"claimStartTime": 1780000000000,
"expireTime": 1780086400000,
"createdAt": 1779999700000,
"claimedByMe": true,
"myClaimAmount": 3000,
"refundAmount": 0,
"refundStatus": "",
"refundedAt": 0,
"claims": []
},
"serverTime": 1780000010000
}
}
```
详情和领取成功响应里的 `packet` 会补发送者 `senderAccount/senderNickname/senderAvatar`。详情接口里的 `claims` 不作为二级领取记录页的资料来源。需要领取用户头像昵称账号时使用 `claim-records`,因为该接口会按当前页批量补用户资料。
## 9. 领取记录
```http
GET /api/v1/app/voice-room/red-packet/claim-records?packetNo=VRRP2026052712340001&page=1&pageSize=20
```
当前实现使用 `page/pageSize`,不要按旧文档使用 `cursor/limit`
返回:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"items": [
{
"claimId": "claim_xxx",
"packetNo": "VRRP2026052712340001",
"userId": 10002,
"account": "10002",
"nickname": "Jerry",
"avatar": "https://example.com/avatar2.png",
"amount": 3000,
"status": "claimed",
"createdAt": 1780000010000
}
],
"total": 1,
"page": 1,
"pageSize": 20,
"serverTime": 1780000010000
}
}
```
Flutter 用途:
- 二级领取记录页使用 `items` 渲染头像、昵称、账号、领取金额和领取时间。
- `account/nickname/avatar` 已由 gateway 从 user-service 补齐。
- 分页加载下一页时递增 `page`
## 10. 我发出的红包记录
```http
GET /api/v1/app/voice-room/red-packet/sent-records?page=1&pageSize=20
```
返回:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"items": [
{
"packetNo": "VRRP2026052712340001",
"roomId": "9001",
"packetMode": "DELAYED",
"totalAmount": 15000,
"totalCount": 5,
"remainingAmount": 12000,
"remainingCount": 4,
"status": "ACTIVE",
"claimStartTime": 1780000000000,
"expireTime": 1780086400000,
"createdAt": 1779999700000,
"refundAmount": 0,
"refundStatus": "",
"refundedAt": 0
}
],
"total": 1,
"page": 1,
"pageSize": 20,
"serverTime": 1780000010000
}
}
```
## 11. IM 消息
腾讯 IM 自定义消息的 `TIMCustomElem.Data` 是 JSON 字符串。Flutter 先解析外层:
```json
{
"type": "ROOM_RED_PACKET",
"data": {}
}
```
只有 `type == "ROOM_RED_PACKET"` 才进入红包处理。
### 11.1 CREATED
当前房间弹窗和同语区飘窗都使用 `CREATED`,通过 `scope` 区分。修正后的 CREATED payload 已带足够的首屏展示字段。
```json
{
"type": "ROOM_RED_PACKET",
"data": {
"event": "CREATED",
"scope": "ROOM",
"packetNo": "VRRP2026052712340001",
"packetId": "VRRP2026052712340001",
"roomId": "9001",
"regionId": 310,
"regionCode": "AR",
"packetMode": "DELAYED",
"senderUserId": "10001",
"actualAccount": "10001",
"userNickname": "Tom",
"userAvatar": "https://example.com/avatar.png",
"totalAmount": 15000,
"totalCount": 5,
"remainAmount": 15000,
"remainCount": 5,
"status": "WAITING_OPEN",
"delaySeconds": 300,
"refundedAmount": 0,
"claimStartTime": 1780000000000,
"expireTime": 1780086400000,
"serverTime": 1779999700000
}
}
```
Flutter 行为:
| `scope` | 行为 |
| --- | --- |
| `ROOM` | 如果当前正在该房间,直接弹抢红包窗口,并更新侧边栏。 |
| `REGION` | 作为同语区飘窗展示;点击后进入 `roomId` 对应房间并打开红包弹窗或详情。 |
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `packetNo` / `packetId` | string | 红包 ID接口参数使用 `packetNo`。 |
| `packetMode` | string | `IMMEDIATE``DELAYED`。 |
| `senderUserId` | string | 发送者长 ID。 |
| `actualAccount` | string | 发送者展示账号。 |
| `userNickname` | string | 发送者昵称。 |
| `userAvatar` | string | 发送者头像 URL。 |
| `totalAmount` | int64 | 红包总金币。 |
| `totalCount` | int | 红包总份数。 |
| `remainAmount` | int64 | 剩余总金币。CREATED 时通常等于 `totalAmount`。 |
| `remainCount` | int | 剩余可领份数。CREATED 时通常等于 `totalCount`。 |
| `status` | string | `ACTIVE``WAITING_OPEN``FINISHED``REFUNDED`。 |
| `delaySeconds` | int64 | `DELAYED` 红包的等待秒数,普通红包为 0。 |
| `claimStartTime` | int64 | 可领取开始时间。 |
| `expireTime` | int64 | 过期时间。 |
| `serverTime` | int64 | 该红包事件生成时间。 |
### 11.2 OPEN_DUE
延时红包到可领取时间后,服务端向房间群发 `OPEN_DUE`。当前 payload 使用同一结构Flutter 只需要关心 `event/scope/packetNo/status/claimStartTime/expireTime/remainCount`
```json
{
"type": "ROOM_RED_PACKET",
"data": {
"event": "OPEN_DUE",
"scope": "ROOM",
"packetNo": "VRRP2026052712340001",
"packetId": "VRRP2026052712340001",
"roomId": "9001",
"packetMode": "DELAYED",
"senderUserId": "10001",
"actualAccount": "10001",
"userNickname": "Tom",
"userAvatar": "https://example.com/avatar.png",
"totalAmount": 15000,
"totalCount": 5,
"remainAmount": 15000,
"remainCount": 5,
"status": "WAITING_OPEN",
"claimStartTime": 1780000000000,
"expireTime": 1780086400000,
"serverTime": 1779999700000
}
}
```
Flutter 收到后:
- 如果用户仍在 `roomId` 房间,强制弹出该红包或把已展示红包切换为可抢。
- 不要在客户端发放金额;仍然调用领取接口。
- 如果本地已经显示该红包,按 `packetNo` 覆盖更新。
### 11.3 STATUS_CHANGED
红包抢完、退款完成或状态变化时,服务端向房间群发 `STATUS_CHANGED`
```json
{
"type": "ROOM_RED_PACKET",
"data": {
"event": "STATUS_CHANGED",
"scope": "ROOM",
"packetNo": "VRRP2026052712340001",
"packetId": "VRRP2026052712340001",
"roomId": "9001",
"status": "FINISHED",
"remainCount": 0,
"refundedAmount": 0,
"serverTime": 1780000020000
}
}
```
Flutter 收到后:
- 更新侧边栏数量。
- 如果当前弹窗或详情页是同一个 `packetNo`,同步刷新状态。
- `remainCount == 0``status == FINISHED/REFUNDED` 时不再展示抢按钮。
## 12. Flutter 状态模型建议
建议本地以 `packetNo` 为 key 存一份红包 read model
```dart
class VoiceRoomRedPacket {
final String packetNo;
final String roomId;
final String packetMode;
final String status;
final String senderUserId;
final String senderAccount;
final String senderNickname;
final String senderAvatar;
final int totalAmount;
final int totalCount;
final int remainingAmount;
final int remainingCount;
final int claimStartTimeMs;
final int expireTimeMs;
final bool claimedByMe;
final int myClaimAmount;
}
```
解析建议:
- 对 `senderUserId/userId` 这类 ID兼容 number 和 string统一转 string。
- HTTP 发送成功、active、详情、领取成功里的 `packet` 使用 `senderAccount/senderNickname/senderAvatar`IM `CREATED/OPEN_DUE` 使用 `actualAccount/userNickname/userAvatar`,落本地模型时归一到发送者字段。
- 对金额和时间,兼容 number 和 string统一转 int。
- `CREATED` IM 里的 `remainAmount/remainCount` 对应 HTTP 里的 `remainingAmount/remainingCount`
- 如果 IM 和 HTTP 字段冲突,以 HTTP 最新查询结果为准。
## 13. 错误码处理
| HTTP code | `code` | Flutter 处理 |
| --- | --- | --- |
| 409 | `voice_room_red_packet_disabled` | 隐藏发送入口,提示暂不可用。 |
| 400 | `voice_room_red_packet_amount_invalid` | 刷新配置,提示金额档位已变化。 |
| 400 | `voice_room_red_packet_count_invalid` | 刷新配置,提示份数档位已变化。 |
| 409 | `voice_room_red_packet_daily_limit_exceeded` | 提示今日发送次数已达上限。 |
| 409 | `voice_room_red_packet_insufficient_balance` | 拉起充值或余额不足提示。 |
| 404 | `voice_room_red_packet_not_found` | 关闭弹窗并刷新 active 列表。 |
| 409 | `voice_room_red_packet_not_started` | 展示倒计时,不重试领取。 |
| 409 | `voice_room_red_packet_expired` | 提示已过期,刷新列表和详情。 |
| 409 | `voice_room_red_packet_finished` | 提示已抢完,刷新列表和详情。 |
| 409 | `voice_room_red_packet_room_presence_required` | 提示需在房间内操作,重新走进房或刷新在线态。 |
| 403 | `voice_room_region_not_match` | 用户语区和红包语区不一致,禁止领取。 |
| 409 | `voice_room_region_missing` | 用户资料缺少语区,提示完善资料或重登刷新。 |
| 409 | `claim_processing` | 展示处理中,可稍后重试或打开详情。 |
## 14. 联调检查清单
- 发送普通红包后,当前房间收到 `CREATED scope=ROOM`payload 带 sender 头像昵称、金额、总份数和剩余份数。
- 发送延时红包后,当前房间收到 `CREATED scope=ROOM`,当前语区群收到 `CREATED scope=REGION`
- 延时红包未到 `claimStartTime` 调领取返回 `voice_room_red_packet_not_started`
- 到点后房间收到 `OPEN_DUE`Flutter 可以强弹并允许点击领取。
- 用户不在红包所属房间时领取返回 `voice_room_red_packet_room_presence_required`
- 同一用户重复领取同一 `packetNo` 返回同一领取结果。
- 抢完后房间收到 `STATUS_CHANGED`,侧边栏数量减少,抢按钮隐藏。
- 领取记录页使用 `claim-records?page=&pageSize=`,每条记录有 `account/nickname/avatar`
- 重连或重新进房后调用 `active` 恢复侧边栏,不依赖历史 IM 补全状态。

View File

@ -1,282 +0,0 @@
# 进房接口 Flutter 对接
## 接口地址
```http
POST /api/v1/rooms/join
Authorization: Bearer <access_token>
Content-Type: application/json
```
新版本进房默认使用 `response_mode=lite``full` 仍保留给旧客户端兼容,不建议新 Flutter 首屏使用。
## 参数
Body
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 房间 ID。 |
| `command_id` | string | 是 | 本次进房命令 ID。同一次点击超时重试复用同一个值下一次重新进房换新值。 |
| `role` | string | 否 | 不传默认 `audience`。房主身份由服务端按房间 owner 判断,客户端不要靠传 `owner` 获取权限。 |
| `password` | string | 否 | 密码房传房间密码,非密码房不传或传空。 |
| `response_mode` | string | 是 | 新版固定传 `lite`。可选值:`lite` / `full`。 |
请求示例:
```json
{
"room_id": "lalu_abc123",
"command_id": "cmd_join_room_lalu_abc123_10001_1778000001000",
"role": "audience",
"password": "",
"response_mode": "lite"
}
```
也支持 query 参数:
```http
POST /api/v1/rooms/join?response_mode=lite
```
Flutter 建议放在 body 里,避免调用层漏拼 query。
## 返回值
统一外壳:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {}
}
```
成功返回示例:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"result": {
"applied": true,
"room_version": 8,
"server_time_ms": 1778000001123
},
"room": {
"room_id": "lalu_abc123",
"im_group_id": "lalu_abc123",
"room_short_id": "10001",
"title": "Live Room",
"cover_url": "https://cdn.example/room.png",
"background_url": "https://cdn.example/bg.png",
"description": "welcome",
"owner_user_id": "10001",
"mode": "voice",
"status": "active",
"chat_enabled": true,
"locked": false,
"heat": 88,
"online_count": 200,
"seat_count": 10,
"occupied_seat_count": 2,
"visible_region_id": 1001,
"version": 8
},
"viewer": {
"user_id": "10001",
"role": "audience",
"joined_at_ms": 1778000001000,
"last_seen_at_ms": 1778000001100
},
"seats": [
{
"seat_no": 1,
"user_id": "10002",
"locked": false,
"publish_state": "publishing",
"mic_session_id": "mic_10002",
"publish_deadline_ms": 1778000030000,
"mic_muted": false,
"seat_status": "publishing",
"mic_heartbeat_at_ms": 1778000000000,
"gift_value": 0
},
{
"seat_no": 2,
"locked": false,
"mic_muted": false,
"gift_value": 0
}
],
"profiles": [
{
"user_id": "10001",
"username": "host",
"avatar": "https://cdn.example/avatar.png",
"display_user_id": "10001"
},
{
"user_id": "10002",
"username": "speaker",
"avatar": "https://cdn.example/avatar2.png",
"display_user_id": "10002"
}
],
"im": {
"group_id": "lalu_abc123",
"need_join_group": true
},
"rtc": {
"need_token": true,
"available": true,
"token": {
"sdk_app_id": 1400000000,
"user_id": "10001",
"rtc_user_id": "10001",
"user_sig": "xxx",
"expire_at_ms": 1778007200000,
"room_id": "lalu_abc123",
"rtc_room_id": "lalu_abc123",
"rtc_room_id_type": "string",
"str_room_id": "lalu_abc123",
"app_scene": "voice_chat_room",
"role": "user"
}
},
"server_time_ms": 1778000001123
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `data.result.applied` | 本次进房是否真正写入。重复 `command_id` 重试可能为 `false`。 |
| `data.result.room_version` | 房间版本,用于客户端状态更新判断。 |
| `data.room.room_id` | 房间 ID。 |
| `data.room.im_group_id` | 腾讯 IM 房间群 ID。当前通常等于 `room_id`,客户端仍以返回值为准。 |
| `data.room.online_count` | 房间在线人数。`lite` 不返回在线用户明细,只返回人数。 |
| `data.room.seat_count` | 麦位总数。 |
| `data.room.occupied_seat_count` | 已占用麦位数。 |
| `data.viewer` | 当前登录用户在房间内的 presence。 |
| `data.viewer.role` | 当前用户房间角色,房主会由服务端返回 `owner`。 |
| `data.seats` | 首屏麦位状态。 |
| `data.seats[].user_id` | 麦上用户 ID空麦不返回该字段。 |
| `data.seats[].publish_state` | 麦上发流状态,常见值:`pending_publish` / `publishing`。 |
| `data.seats[].mic_session_id` | 麦位发流会话 ID。麦上心跳、发流确认需要使用它。 |
| `data.seats[].seat_status` | 麦位展示状态,常见值:`empty` / `locked` / `occupied` / `publishing` / `muted`。 |
| `data.profiles` | 首屏基础资料,只包含房主、当前用户、麦上用户。 |
| `data.profiles[].username` | 昵称。 |
| `data.profiles[].avatar` | 头像。 |
| `data.profiles[].display_user_id` | 展示 ID。 |
| `data.im.group_id` | Flutter 调腾讯 IM `joinGroup` 使用的群 ID。 |
| `data.im.need_join_group` | 为 `true` 时需要加入腾讯 IM 群。 |
| `data.rtc.available` | 是否拿到 RTC 票据。 |
| `data.rtc.token` | RTC 进房票据。`available=false` 时可能为空。 |
| `data.rtc.reason` | RTC 不可用原因。业务进房成功时RTC 不可用不代表进房失败。 |
| `data.server_time_ms` | 服务端时间UTC epoch milliseconds。 |
`lite` 不返回这些内容:
| 字段 | 说明 |
| --- | --- |
| `contribution_rank` | 贡献榜不在进房首屏主包返回。 |
| `profiles[].avatar_frame` | 头像框后置拉。 |
| `profiles[].profile_card` | 资料卡后置拉。 |
| `profiles[].vehicle` | 座驾后置拉。 |
| `profiles[].mic_seat_animation` | 麦位动画后置拉。 |
| `profiles[].badges` | 等级/成就徽章后置拉。 |
| `profiles[].vip` | VIP 展示后置拉。 |
## 相关 IM
进房成功后 Flutter 执行:
```text
1. POST /api/v1/rooms/joinbody 传 response_mode=lite
2. 如果 data.im.need_join_group=true调用腾讯 IM joinGroup(data.im.group_id)
3. joinGroup 成功或已在群内,都按进房 IM 成功处理
4. 页面开始房间 heartbeat
5. 如果 data.rtc.available=true用 data.rtc.token 进入 RTC
```
说明:
| 项 | 说明 |
| --- | --- |
| IM 登录 | 进房接口不返回 IM UserSig。Flutter 仍然通过 `GET /api/v1/im/usersig` 获取并登录腾讯 IM。 |
| IM 入群 | 必须先 `JoinRoom` 成功,再调腾讯 IM `joinGroup`。不要先入 IM 群。 |
| 入房广播 | 服务端会发 `RoomUserJoined` 房间事件。Flutter 按现有房间 IM 事件逻辑展示入房飘窗。 |
| 重复进房 | 用户重新打开房间页或重连时服务端仍会给展示用入房事件Flutter 正常展示即可。 |
| 入场座驾 | `lite` 进房不携带座驾快照。座驾、头像框、资料卡、麦位动画等展示资料在进房 UI 出来后异步补齐。 |
## 后置资料接口
进房首屏出来后,再按需要异步拉展示资料:
```http
GET /api/v1/users/room-display-profiles:batch?user_ids=10001,10002
Authorization: Bearer <access_token>
```
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `user_ids` | string | 是 | 逗号分隔的用户 ID。也支持多个 `user_id=10001&user_id=10002`。 |
返回值:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {
"profiles": [
{
"user_id": "10001",
"username": "host",
"avatar": "https://cdn.example/avatar.png",
"display_user_id": "10001",
"avatar_frame": {},
"profile_card": {},
"vehicle": {},
"mic_seat_animation": {},
"badges": [],
"vip": {},
"level": {},
"level_badges": {},
"charm": 0
}
]
}
}
```
当前用户自己的装扮也可以后置拉:
```http
GET /api/v1/appearance
GET /api/v1/users/me/appearance
```
## 常见错误
| code | HTTP | 说明 |
| --- | --- | --- |
| `INVALID_JSON` | 400 | 请求体不是合法 JSON。 |
| `INVALID_ARGUMENT` | 400 | 参数错误,例如 `response_mode` 不是 `lite``full`。 |
| `UNAUTHORIZED` | 401 | 未登录或 token 无效。 |
| `PROFILE_REQUIRED` | 403 | 用户资料未补全。 |
| `PERMISSION_DENIED` | 403 | 密码错误、被踢/被禁等权限问题。 |
| `ROOM_CLOSED` | 409 | 房间已关闭;非房主不能进入。 |
| `NOT_FOUND` | 404 | 房间不存在。 |
| `UPSTREAM_ERROR` | 502 | 上游服务异常。 |

View File

@ -1,477 +0,0 @@
# 通用确认消息 Flutter 对接
本文定义 Flutter 对接 `im_confirm` 私聊确认消息所需的 IM 解析、HTTP 接口、状态刷新和按钮置灰规则。业务类型首版是 `host / agency / bd` 身份邀请Flutter 只做通用展示,不写业务分支。
## 通用约定
### Base URL
```text
https://{api_host}
```
本地开发:
```text
http://127.0.0.1:13000
```
### Headers
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Authorization: Bearer {access_token}` | 是 | 登录后的 App token |
| `Content-Type: application/json` | POST 必填 | 请求体 JSON |
| `X-App-Code` | 否 | 多 App 包场景可传,通常由 gateway 解析 |
### 响应 Envelope
成功:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_xxx",
"data": {}
}
```
失败:
```json
{
"code": "CONFLICT",
"message": "conflict",
"request_id": "req_xxx"
}
```
## IM 消息格式
腾讯 IM C2C 自定义消息:
| TIM 字段 | 值 |
| --- | --- |
| `Desc` | `im_confirm` |
| `Ext` | `im_confirm` |
| `Data` | JSON 字符串 |
`Data` 示例:
```json
{
"type": "im_confirm",
"version": 1,
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"msg": "Alice has invited you to join ABC guild",
"status": "pending",
"actions": [
{
"action": "reject",
"label": "Reject",
"style": "secondary"
},
{
"action": "accept",
"label": "Accept",
"style": "primary"
}
],
"created_at_ms": 1710000000000,
"expire_at_ms": 1710604800000
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `type` | string | 固定 `im_confirm` |
| `version` | int | 当前为 `1` |
| `confirm_id` | string | 确认消息 ID后续接口只传这个 |
| `business_type` | string | 业务类型,只用于日志和埋点 |
| `business_subtype` | string | 业务子类型,只用于展示或埋点 |
| `msg` | string | 气泡文案 |
| `status` | string | 初始状态,一般为 `pending` |
| `actions` | array | 按钮配置 |
| `created_at_ms` | int64 | 创建时间 |
| `expire_at_ms` | int64 | 过期时间,可能为 0 |
Flutter 不要把 `business_type``business_subtype` 当成接口参数。确认接口只需要 `confirm_id`
## 状态含义
| 状态 | Flutter 展示 |
| --- | --- |
| `pending` | 展示可点击按钮 |
| `processing` | 按钮置灰,显示处理中或本地 loading |
| `accepted` | 按钮置灰,接受态 |
| `rejected` | 按钮置灰,拒绝态 |
| `cancelled` | 按钮置灰,已取消 |
| `expired` | 按钮置灰,已过期 |
| `failed` | 按钮置灰,可提示稍后重试或联系客服 |
按钮是否可点只看最新状态是否为 `pending`
## 接口 1接受
地址:
```text
POST /api/v1/message/action-confirms/{confirm_id}/accept
```
参数:
```json
{
"command_id": "cfm_123_accept_001"
}
```
返回值 `data`
```json
{
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "accepted",
"processed_action": "accept",
"processed_at_ms": 1710000001000
}
```
相关 IM用户点击 `actions.action=accept` 的按钮时调用。
## 接口 2拒绝
地址:
```text
POST /api/v1/message/action-confirms/{confirm_id}/reject
```
参数:
```json
{
"command_id": "cfm_123_reject_001",
"reason": ""
}
```
返回值 `data`
```json
{
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "rejected",
"processed_action": "reject",
"processed_at_ms": 1710000001000
}
```
相关 IM用户点击 `actions.action=reject` 的按钮时调用。
## 接口 3批量刷新状态
地址:
```text
POST /api/v1/message/action-confirms/status
```
参数:
```json
{
"confirm_ids": ["cfm_123", "cfm_456"]
}
```
返回值 `data`
```json
{
"items": [
{
"confirm_id": "cfm_123",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "accepted",
"processed_action": "accept",
"processed_at_ms": 1710000001000
}
]
}
```
使用场景:
- 打开会话时,收集当前会话里所有 `im_confirm.confirm_id` 后批量刷新。
- App 从后台回到前台时,刷新当前可见页面的确认消息状态。
- 接口返回的状态覆盖 IM 初始 `status`
## 接口 4按会话刷新状态
地址:
```text
GET /api/v1/message/action-confirms?peer_user_id=10001&page_size=50
```
参数:
| 参数 | 必填 | 说明 |
| --- | --- | --- |
| `peer_user_id` | 是 | 当前会话对方用户 ID |
| `page_size` | 否 | 默认 50最大 100 |
| `page_token` | 否 | 下一页游标 |
返回值 `data`
```json
{
"items": [
{
"confirm_id": "cfm_123",
"sender_user_id": "10001",
"receiver_user_id": "10002",
"business_type": "role_invitation",
"business_subtype": "host",
"business_id": "987",
"status": "pending",
"processed_action": "",
"processed_at_ms": 0,
"created_at_ms": 1710000000000
}
],
"next_page_token": ""
}
```
使用场景:
- 当前 IM SDK 本地消息无法快速遍历 `confirm_id` 时使用。
- 当前用户由 token 决定Flutter 只传对方 `peer_user_id`
## Flutter 数据模型
```dart
class ImConfirmMessage {
final String confirmId;
final String businessType;
final String businessSubtype;
final String msg;
final String status;
final List<ConfirmAction> actions;
final int createdAtMs;
final int expireAtMs;
ImConfirmMessage({
required this.confirmId,
required this.businessType,
required this.businessSubtype,
required this.msg,
required this.status,
required this.actions,
required this.createdAtMs,
required this.expireAtMs,
});
bool get canOperate => status == 'pending';
factory ImConfirmMessage.fromJson(Map<String, dynamic> json) {
return ImConfirmMessage(
confirmId: (json['confirm_id'] ?? '').toString(),
businessType: (json['business_type'] ?? '').toString(),
businessSubtype: (json['business_subtype'] ?? '').toString(),
msg: (json['msg'] ?? '').toString(),
status: (json['status'] ?? 'pending').toString(),
actions: ((json['actions'] as List?) ?? [])
.map((item) => ConfirmAction.fromJson(Map<String, dynamic>.from(item as Map)))
.toList(),
createdAtMs: (json['created_at_ms'] as num?)?.toInt() ?? 0,
expireAtMs: (json['expire_at_ms'] as num?)?.toInt() ?? 0,
);
}
}
class ConfirmAction {
final String action;
final String label;
final String style;
ConfirmAction({
required this.action,
required this.label,
required this.style,
});
factory ConfirmAction.fromJson(Map<String, dynamic> json) {
return ConfirmAction(
action: (json['action'] ?? '').toString(),
label: (json['label'] ?? '').toString(),
style: (json['style'] ?? '').toString(),
);
}
}
class ConfirmStatus {
final String confirmId;
final String status;
final String processedAction;
final int processedAtMs;
ConfirmStatus({
required this.confirmId,
required this.status,
required this.processedAction,
required this.processedAtMs,
});
bool get canOperate => status == 'pending';
factory ConfirmStatus.fromJson(Map<String, dynamic> json) {
return ConfirmStatus(
confirmId: (json['confirm_id'] ?? '').toString(),
status: (json['status'] ?? '').toString(),
processedAction: (json['processed_action'] ?? '').toString(),
processedAtMs: (json['processed_at_ms'] as num?)?.toInt() ?? 0,
);
}
}
```
## IM 解析
```dart
ImConfirmMessage? parseImConfirmCustomData(String data) {
final dynamic decoded = jsonDecode(data);
if (decoded is! Map) return null;
final map = Map<String, dynamic>.from(decoded);
if (map['type'] != 'im_confirm') return null;
final confirmId = (map['confirm_id'] ?? '').toString();
if (confirmId.isEmpty) return null;
return ImConfirmMessage.fromJson(map);
}
```
解析规则:
- 只处理 `type == im_confirm`
- `confirm_id` 为空时按普通不支持消息处理。
- `msg` 直接展示,不在 Flutter 端拼接业务文案。
- `actions` 为空时只展示文案,不展示按钮。
## API 调用
```dart
String newConfirmCommandId(String confirmId, String action) {
return '${confirmId}_${action}_${DateTime.now().millisecondsSinceEpoch}';
}
Future<ConfirmStatus> acceptConfirm(String confirmId) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/message/action-confirms/$confirmId/accept'),
headers: authJsonHeaders(),
body: jsonEncode({
'command_id': newConfirmCommandId(confirmId, 'accept'),
}),
);
return parseConfirmStatusEnvelope(response);
}
Future<ConfirmStatus> rejectConfirm(String confirmId, {String reason = ''}) async {
final response = await http.post(
Uri.parse('$baseUrl/api/v1/message/action-confirms/$confirmId/reject'),
headers: authJsonHeaders(),
body: jsonEncode({
'command_id': newConfirmCommandId(confirmId, 'reject'),
'reason': reason,
}),
);
return parseConfirmStatusEnvelope(response);
}
```
`command_id` 每次点击生成一个新值。网络重试同一次请求时可以复用同一个 `command_id`
## 状态刷新
```dart
Future<Map<String, ConfirmStatus>> batchRefreshConfirmStatus(List<String> confirmIds) async {
final ids = confirmIds.where((id) => id.isNotEmpty).toSet().toList();
if (ids.isEmpty) return {};
final response = await http.post(
Uri.parse('$baseUrl/api/v1/message/action-confirms/status'),
headers: authJsonHeaders(),
body: jsonEncode({'confirm_ids': ids}),
);
final data = parseEnvelopeData(response);
final items = (data['items'] as List? ?? []);
return {
for (final item in items)
ConfirmStatus.fromJson(Map<String, dynamic>.from(item as Map)).confirmId:
ConfirmStatus.fromJson(Map<String, dynamic>.from(item as Map)),
};
}
```
刷新时机:
- 进入 IM 会话页。
- App resume。
- 收到新的 `im_confirm`
- 接受或拒绝接口返回 `CONFLICT`
## UI 规则
- 气泡文案展示 `msg`
- `status == pending` 时展示 `actions` 里的按钮。
- `status != pending` 时按钮置灰,不能再次点击。
- 本地发起请求后,当前按钮进入 loading同一条消息两个按钮都置灰。
- 接口成功后,用返回状态覆盖本地状态。
- 接口失败但不是鉴权错误时,调用状态接口刷新一次。
按钮样式建议:
| style | 展示 |
| --- | --- |
| `primary` | 主按钮,例如绿色 Accept |
| `secondary` | 次按钮,例如描边 Reject |
Flutter 不要根据 `business_subtype` 写死按钮文案,按钮文案以 `actions.label` 为准。
## 错误处理
| code | Flutter 处理 |
| --- | --- |
| `UNAUTHORIZED` | 重新登录 |
| `PERMISSION_DENIED` | 置灰并提示无权限 |
| `NOT_FOUND` | 置灰,消息已失效 |
| `CONFLICT` | 调状态接口刷新,按最新状态置灰 |
| `UPSTREAM_ERROR` | 解除 loading允许稍后重试 |
| `INTERNAL_ERROR` | 解除 loading提示稍后重试 |
重复点击、跨设备已处理、旧消息重新打开,都以状态接口返回为准。
## 对接检查
- 收到 `im_confirm` 后能渲染文案和两个按钮。
- 点击 Accept 后调用接受接口,成功后按钮置灰。
- 点击 Reject 后调用拒绝接口,成功后按钮置灰。
- 同一条消息重复点击不会重复请求;如果请求已经发出,两个按钮都置灰。
- App 重新进入会话后,通过状态接口刷新旧消息状态。
- 另一个设备处理后,本设备 resume 能刷新为终态。
- `CONFLICT` 时不弹业务成功,只刷新状态后展示最终状态。

View File

@ -1,474 +0,0 @@
# game-service 游戏结构设计
本文定义 `game-service` 后续承载游戏的结构边界。当前仓库已经有第三方 H5 游戏接入能力:游戏列表、启动会话、厂商回调、钱包改账、统计 outbox 和等级事件 outbox 都在 `game-service` 内。本文补的是自研小游戏的结构,避免把骰子、猜拳这类玩法塞进第三方平台 adapter。
## 结论
- `game-service` 是游戏事实 owner。游戏列表、启动会话、匹配、局内状态、结算、补偿和游戏事件都归它管理。
- `gateway-service` 只做 App HTTP 入口、鉴权、参数转换和 gRPC 转发,不保存游戏状态。
- `wallet-service` 仍然是金币 owner。`game-service` 只能通过钱包 gRPC 扣款、派奖、退款,不能直接改余额。
- `room-service` 不拥有游戏状态。房间只提供入口、房间上下文和必要的展示事件。
- 第三方 H5 游戏继续走现有 `platform/catalog/session/callback/order` 结构。
- 自研小游戏单独按 `game_code` 建领域模块、服务模块和表,不提前抽一个复杂通用引擎。
- 只有当两个以上自研游戏出现相同匹配、计时、结算、IM 投递逻辑时,再把公共能力抽到 `internal/domain/minigame``internal/service/minigame`
## 当前事实
现有入口:
- App 游戏列表:`GET /api/v1/games`
- 最近游戏:`GET /api/v1/games/recent`
- 桥接脚本:`GET /api/v1/games/bridge-script`
- 启动游戏:`POST /api/v1/games/{game_id}/launch`
- 厂商回调:`POST /api/v1/game-callbacks/{platform_code}/{operation}`
现有内部 RPC
- `GameAppService.ListGames`
- `GameAppService.ListRecentGames`
- `GameAppService.GetBridgeScript`
- `GameAppService.LaunchGame`
- `GameCallbackService.HandleCallback`
- `GameAdminService` 管理平台和目录
- `GameCronService.ProcessLevelEventOutboxBatch`
现有核心表:
- `game_platforms`:第三方平台配置
- `game_catalog`App 看到的游戏目录
- `game_display_rules`:按场景、区域、语言和客户端平台控制展示
- `game_launch_sessions`H5 启动会话
- `game_orders`:第三方或游戏结算引起的钱包订单
- `game_callback_logs`:第三方回调审计
- `game_level_event_outbox`:游戏消耗投递 activity-service
- `game_outbox`:游戏订单事实投递 RocketMQ给 statistics-service 等下游消费
## 分层结构
```text
gateway-service
internal/transport/http/gameapi
只接 App HTTP拿 user_id、request_id、app_code 后转 game-service gRPC
api/proto/game/v1
game.proto
所有跨服务契约,新增 App 接口或后台接口先改这里
services/game-service
internal/domain/game
已有通用游戏目录、平台、会话、订单、回调、outbox 领域对象
internal/service/game
已有第三方 H5 平台接入和游戏列表、启动、回调主流程
internal/domain/<game_code>
自研游戏纯规则不能依赖数据库、gRPC、HTTP、钱包 client
internal/service/<game_code>
自研游戏用例层,负责匹配、状态推进、调用钱包、写 outbox
internal/storage/mysql/<game_code>_repository.go
自研游戏持久化,行锁、幂等、状态转移都在这里收口
internal/transport/grpc
把 game.proto 的 RPC 转成 service 调用,不写业务规则
```
目录原则:
- `internal/service/game` 保留给已有平台接入,不继续堆自研玩法。
- 每个自研玩法用独立包,例如 `internal/domain/dice``internal/service/dice`
- 自研玩法可以复用 `game_catalog``game_display_rules` 做列表展示。
- 自研玩法不走第三方 `PlatformAdapter`,也不走 `HandleCallback`
- 自研玩法的 HTTP 入口仍在 gateway跨服务契约仍在 `api/proto/game/v1/game.proto`
## 游戏类型
| 类型 | 例子 | 列表和启动 | 状态 owner | 结算方式 |
| ---------------- | ---------------------- | ------------------------------------------------- | --------------------------------------- | --------------------------------------------- |
| 第三方 H5 | 百顺、VivaGames、Reyou | 现有 `/games``/launch` | 第三方平台game-service 保存会话和订单 | 厂商回调到 game-service再调 wallet-service |
| 自研 H5 多人结算 | 骰子首版 | 复用 `/games``/launch`,局内操作走新增 HTTP | game-service | game-service 直接调 wallet-service |
| 自研实时对战 | 猜拳、后续双人骰子 | 复用 `/games``/launch`,匹配和出招走新增 HTTP | game-service | game-service 调 wallet-serviceIM 只下发事件 |
## 自研游戏统一链路
```mermaid
sequenceDiagram
participant App
participant Gateway as gateway-service
participant Game as game-service
participant Wallet as wallet-service
participant Notice as notice-service/Tencent IM
participant DB as hyapp_game
App->>Gateway: POST /api/v1/games/{game_id}/launch
Gateway->>Game: LaunchGame
Game->>DB: create game_launch_sessions
Game-->>Gateway: launch_url/session_id
Gateway-->>App: H5 url
App->>Gateway: POST /api/v1/games/dice/matches
Gateway->>Game: Create self-game match
Game->>DB: create match + first participant
App->>Gateway: POST /api/v1/games/dice/matches/{match_id}/join
Gateway->>Game: JoinDiceMatch
Game->>DB: lock match, insert participant
App->>Gateway: POST /api/v1/games/dice/matches/{match_id}/roll
Gateway->>Game: RollDiceMatch
Game->>DB: claim match for settling
Game->>Wallet: ApplyGameCoinChange debit/credit/refund
Wallet-->>Game: wallet_transaction_id/balance_after
Game->>DB: save dice points, orders, result and outbox
Game-->>Gateway: match/result state
Gateway-->>App: business response
Game-->>Notice: optional realtime outbox
Notice-->>App: optional IM custom event
```
关键约束:
- 用户身份只信任 gateway 从 JWT 得到的 `user_id`
- `request_id` 只做链路追踪,不做业务幂等键。
- 业务幂等键使用 `command_id``match_id``round_id``provider_order_id` 或唯一索引。
- 所有时间字段使用 UTC epoch milliseconds字段名统一 `*_ms`
- 金币金额必须是正数,扣款、派奖、退款用 `op_type` 表达方向。
- 同一局的投注、派奖、退款必须能通过 `match_id/round_id/order_id` 追踪。
## 自研游戏表结构原则
不要用一个大表兼容所有玩法。每个玩法先建自己的事实表,公共 outbox 可以复用。
建议公共表:
### game_realtime_outbox
用于自研游戏的 IM 实时事件。不要复用 `game_outbox`,因为 `game_outbox` 是订单和统计事实,字段强依赖 `order_id/op_type/coin_amount`
关键字段:
- `app_code`
- `event_id`
- `event_type`
- `game_code`
- `match_id`
- `target_user_id`
- `payload_json`
- `status`
- `worker_id`
- `lock_until_ms`
- `retry_count`
- `next_retry_at_ms`
- `last_error`
- `created_at_ms`
- `updated_at_ms`
推荐索引:
- `PRIMARY KEY(app_code, event_id)`
- `idx_game_realtime_claim(app_code, status, next_retry_at_ms, created_at_ms, event_id)`
- `idx_game_realtime_target(app_code, target_user_id, created_at_ms)`
## Redis 和 MySQL 边界
局信息可以同时出现在 MySQL 和 Redis但不能是“双重事实”。
- MySQL 是唯一事实。参与人、扣款、派奖、结果、状态流转和补偿都必须能只靠 MySQL 恢复。
- Redis 只保存热状态。可以存等待队列、房间当前局快照、倒计时、座位临时锁和防重复点击标记。
- Redis 数据必须可丢。Redis 重启、Key 过期或缓存击穿时,服务必须从 MySQL 查回当前局。
- 金币相关状态只以 MySQL 和 wallet-service 返回为准,不以 Redis 为准。
- 关键状态推进用 MySQL 事务、唯一索引和行锁收口Redis 锁只能减少并发压力,不能作为最终防线。
推荐 Key
- `game:dice:room:{app_code}:{room_id}:active_match`房间当前局快照TTL 按局时长加缓冲。
- `game:dice:match:{app_code}:{match_id}`局详情快照TTL 按局时长加缓冲。
- `game:dice:join_lock:{app_code}:{match_id}`:短 TTL 加入锁,用来减少并发抢座。
- `game:dice:action:{app_code}:{match_id}:{user_id}:{action}`:短 TTL 动作防抖,最终幂等仍看 MySQL。
写入顺序:
1. 先在 MySQL 事务里锁定 `game_dice_matches`
2. 创建/加入只写 `game_dice_participants` 和局状态。
3. 开奖阶段用钱包幂等键写 `game_orders`,再保存点数、派奖和局结果。
4. MySQL 事实提交后刷新 Redis 快照。
5. Redis 刷新失败只记录日志和重试,不回滚已提交的 MySQL 事实。
## 骰子游戏首版结构
首版按多人可扩展结构设计。当前实现要求至少 2 人,默认最多 2 人,硬上限 6 人3 人局只增加参与者行不改表。H5 只负责展示和发起操作,随机、扣款、派奖和结果都由 `game-service` 完成。
当前骰子规则是单局胜场制。每个参与者每轮只扔 1 颗骰子,服务端把这个点数写入 `dice_points[0]`点数高的一方获胜。同点不是终局不退本金、不派奖H5 展示点数 2 秒后继续调用 `roll`,同一个 `match_id` 会一直重投到分出胜负。
### 模块
```text
services/game-service/internal/domain/dice
rule.go 赔率、可选押注、结果计算
match.go match 状态、参与者、局次、结果
services/game-service/internal/service/dice
service.go 创建局、加入局、扣款、开奖、派奖、查询
services/game-service/internal/storage/mysql/dice_repository.go
创建局、参与人行锁读取、状态推进、订单关联、查询
```
### 状态机
```mermaid
stateDiagram-v2
[*] --> created
created --> joining
joining --> settling
created --> settling
settling --> payout_applying
payout_applying --> settled
settling --> failed
payout_applying --> failed
```
状态说明:
- `created`:收到创建请求,已生成 `match_id`
- `joining`:等待参与者加入,人数达到 `min_players` 后可以开奖。
- `settling`:已抢占结算权,正在扣押注、生成骰子结果。
- `payout_applying`:骰子点数已经固定,正在派奖或退款;重试必须复用旧点数。
- `settled`:局已结束,结果固定。
- `failed`:扣款、派奖或退款失败,后续由补偿任务继续处理。
### 表
#### game_dice_matches
关键字段:
- `app_code`
- `match_id`
- `game_id`
- `platform_code`
- `provider_game_id`
- `room_id`
- `region_id`
- `min_players`
- `max_players`
- `current_players`
- `stake_coin`
- `round_no`
- `result`
- `status`
- `join_deadline_ms`
- `created_at_ms`
- `updated_at_ms`
- `settled_at_ms`
索引:
- `PRIMARY KEY(app_code, match_id)`
- `idx_game_dice_room_active(app_code, room_id, status, created_at_ms)`
- `idx_game_dice_status_time(app_code, status, updated_at_ms, match_id)`
#### game_dice_participants
一局一个参与者一行。2 人、3 人或 6 人只改变参与者数量,不改主表结构。
关键字段:
- `app_code`
- `match_id`
- `user_id`
- `seat_no`
- `status`
- `stake_coin`
- `dice_points_json`
- `result`
- `payout_coin`
- `debit_order_id`
- `payout_order_id`
- `refund_order_id`
- `balance_after`
- `joined_at_ms`
- `updated_at_ms`
索引:
- `PRIMARY KEY(app_code, match_id, user_id)`
- `UNIQUE KEY uk_game_dice_seat(app_code, match_id, seat_no)`
- `idx_game_dice_participant_user(app_code, user_id, updated_at_ms)`
- `idx_game_dice_participant_status(app_code, match_id, status, seat_no)`
#### game_dice_round_logs
如果首版只有一局,可以先不建。若后续做多轮、连胜、多人奖池或房间内广播,再补小局日志表。
关键字段:
- `app_code`
- `match_id`
- `round_no`
- `result_json`
- `created_at_ms`
## 骰子接口草案
接口文档只保留地址、参数、返回值和相关 IM。
### 1. 创建骰子局
地址:`POST /api/v1/games/dice/matches`
参数:
- `game_id`
- `room_id`
- `stake_coin`
- `min_players`
- `max_players`
返回值:
- `match_id`
- `status`
- `min_players`
- `max_players`
- `current_players`
- `stake_coin`
- `participants`
- `server_time_ms`
相关 IM
- 首版无
### 2. 加入骰子局
地址:`POST /api/v1/games/dice/matches/{match_id}/join`
参数:
- `match_id`
返回值:
- `match_id`
- `status`
- `min_players`
- `max_players`
- `current_players`
- `stake_coin`
- `participants`
- `server_time_ms`
相关 IM
- 首版无
### 3. 查询骰子局
地址:`GET /api/v1/games/dice/matches/{match_id}`
参数:
- `match_id`
返回值:
- `match_id`
- `status`
- `min_players`
- `max_players`
- `current_players`
- `stake_coin`
- `result`
- `participants`
- `server_time_ms`
相关 IM
- 首版无
### 4. 开奖
地址:`POST /api/v1/games/dice/matches/{match_id}/roll`
参数:
- `match_id`
返回值:
- `match_id`
- `status`
- `stake_coin`
- `result`
- `participants`
- `server_time_ms`
相关 IM
- 首版无
- 如果后续做房间公屏展示,再增加 `dice_match_settled`
## 钱包和订单
自研游戏也要落 `game_orders`,不能直接只写玩法表。
骰子推荐订单:
- 押注扣款:`op_type=debit``provider_order_id=dice:{match_id}:{user_id}:debit`
- 派奖加款:`op_type=credit``provider_order_id=dice:{match_id}:{user_id}:credit`
- 异常退款:`op_type=refund``provider_order_id=dice:{match_id}:{user_id}:refund`
`wallet-service``CommandId`
- `game:dice:{match_id}:{user_id}:debit`
- `game:dice:{match_id}:{user_id}:credit`
- `game:dice:{match_id}:{user_id}:refund`
扣款成功后:
- 写 `game_orders`
- 写 `game_outbox`,供统计使用
- 写 `game_level_event_outbox`,供 activity-service 游戏等级使用
派奖和退款是否进等级轨道由产品规则决定,默认只有 `debit` 进游戏等级。
## IM 事件边界
首版骰子不强依赖 IM。客户端发起 HTTPHTTP 返回结果。
需要 IM 的场景:
- 双人匹配成功
- 倒计时开始
- 双方状态变化
- 结果揭晓
- 对方离开或超时
IM 规则:
- 客户端不能通过 IM 提交游戏动作。
- 客户端动作必须走 HTTP 到 gateway再转 game-service。
- IM 只承载服务端事实事件。
- IM payload 必须带 `event_id``game_code``match_id``server_time_ms`
- 客户端按 `event_id` 去重。
## 上线顺序
1. 先补 `api/proto/game/v1/game.proto` 的骰子 RPC。
2. 生成 proto`make proto`
3. 在 `gateway-service/internal/transport/http/gameapi` 加 HTTP handler。
4. 在 `game-service``domain/dice``service/dice``storage/mysql/dice_repository.go`
5. 更新 `services/game-service/deploy/mysql/initdb/001_game_service.sql`
6. 更新 runtime `Migrate`,保证线上库自动补表和索引。
7. 后台把骰子写入 `game_catalog` 和默认 `game_display_rules`
8. H5 `game/touzi` 通过启动页拿 `game_id/session_id`,局内操作走新增骰子接口。
9. 跑测试:`make proto``go test ./services/game-service/... ./services/gateway-service/...`
10. 本地拉起:`docker compose config`,必要时 `docker compose up -d game-service gateway-service` 验证。
## 不做的事
- 不把骰子局状态放进 `room-service`
- 不让 H5 直接调 `wallet-service`
- 不把骰子动作放进第三方 `HandleCallback`
- 不把所有玩法硬塞进一个通用 `game_matches` 大表。
- 不用 Redis 做唯一事实。Redis 后续只能做短期匹配队列、限流或缓存MySQL 仍是恢复来源。

View File

@ -267,7 +267,7 @@ rocketmq:
1. 钱包余额通知:已按 `walletnotice` 模块落地。 1. 钱包余额通知:已按 `walletnotice` 模块落地。
2. 房间踢人私有通知:已按 `roomnotice` 模块落地,只消费 `RoomUserKicked` 的 C2C 通知。 2. 房间踢人私有通知:已按 `roomnotice` 模块落地,只消费 `RoomUserKicked` 的 C2C 通知。
3. 幸运礼物中奖:仍由 `wallet-service` 写余额 outboxnotice 不需要新增特殊账务逻辑,只要客户端按 `balance_version` 更新。 3. 幸运礼物中奖:仍由 `wallet-service` 写余额 outboxnotice 不需要新增特殊账务逻辑,只要客户端按 `balance_version` 更新。
4. 语音房火箭私有通知:如产品需要私聊提醒,可在 `roomnotice` 中消费 `RoomRocketRewardGranted` 或由活动/系统消息模块转发;奖励入账事实仍由 room-service 调 wallet 完成notice 只投通知。 4. 语音房宝箱私有通知:如产品需要私聊提醒,可在 `roomnotice` 中消费 `RoomTreasureRewardGranted` 或由活动/系统消息模块转发;奖励入账事实仍由 room-service 调 wallet 完成notice 只投通知。
5. 活动/系统私有通知:新增 `activitynotice` 模块,读取 activity outbox复用 `notice_delivery_events` 或新增同语义位点表。 5. 活动/系统私有通知:新增 `activitynotice` 模块,读取 activity outbox复用 `notice_delivery_events` 或新增同语义位点表。
6. 站内 inbox新增 inbox 模块,写 notice-service 自己的 inbox 表,再按客户端分页接口读取。 6. 站内 inbox新增 inbox 模块,写 notice-service 自己的 inbox 表,再按客户端分页接口读取。
7. Push新增 push 模块,和 IM 投递使用不同 `channel`,独立重试和死信。 7. Push新增 push 模块,和 IM 投递使用不同 `channel`,独立重试和死信。

View File

@ -550,7 +550,6 @@ definitions:
- region - region
- country - country
- all_active_users - all_active_users
- all_registered_users
target_filter_json: target_filter_json:
type: string type: string
template_snapshot_json: template_snapshot_json:

View File

@ -123,7 +123,7 @@ responses:
schema: schema:
$ref: "#/definitions/ErrorEnvelope" $ref: "#/definitions/ErrorEnvelope"
Forbidden: Forbidden:
description: 已认证但资料未完成、被禁用、封禁或无权限;常见 `code` 为 `PROFILE_REQUIRED`、`USER_DISABLED`、`PERMISSION_DENIED` description: 已认证但被禁用、封禁或无权限。
schema: schema:
$ref: "#/definitions/ErrorEnvelope" $ref: "#/definitions/ErrorEnvelope"
NotFound: NotFound:
@ -393,62 +393,6 @@ paths:
$ref: "#/definitions/H5LinkListEnvelope" $ref: "#/definitions/H5LinkListEnvelope"
"502": "502":
$ref: "#/responses/UpstreamError" $ref: "#/responses/UpstreamError"
/api/v1/app/splash-screens:
get:
tags:
- app-config
summary: 获取 App 开屏配置
operationId: listSplashScreens
description: 公开读接口,返回后台 APP配置/开屏配置 中当前有效的开屏列表,客户端按列表顺序取最高优先级配置展示。
parameters:
- name: platform
in: query
required: false
type: string
enum: [android, ios]
description: 可选平台覆盖值;不传时读取 `X-App-Platform` / `X-Platform`。
- name: region_id
in: query
required: false
type: integer
format: int64
description: 当前用户区域 ID0 或不传只命中全区域配置。
- name: country
in: query
required: false
type: string
description: 当前用户国家码;不传时读取 `X-Country-Code` / `X-App-Country`。
- name: X-App-Platform
in: header
required: false
type: string
enum: [android, ios]
- name: X-Country-Code
in: header
required: false
type: string
responses:
"200":
description: 查询成功,`data.items` 返回当前可展示的开屏配置。
schema:
$ref: "#/definitions/SplashScreenListEnvelope"
"502":
$ref: "#/responses/UpstreamError"
/api/v1/app/popups:
get:
tags:
- app-config
summary: 获取 App 弹窗配置
operationId: listAppPopups
description: 公开读接口,返回后台 APP配置/弹窗配置 中当前有效的弹窗列表。客户端按列表顺序选择弹窗,并按 display_period_days 做本地展示频控。
parameters:
responses:
"200":
description: 查询成功,`data.items` 返回当前可展示的弹窗配置。
schema:
$ref: "#/definitions/PopupListEnvelope"
"502":
$ref: "#/responses/UpstreamError"
/api/v1/im/usersig: /api/v1/im/usersig:
get: get:
tags: tags:
@ -807,29 +751,6 @@ paths:
$ref: "#/responses/Internal" $ref: "#/responses/Internal"
"502": "502":
$ref: "#/responses/UpstreamError" $ref: "#/responses/UpstreamError"
/api/v1/users/me/invite-overview:
get:
tags:
- users
summary: 查询当前用户邀请概览
operationId: getMyInviteOverview
description: 返回当前登录用户自己的邀请码和邀请计数;该接口只服务私有 H5 分享页,不并入我的页首屏聚合摘要。
security:
- BearerAuth: []
parameters:
responses:
"200":
description: 查询成功,`data` 返回当前用户邀请概览。
schema:
$ref: "#/definitions/MyInviteOverviewEnvelope"
"401":
$ref: "#/responses/Unauthorized"
"403":
$ref: "#/responses/Forbidden"
"500":
$ref: "#/responses/Internal"
"502":
$ref: "#/responses/UpstreamError"
/api/v1/users/me/onboarding/complete: /api/v1/users/me/onboarding/complete:
post: post:
tags: tags:
@ -866,7 +787,7 @@ paths:
- users - users
summary: 修改当前用户基础资料 summary: 修改当前用户基础资料
operationId: updateMyProfile operationId: updateMyProfile
description: 只修改 `username`、`avatar`、`birth`主页背景图和国家修改都走独立接口。 description: 只修改 `username`、`avatar`、`birth`国家修改必须走独立国家接口。
security: security:
- BearerAuth: [] - BearerAuth: []
parameters: parameters:
@ -890,36 +811,6 @@ paths:
$ref: "#/responses/Internal" $ref: "#/responses/Internal"
"502": "502":
$ref: "#/responses/UpstreamError" $ref: "#/responses/UpstreamError"
/api/v1/users/me/profile-bg-img:
post:
tags:
- users
summary: 修改当前用户主页背景图
operationId: updateMyProfileBackground
description: App 先自行上传图片,本接口只接收图片 URL 并保存到 `profile_bg_img`,和背包、`profile_card`、佩戴无关。
security:
- BearerAuth: []
parameters:
- name: body
in: body
required: true
schema:
$ref: "#/definitions/UpdateUserProfileBackgroundRequest"
responses:
"200":
description: 修改成功,`data.profile_bg_img` 返回最新背景图 URL。
schema:
$ref: "#/definitions/UpdateUserProfileBackgroundEnvelope"
"400":
$ref: "#/responses/BadRequest"
"401":
$ref: "#/responses/Unauthorized"
"404":
$ref: "#/responses/NotFound"
"500":
$ref: "#/responses/Internal"
"502":
$ref: "#/responses/UpstreamError"
/api/v1/users/me/country/change: /api/v1/users/me/country/change:
post: post:
tags: tags:
@ -1014,41 +905,13 @@ paths:
$ref: "#/responses/Internal" $ref: "#/responses/Internal"
"502": "502":
$ref: "#/responses/UpstreamError" $ref: "#/responses/UpstreamError"
/api/v1/rooms/filters:
get:
tags:
- rooms
summary: 首页房间区域与国家筛选目录
operationId: listRoomFilters
description: >-
返回可直接用于房间发现列表的筛选项。每个 option 的 query_key/query_value 可原样作为
GET /api/v1/rooms 的查询参数All 的两个字段为空,表示不附加 region_id/country_code。
Huwaa 返回当前 app scope 的全部 active 区域及其 enabled 国家,其他 App 只返回当前登录用户所在 active 区域,防止下发不可用的跨区选项。
该路由要求 access token 有效且用户资料已完成;资料未完成返回 403/PROFILE_REQUIRED。
security:
- BearerAuth: []
responses:
"200":
description: 查询成功;国家项只包含 enabled 国家与 active 区域映射的交集。
schema:
$ref: "#/definitions/RoomFilterCatalogEnvelope"
"401":
$ref: "#/responses/Unauthorized"
"403":
$ref: "#/responses/Forbidden"
"502":
$ref: "#/responses/UpstreamError"
/api/v1/rooms: /api/v1/rooms:
get: get:
tags: tags:
- rooms - rooms
summary: 房间发现列表 summary: 房间发现列表
operationId: listRooms operationId: listRooms
description: >- description: 按当前登录用户的服务端 region_id 查询房间发现列表;响应卡片显式返回 `im_group_id`,当前值等于 `room_id`,但客户端不能硬编码该映射。
Huwaa 不传 region_id/country_code 时查询 All传 region_id 时查指定 active 区域,传 country_code 时查指定 enabled 国家,两者互斥。
非 Huwaa App 仍按当前登录用户的服务端 region_id 隔离,不接受客户端 region_id。
置顶之后的普通房依次是本国家有人、其他国家/区域有人、本国家没人、其他国家/区域没人。
响应卡片显式返回 im_group_id当前值等于 room_id但客户端不能硬编码该映射。
security: security:
- BearerAuth: [] - BearerAuth: []
parameters: parameters:
@ -1077,23 +940,6 @@ paths:
in: query in: query
required: false required: false
type: string type: string
- name: region_id
in: query
required: false
type: integer
format: int64
minimum: 1
description: Huwaa 区域筛选;必须原样使用 `/api/v1/rooms/filters` 的 active `region_id`,不得与 `country_code` 同传。
- name: country_code
in: query
required: false
type: string
description: Huwaa 国家筛选;必须原样使用 `/api/v1/rooms/filters` 的 enabled 大写国家码,不得与 `region_id` 同传。
- name: country
in: query
required: false
type: string
description: 旧客户端兼容别名,等价于 `country_code`;新客户端统一使用 `country_code`。
responses: responses:
"200": "200":
description: 查询成功,`data.rooms[*].im_group_id` 是 JoinRoom 成功后客户端加入的腾讯 IM 房间群 ID。 description: 查询成功,`data.rooms[*].im_group_id` 是 JoinRoom 成功后客户端加入的腾讯 IM 房间群 ID。
@ -1268,56 +1114,6 @@ paths:
$ref: "#/responses/Conflict" $ref: "#/responses/Conflict"
"502": "502":
$ref: "#/responses/UpstreamError" $ref: "#/responses/UpstreamError"
/api/v1/rooms/{room_id}/contribution-rank:
get:
tags:
- rooms
summary: 查询房内用户贡献周期榜
operationId: getRoomContributionRank
description: 房间内 day/week/month 三个 tab 使用的用户贡献榜gateway 使用 access token user_id 作为 viewerroom-service 要求 viewer 仍在该房间。查询只读周期统计表,不刷新 presence。
security:
- BearerAuth: []
parameters:
- name: room_id
in: path
required: true
type: string
pattern: "^[A-Za-z0-9_-]{1,48}$"
maxLength: 48
- name: period
in: query
required: false
type: string
default: day
enum:
- day
- week
- month
- name: limit
in: query
required: false
type: integer
format: int32
default: 20
minimum: 1
maximum: 100
responses:
"200":
description: 返回当前房间指定周期的用户贡献榜和榜单用户展示资料。
schema:
$ref: "#/definitions/RoomContributionRankEnvelope"
"400":
$ref: "#/responses/BadRequest"
"401":
$ref: "#/responses/Unauthorized"
"403":
$ref: "#/responses/Forbidden"
"404":
$ref: "#/responses/NotFound"
"409":
$ref: "#/responses/Conflict"
"502":
$ref: "#/responses/UpstreamError"
/api/v1/rooms/{room_id}/follow: /api/v1/rooms/{room_id}/follow:
post: post:
tags: tags:
@ -2028,7 +1824,7 @@ paths:
- manager-center - manager-center
summary: 经理中心赠送单个资源 summary: 经理中心赠送单个资源
operationId: grantManagerResource operationId: grantManagerResource
description: 客户端提交命令 ID、目标用户、资源 ID 和可选有效期;数量固定为 1有效期只能是 7/30/90 天,未传默认 7 天`grant_source` 固定为 `manager_center`。 description: 客户端只提交命令 ID、目标用户和资源 ID数量固定为 1有效期固定为 0`grant_source` 固定为 `manager_center`。
security: security:
- BearerAuth: [] - BearerAuth: []
parameters: parameters:
@ -2200,7 +1996,6 @@ paths:
tags: tags:
- resources - resources
summary: 佩戴我的资源 summary: 佩戴我的资源
description: vip_trial_card 会在 wallet-service 内部委托 VIP 专用状态机;该通用响应不含完整 VipStateVIP 客户端应使用 /api/v1/vip/trial-cards/{entitlement_id}/equip。
operationId: equipMyResource operationId: equipMyResource
security: security:
- BearerAuth: [] - BearerAuth: []
@ -2521,7 +2316,6 @@ definitions:
- DISPLAY_USER_ID_LEASE_EXPIRED - DISPLAY_USER_ID_LEASE_EXPIRED
- DISPLAY_USER_ID_PAYMENT_REQUIRED - DISPLAY_USER_ID_PAYMENT_REQUIRED
- USER_DISABLED - USER_DISABLED
- PROFILE_REQUIRED
- PERMISSION_DENIED - PERMISSION_DENIED
- NOT_FOUND - NOT_FOUND
- CONFLICT - CONFLICT
@ -2539,7 +2333,6 @@ definitions:
- conflict - conflict
- insufficient balance - insufficient balance
- permission denied - permission denied
- profile required
- not found - not found
request_id: request_id:
type: string type: string
@ -3136,7 +2929,7 @@ definitions:
- host_center - host_center
- agency_center - agency_center
- bd_center - bd_center
- superadmin_center - bd_leader_center
- coin_seller_center - coin_seller_center
- settings - settings
- language - language
@ -3182,28 +2975,6 @@ definitions:
server_time_ms: server_time_ms:
type: integer type: integer
format: int64 format: int64
MyInviteOverviewData:
type: object
required:
- my_invite_code
- invite_enabled
- invite_count
- valid_invite_count
- valid_invite_threshold_coin
properties:
my_invite_code:
type: string
invite_enabled:
type: boolean
invite_count:
type: integer
format: int64
valid_invite_count:
type: integer
format: int64
valid_invite_threshold_coin:
type: integer
format: int64
UserProfileData: UserProfileData:
type: object type: object
properties: properties:
@ -3272,9 +3043,6 @@ definitions:
updated_at_ms: updated_at_ms:
type: integer type: integer
format: int64 format: int64
profile_bg_img:
type: string
description: 用户主页信息卡背景图片 URL来自 users.profile_bg_img和 `profile_card` 装扮资源无关,未设置时为空字符串。
next_country_change_allowed_at_ms: next_country_change_allowed_at_ms:
type: integer type: integer
format: int64 format: int64
@ -3484,7 +3252,13 @@ definitions:
properties: properties:
key: key:
type: string type: string
description: 后台 H5 配置 keyApp 只对已接入的 key 做入口展示,例如 rank、explore_game_rank、game_rock、game_dice、vip、level、task。 enum:
- host-center
- manager-center
- bd-center
- bd-leader-center
- agency-center
- invite-user
label: label:
type: string type: string
url: url:
@ -3504,100 +3278,6 @@ definitions:
total: total:
type: integer type: integer
format: int32 format: int32
SplashScreenData:
type: object
properties:
id:
type: integer
format: uint64
cover_url:
type: string
description: 开屏图片 URL。
type:
type: string
enum: [h5, app]
description: h5 表示 param 是 H5 链接app 表示 param 由客户端解释。
param:
type: string
platform:
type: string
enum: [android, ios]
sort_order:
type: integer
format: int32
region_id:
type: integer
format: int64
country_code:
type: string
description:
type: string
starts_at_ms:
type: integer
format: int64
ends_at_ms:
type: integer
format: int64
updated_at_ms:
type: integer
format: int64
SplashScreenListData:
type: object
properties:
items:
type: array
items:
$ref: "#/definitions/SplashScreenData"
total:
type: integer
format: int32
PopupData:
type: object
properties:
id:
type: integer
format: uint64
code:
type: string
description: 弹窗编码,同一个 App 下唯一,用于客户端本地频控和埋点识别。
name:
type: string
description: 弹窗名字。
image_url:
type: string
description: 弹窗图片 URL为空表示无图片。
jump_type:
type: string
enum: [h5, app]
description: h5 表示 jump_url 是 H5 链接app 表示 jump_url 由客户端解释。
jump_url:
type: string
display_period_days:
type: integer
format: int32
description: 展示周期天数0 表示每次打开都展示。
sort_order:
type: integer
format: int32
starts_at_ms:
type: integer
format: int64
ends_at_ms:
type: integer
format: int64
updated_at_ms:
type: integer
format: int64
PopupListData:
type: object
properties:
items:
type: array
items:
$ref: "#/definitions/PopupData"
total:
type: integer
format: int32
AppBootstrapTab: AppBootstrapTab:
type: object type: object
properties: properties:
@ -3725,28 +3405,6 @@ definitions:
birth: birth:
type: string type: string
description: yyyy-mm-dd传空字符串表示清空生日。 description: yyyy-mm-dd传空字符串表示清空生日。
UpdateUserProfileBackgroundRequest:
type: object
required:
- url
properties:
url:
type: string
description: App 已上传好的个人主页背景图 URL。
UpdateUserProfileBackgroundData:
type: object
properties:
profile_bg_img:
type: string
profile:
$ref: "#/definitions/UserProfileData"
UpdateUserProfileBackgroundEnvelope:
allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase"
- type: object
properties:
data:
$ref: "#/definitions/UpdateUserProfileBackgroundData"
ChangeUserCountryRequest: ChangeUserCountryRequest:
type: object type: object
properties: properties:
@ -3917,16 +3575,10 @@ definitions:
resource_id: resource_id:
type: string type: string
description: 资源 IDwallet-service 会在事务内复验 `manager_grant_enabled`。 description: 资源 IDwallet-service 会在事务内复验 `manager_grant_enabled`。
duration_ms:
type: integer
format: int64
enum: [604800000, 2592000000, 7776000000]
default: 604800000
description: 资源有效期,仅允许 7/30/90 天;未传时 gateway 默认 7 天。
reason: reason:
type: string type: string
default: manager_center default: manager_center
description: 不接受 `quantity`、`wallet_asset_amount` 等客户端计算字段。 description: 不接受 `quantity`、`duration_ms`、`wallet_asset_amount` 等客户端计算字段。
ManagerResourceGrantData: ManagerResourceGrantData:
type: object type: object
properties: properties:
@ -4877,12 +4529,6 @@ definitions:
type: string type: string
room_short_id: room_short_id:
type: string type: string
country_code:
type: string
description: 房主国家投影的大写国家码,首页国家筛选和卡片展示均以该字段为准。
country_flag:
type: string
description: 根据 `country_code` 派生的国旗 emoji无有效国家投影时省略。
RoomListData: RoomListData:
type: object type: object
properties: properties:
@ -4892,134 +4538,6 @@ definitions:
$ref: "#/definitions/RoomListItemData" $ref: "#/definitions/RoomListItemData"
next_cursor: next_cursor:
type: string type: string
countries:
type: array
description: 兼容旧客户端的当前用户区域国家列表;完整跨区域目录请使用 `/api/v1/rooms/filters`。
items:
$ref: "#/definitions/RoomListCountryData"
RoomListCountryData:
type: object
required:
- country_code
properties:
country_id:
type: integer
format: int64
country_code:
type: string
country_name:
type: string
country_display_name:
type: string
country_flag:
type: string
sort_order:
type: integer
format: int32
RoomFilterAllData:
type: object
required:
- filter_type
- query_key
- query_value
properties:
filter_type:
type: string
enum: [all]
query_key:
type: string
description: 固定为空字符串;客户端不附加筛选参数。
query_value:
type: string
description: 固定为空字符串。
RoomFilterRegionData:
type: object
required:
- filter_type
- query_key
- query_value
- region_id
- region_code
- region_name
- sort_order
properties:
filter_type:
type: string
enum: [region]
query_key:
type: string
enum: [region_id]
query_value:
type: string
description: region_id 的十进制字符串,可原样回传。
region_id:
type: integer
format: int64
region_code:
type: string
region_name:
type: string
sort_order:
type: integer
format: int32
RoomFilterCountryData:
type: object
required:
- filter_type
- query_key
- query_value
- parent_region_id
- country_id
- country_code
- country_name
- country_display_name
- country_flag
- sort_order
properties:
filter_type:
type: string
enum: [country]
query_key:
type: string
enum: [country_code]
query_value:
type: string
description: 规范化后的大写 country_code可原样回传。
parent_region_id:
type: integer
format: int64
description: 仅用于客户端把国家分组到区域;筛选国家时不要同时传 region_id。
country_id:
type: integer
format: int64
country_code:
type: string
country_name:
type: string
country_display_name:
type: string
country_flag:
type: string
sort_order:
type: integer
format: int32
RoomFilterCatalogData:
type: object
required:
- all
- regions
- countries
properties:
all:
$ref: "#/definitions/RoomFilterAllData"
regions:
type: array
items:
$ref: "#/definitions/RoomFilterRegionData"
countries:
type: array
items:
$ref: "#/definitions/RoomFilterCountryData"
MyRoomData: MyRoomData:
type: object type: object
required: required:
@ -5125,25 +4643,6 @@ definitions:
updated_at_ms: updated_at_ms:
type: integer type: integer
format: int64 format: int64
RoomContributionRankItemData:
type: object
properties:
rank:
type: integer
format: int64
user_id:
type: string
score:
type: integer
format: int64
gift_value:
type: integer
format: int64
updated_at_ms:
type: integer
format: int64
profile:
$ref: "#/definitions/RoomUserProfileData"
RoomUserProfileData: RoomUserProfileData:
type: object type: object
properties: properties:
@ -5234,48 +4733,6 @@ definitions:
server_time_ms: server_time_ms:
type: integer type: integer
format: int64 format: int64
RoomContributionRankData:
type: object
required:
- room_id
- period
- start_at_ms
- end_at_ms
- total
- contribution_rank
- profiles
- server_time_ms
properties:
room_id:
type: string
period:
type: string
enum:
- day
- week
- month
start_at_ms:
type: integer
format: int64
description: 周期开始 UTC epoch ms包含。
end_at_ms:
type: integer
format: int64
description: 周期结束 UTC epoch ms不包含。
total:
type: integer
format: int64
contribution_rank:
type: array
items:
$ref: "#/definitions/RoomContributionRankItemData"
profiles:
type: array
items:
$ref: "#/definitions/RoomUserProfileData"
server_time_ms:
type: integer
format: int64
RoomFollowData: RoomFollowData:
type: object type: object
required: required:
@ -5574,13 +5031,6 @@ definitions:
properties: properties:
data: data:
$ref: "#/definitions/MyOverviewData" $ref: "#/definitions/MyOverviewData"
MyInviteOverviewEnvelope:
allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase"
- type: object
properties:
data:
$ref: "#/definitions/MyInviteOverviewData"
UserProfileEnvelope: UserProfileEnvelope:
allOf: allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase" - $ref: "#/definitions/GatewayOKEnvelopeBase"
@ -5665,20 +5115,6 @@ definitions:
properties: properties:
data: data:
$ref: "#/definitions/H5LinkListData" $ref: "#/definitions/H5LinkListData"
SplashScreenListEnvelope:
allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase"
- type: object
properties:
data:
$ref: "#/definitions/SplashScreenListData"
PopupListEnvelope:
allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase"
- type: object
properties:
data:
$ref: "#/definitions/PopupListData"
ResourceListEnvelope: ResourceListEnvelope:
allOf: allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase" - $ref: "#/definitions/GatewayOKEnvelopeBase"
@ -5763,13 +5199,6 @@ definitions:
properties: properties:
data: data:
$ref: "#/definitions/RoomListData" $ref: "#/definitions/RoomListData"
RoomFilterCatalogEnvelope:
allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase"
- type: object
properties:
data:
$ref: "#/definitions/RoomFilterCatalogData"
MyRoomEnvelope: MyRoomEnvelope:
allOf: allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase" - $ref: "#/definitions/GatewayOKEnvelopeBase"
@ -5791,13 +5220,6 @@ definitions:
properties: properties:
data: data:
$ref: "#/definitions/RoomSnapshotData" $ref: "#/definitions/RoomSnapshotData"
RoomContributionRankEnvelope:
allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase"
- type: object
properties:
data:
$ref: "#/definitions/RoomContributionRankData"
RoomFollowEnvelope: RoomFollowEnvelope:
allOf: allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase" - $ref: "#/definitions/GatewayOKEnvelopeBase"

View File

@ -927,9 +927,6 @@ definitions:
description: 用户选择的国家主数据 country_code。 description: 用户选择的国家主数据 country_code。
avatar: avatar:
type: string type: string
profile_bg_img:
type: string
description: 用户主页信息卡背景图片 URL属于用户资料字段和 `profile_card` 装扮资源无关。
birth: birth:
type: string type: string
description: yyyy-mm-dd。 description: yyyy-mm-dd。
@ -1039,7 +1036,6 @@ definitions:
type: string type: string
enum: enum:
- all_active_users - all_active_users
- all_registered_users
- region - region
- country - country
cursor_user_id: cursor_user_id:
@ -1089,22 +1085,6 @@ definitions:
properties: properties:
user: user:
$ref: "#/definitions/User" $ref: "#/definitions/User"
UpdateUserProfileBackgroundRequest:
type: object
properties:
meta:
$ref: "#/definitions/RequestMeta"
user_id:
type: integer
format: int64
profile_bg_img:
type: string
description: 用户主页背景图 URL。
UpdateUserProfileBackgroundResponse:
type: object
properties:
user:
$ref: "#/definitions/User"
CompleteOnboardingRequest: CompleteOnboardingRequest:
type: object type: object
properties: properties:

View File

@ -1,387 +0,0 @@
# wallet-service 模块化拆分方案
## 结论
`wallet-service` 需要一起拆三层:`service` 用例层、`storage/mysql` 持久化层、`transport/grpc` 协议适配层。只拆 `repository.go` 不够,因为当前 service 层的 `Repository` 接口已经把礼物扣费、充值、红包、资源、VIP、游戏账变、币商和投影混成一个大端口文件变小后边界仍然不清晰。
推荐路线是两阶段:
| 阶段 | 目标 | 变更方式 |
| --- | --- | --- |
| 第一阶段 | 低风险整理文件,不改行为 | 保持 `walletservice.Service``mysql.Repository` 和 gRPC 注册方式不变,只在同 package 内按业务域拆文件,并把大 `Repository` 接口拆成嵌入式能力接口 |
| 第二阶段 | 真正目录模块化 | 抽出 `ports``usecase/*` 子包,`wallet.Service` 只保留门面方法transport 继续依赖门面,避免 gRPC 层直接感知内部用例拆分 |
不要第一步就把 MySQL repository 拆成多个 Go package。钱包账务依赖同一个 `*sql.Tx` 保证账户、交易、分录、outbox 和幂等结果同事务提交;过早拆 package 会迫使大量事务 helper、metadata 和 outbox 构造函数导出,风险比收益大。
## 当前结构和问题
当前 wallet-service 的主要文件规模:
| 路径 | 行数 | 问题 |
| --- | ---: | --- |
| `internal/storage/mysql/repository.go` | 5089 | 混合核心账本、送礼、活动奖励、游戏账变、币商、工资兑换、outbox、账号锁、receipt 和 metadata |
| `internal/storage/mysql/resource_repository.go` | 3987 | 资源目录、资源组、礼物配置、发放、穿戴、商城和资源 outbox 混在一起 |
| `internal/service/wallet/service.go` | 1259 | service 门面、依赖注入、礼物、工资、币商、奖励、游戏、充值、VIP、投影混在一个文件 |
| `internal/service/wallet/service_test.go` | 5625 | 测试文件过大,说明 service 用例边界没有被稳定拆开 |
| `internal/transport/grpc/server.go` | 1389 | gRPC handler 和 proto/domain mapper 混合,资源和红包已经拆出一部分,但主文件仍覆盖过多业务 |
| `internal/app/app.go` | 576 | 进程装配、MQ 生命周期、outbox worker、投影 consumer、三方支付补偿和红包过期 worker 混合 |
| `internal/domain/ledger/ledger.go` | 1407 | 账本领域对象混合所有业务命令和回执 |
| `internal/domain/resource/resource.go` | 729 | 资源目录、礼物配置、用户权益、商城和发放对象混合 |
当前最核心的问题不是单个文件长,而是变化原因互相交叉:
| 变化原因 | 当前落点 | 应独立出来的边界 |
| --- | --- | --- |
| 送礼扣费和背包送礼 | `service.go` + `repository.go` + `resource_repository.go` | gift ledger |
| 后台调账、活动奖励、游戏账变 | `service.go` + `repository.go` | ledger operations |
| Google、H5、MiFaPay、V5Pay、USDT | `service.go` + `external_recharge.go` + `third_party_payment_repository.go` | recharge |
| 资源、礼物、穿戴、商城 | `resource.go` + `resource_repository.go` + `transport/grpc/resource.go` | resource |
| VIP 和钱包首页查询 | `service.go` + `app_wallet_repository.go` | app wallet |
| 红包 | `red_packet.go` + `red_packet_repository.go` + `transport/grpc/red_packet.go` | red packet |
| 钱包 outbox 和投影 | `app.go` + `service.go` + `repository.go` + projection files | outbox/projection |
| 主播工资、币商库存和工资换币 | `service.go` + `repository.go` + `host_salary_settlement.go` | host salary / coin seller |
## 拆分原则
1. `wallet-service` 仍然是账务事实 owner。余额、交易、分录和 `wallet_outbox` 不拆到其他服务。
2. `wallet_outbox` 仍然只由 wallet-service 直接读取和发布。下游服务只能消费 RocketMQ 或自己的投影表,不能回扫钱包库补业务事实。
3. 第一阶段不改 protobuf、不改 gRPC 方法名、不改 `walletservice.New(repository, activity)` 装配方式。
4. MySQL 第一阶段保持一个 `mysql.Repository` 类型,方法可以拆文件,但事务 helper 仍留在同 package。
5. service 第一阶段保持一个 `wallet.Service` 门面,外部调用不变;先拆文件和接口,再考虑 usecase 子包。
6. 每一步只搬同一业务域的方法、metadata、request hash、receipt 和 outbox event避免把一个事务链路拆成多次提交。
## 第一阶段目标结构
### service 层
保留 package
```text
services/wallet-service/internal/service/wallet/
```
第一阶段目标文件:
```text
service.go # Service struct、New、依赖注入 setter、通用 guard
repository.go # Repository 总接口和嵌入式能力接口
clients.go # ActivityBadgeClient、GooglePlayClient、MifaPayClient、V5PayClient、TronUSDTClient
gift.go # DebitGift、DebitRobotGift、BatchDebitGift
balance.go # GetBalances、GetWalletOverview、GetWalletValueSummary、GetUserGiftWall、ListWalletTransactions
host_salary.go # GetActiveHostSalaryPolicy、GetHostSalaryProgress、ProcessHostSalarySettlementBatch
coin_seller.go # AdminCreditCoinSellerStock、TransferCoinFromSeller、ExchangeSalaryToCoin、TransferSalaryToCoinSeller
admin_ledger.go # AdminCreditAsset
reward.go # CreditTaskReward、CreditLuckyGiftReward、CreditWheelReward、CreditRoomTurnoverReward、CreditInviteActivityReward、CreditAgencyOpeningReward
game.go # ApplyGameCoinChange
recharge.go # ListRechargeBills、ListRechargeProducts、ListAdminRechargeProducts、Create/Update/DeleteRechargeProduct、ConfirmGooglePayment
external_recharge.go # H5 三方充值、通知、查单补偿,保留现有文件但只放外部充值
vip.go # ListVipPackages、GetMyVip、PurchaseVip、GrantVip、ListAdminVipLevels、UpdateAdminVipLevels
red_packet.go # 红包用例,保留现有文件
resource.go # 资源目录、资源组、礼物配置、发放、穿戴、商城,后续再细拆
projection.go # ProjectPendingGiftWallEvents、ProcessWalletProjectionMessage、ProjectPendingBadgeGrantEvents
```
`repository.go` 建议按 room-service 的方式拆成嵌入式能力接口:
```go
type Repository interface {
GiftLedgerStore
BalanceStore
HostSalaryStore
CoinSellerStore
AdminLedgerStore
RewardLedgerStore
GameLedgerStore
RechargeStore
ExternalRechargeStore
VIPStore
RedPacketStore
ResourceCatalogStore
ResourceGrantStore
ResourceEquipmentStore
ResourceShopStore
WalletProjectionStore
BadgeProjectionStore
}
```
接口边界建议:
| 接口 | 方法范围 |
| --- | --- |
| `GiftLedgerStore` | `DebitGift``BatchDebitGift` |
| `BalanceStore` | `GetBalances`、钱包首页、钱包流水、礼物墙 |
| `HostSalaryStore` | 工资政策、工资进度、工资结算批处理 |
| `CoinSellerStore` | 币商库存、币商转币、工资换币 |
| `AdminLedgerStore` | 后台调账 |
| `RewardLedgerStore` | 任务、幸运礼物、转盘、房间流水、邀请、开业奖励 |
| `GameLedgerStore` | 游戏扣款、返奖、退款、冲正 |
| `RechargeStore` | 充值档位、Google 支付、充值账单 |
| `ExternalRechargeStore` | H5 三方支付方式、下单、回调、查单补偿 |
| `VIPStore` | VIP 包、购买、发放、后台配置 |
| `RedPacketStore` | 红包配置、创建、领取、过期退款、后台重试 |
| `ResourceCatalogStore` | 资源、资源组、礼物配置、礼物类型 |
| `ResourceGrantStore` | 单资源发放、资源组发放、发放记录 |
| `ResourceEquipmentStore` | 用户权益、穿戴、卸下、批量读取穿戴 |
| `ResourceShopStore` | 资源商城档位、购买记录、购买 |
| `WalletProjectionStore` | 礼物墙投影和 wallet outbox message 投影入口 |
| `BadgeProjectionStore` | badge grant 投影 claim、delivered、failed |
### storage/mysql 层
保留 package
```text
services/wallet-service/internal/storage/mysql/
```
`repository.go` 第一阶段只保留:
```text
Repository struct
Open / Close / Ping
通用常量中真正跨业务域使用的部分
```
建议拆出的文件:
```text
schema.go # ensure*Schema、重复列/重复索引兼容判断
account.go # walletAccount、lockAccount、lockAccountsOrdered、applyAccountDelta
transaction.go # transactionRow、lookupTransaction、insertTransaction、transactionID、request hash 公共能力
entry.go # walletEntry、insertEntry、balanceAfterTransaction
outbox.go # WalletOutboxRecord、WalletOutboxClaimFilter、Claim/Mark/insertWalletOutbox、事件类型过滤
gift_ledger.go # DebitGift、BatchDebitGift、giftDebitBizType、背包送礼消耗
gift_pricing.go # resolveGiftPrice、resolveGiftDiamondRatio、resolveActiveGiftConfig
gift_receipt.go # giftMetadata、receiptFromGiftMetadata、gift outbox events
host_diamond.go # creditHostPeriodDiamonds、host period diamond account/entry
admin_ledger.go # AdminCreditAsset、adminCreditMetadata、adminCreditRequestHash
reward_ledger.go # 各活动奖励入账、metadata、receipt、request hash、outbox event
game_ledger.go # ApplyGameCoinChange、gameCoinMetadata、gameBizTypeAndDelta
coin_seller_ledger.go # stock、seller transfer、salary exchange、salary transfer、相关 metadata/hash/receipt
```
已存在文件的后续整理:
```text
app_wallet_repository.go # VIP + 钱包首页较大,后续拆 vip_repository.go / wallet_query_repository.go
resource_repository.go # 后续拆 resource_catalog.go / resource_group.go / resource_grant.go / user_resource.go / resource_shop.go / resource_outbox.go
third_party_payment_repository.go # 后续拆 payment_method_repository.go / external_recharge_order_repository.go
red_packet_repository.go # 已独立,但可继续拆 config/create/claim/refund/query
```
### transport/grpc 层
保留 `Server` 类型和 protobuf 注册方式不变,先按 RPC 业务域拆文件:
```text
server.go # Server struct、NewServer、通用 helper
gift.go # DebitGift、BatchDebitGift、DebitRobotGift、gift response mapper
balance.go # GetBalances、钱包首页、钱包流水、礼物墙 mapper
host_salary.go # 工资政策、工资进度、cron 结果 mapper
admin_ledger.go # AdminCreditAsset
coin_seller.go # 币商库存、转币、工资换币
reward.go # 活动奖励 RPC
game.go # ApplyGameCoinChange
recharge.go # 充值账单、Google、充值档位
external_recharge.go # H5 三方支付 RPC 和 mapper
vip.go # VIP RPC 和 mapper
resource.go # 资源 handler后续再拆 catalog/grant/shop/equipment
resource_mapper.go # resource proto/domain mapper
red_packet.go # 红包 handler
red_packet_mapper.go # 红包 proto/domain mapper
cron_server.go # cron gRPC server保留独立
```
`transport/grpc` 的拆分原则是handler 和 mapper 可以同业务域放一起,但一个文件超过 500 行时应把 mapper 单独拆出。transport 只能做 proto/domain 转换和错误透传,不要在这里补账务规则。
### app 装配层
`internal/app/app.go` 可以低风险拆成:
```text
app.go # App struct、New、Run、Close
clients.go # Activity、Google、MiFaPay、V5Pay、TronGrid 客户端装配
mq.go # RocketMQ producer/consumer 创建、start/shutdown
outbox_worker.go # wallet outbox claim/publish/mark
projection_worker.go # wallet projection consumer
external_recharge_worker.go # 外部充值查单补偿 worker
red_packet_worker.go # 红包过期退款 worker
health.go # health HTTP run/close
```
app 层不承载业务规则。worker 只负责任务循环和调用 service用例幂等必须仍在 service/storage 内保证。
### domain 层
第一阶段不拆 package只拆文件
```text
internal/domain/ledger/
ledger.go # 通用常量、AssetBalance、基础类型
gift.go # 送礼 command/receipt
host_salary.go # 工资 policy/progress/settlement
coin_seller.go # 币商库存、转币、工资换币
recharge.go # 充值档位、Google、充值账单
external_recharge.go # H5 三方订单、通知、支付方式
wallet_query.go # 钱包首页、流水、礼物墙
vip.go # VIP
reward.go # 活动奖励
game.go # 游戏账变
red_packet.go # 红包
internal/domain/resource/
resource.go # 通用 Resource
gift_config.go # GiftConfig、GiftTypeConfig
group.go # ResourceGroup
grant.go # GrantResource、ResourceGrant
entitlement.go # UserResourceEntitlement、穿戴
shop.go # ResourceShopItem、PurchaseOrder
```
领域对象拆文件的目标是让业务域可读,不要把领域对象拆成过细 package。否则 transport、service、storage 都要同时改 import收益不高。
## 第二阶段目标目录
第一阶段稳定后,再考虑 service 真正目录化。目标是 `wallet.Service` 作为门面,内部 usecase 子包只依赖 ports 和 domain
```text
services/wallet-service/internal/service/wallet/
service.go # 门面,保留现有对外方法
ports/
repository.go # repository 能力接口
clients.go # 三方 client 能力接口
clock.go # now/clock 抽象,如需要
usecase/
gift/
balance/
hostsalary/
coinseller/
adminledger/
reward/
game/
recharge/
externalrecharge/
vip/
redpacket/
resource/
projection/
```
依赖方向:
```text
transport/grpc -> service/wallet facade -> service/wallet/usecase/* -> service/wallet/ports
storage/mysql -> implements service/wallet/ports.Repository
client/* -> implements service/wallet/ports third-party clients
domain/* -> shared pure data and validation helpers
```
第二阶段不要让 `transport/grpc` 直接调用 `usecase/*`。gRPC 层直接感知所有 usecase 会把装配复杂度扩散到 transport后续新增 RPC 时更容易乱。
## 推荐实施顺序
### 1. service 低风险拆文件
先拆 `internal/service/wallet/service.go`,不改任何方法签名:
1. 新建 `repository.go`,移动 `Repository interface`,并改成嵌入式能力接口。
2. 新建 `clients.go`,移动 Activity、Google、MiFaPay、V5Pay、TronUSDT client 接口。
3. 按业务域移动 Service 方法到 `gift.go``host_salary.go``coin_seller.go``reward.go``game.go``recharge.go``vip.go``balance.go``projection.go`
4. 每移动一个业务域,运行 `go test ./services/wallet-service/internal/service/wallet`
### 2. transport 跟随拆分
`server.go` 只保留 `Server``NewServer` 和通用 helper。按 service 文件域拆 handler 文件mapper 超过 500 行时单独放 `*_mapper.go`
验证:
```bash
go test ./services/wallet-service/internal/transport/grpc
```
### 3. storage/mysql 拆核心 repository
先拆基础设施,再拆业务:
1. `schema.go`
2. `account.go`
3. `transaction.go`
4. `entry.go`
5. `outbox.go`
6. `gift_ledger.go``gift_pricing.go``gift_receipt.go``host_diamond.go`
7. `admin_ledger.go``reward_ledger.go``game_ledger.go``coin_seller_ledger.go`
每一步只搬代码,不改 SQL不改事务边界不改 outbox payload。
验证:
```bash
go test ./services/wallet-service/internal/storage/mysql
go test ./services/wallet-service/...
```
### 4. 拆 resource_repository.go
`resource_repository.go` 单独作为第二个大 storage 任务处理,避免和核心账本拆分混在一起:
```text
resource_catalog.go
resource_group.go
resource_grant.go
user_resource.go
resource_equipment.go
resource_shop.go
resource_outbox.go
```
这一步要特别保护背包送礼链路,因为 `gift_ledger.go` 会调用用户权益消耗相关 helper。
### 5. app worker 拆分
最后拆 `internal/app/app.go`。原因是 app 层只是装配和 worker 循环,等 service/storage 边界清晰后再拆更不容易来回调整。
验证:
```bash
go test ./services/wallet-service/...
```
## 每阶段完成标准
| 阶段 | 完成标准 |
| --- | --- |
| service 拆分 | `service.go` 只保留 Service 结构、构造和依赖 setter`Repository` 变成嵌入式能力接口service 单测通过 |
| transport 拆分 | `server.go` 不再承载大批业务 handlermapper 按业务域归位transport 单测通过 |
| storage 核心拆分 | `repository.go` 小于 250 行;核心账本 helper 有明确文件wallet storage 单测通过 |
| resource storage 拆分 | `resource_repository.go` 小于 500 行;资源目录、发放、穿戴、商城各自可定位;资源 storage 单测通过 |
| app 拆分 | `app.go` 只负责生命周期worker 文件按 MQ/outbox/projection/reconcile/expiry 分开wallet-service 全包测试通过 |
最终验证:
```bash
go test ./services/wallet-service/...
make test
```
如果 `make test` 暴露其他服务或既有配置失败,需要单独记录为基线问题,不能把它当成钱包拆分失败,也不能因为无关失败回滚已验证的 wallet-service 拆分。
## 风险点
| 风险 | 规避方式 |
| --- | --- |
| 事务 helper 被跨 package 导出 | 第一阶段 storage 保持同 package只拆文件 |
| request hash 或 biz type 和原逻辑不一致 | 每个业务域移动时把 command、metadata、request hash、receipt、outbox event 一起移动 |
| service 接口拆完但测试 mock 更复杂 | 总 `Repository` 保持嵌入式组合,测试按用例只依赖小接口 |
| transport 拆分后 mapper 找不到 | handler 和 mapper 同域移动,通用 mapper 留在 `server.go``mapper.go` |
| app worker 拆早导致来回改 | app 层最后拆,等 service 方法和 storage outbox 边界稳定 |
| 背包送礼跨 resource 和 gift ledger | `consumeGiftEntitlementForSend` 先留在 gift ledger 同 package第二阶段再决定是否抽资源权益端口 |
## 不建议做的事
1. 不要第一步创建多个 MySQL repository 实例。账务事务必须共享同一个连接池和同一个事务提交边界。
2. 不要让 `room-service``activity-service``notice-service` 直连钱包库查询交易明细。
3. 不要把 outbox 发布状态交给下游服务更新。wallet outbox 位点仍归 wallet-service。
4. 不要在 transport 层补业务幂等、扣费判断或支付状态机。
5. 不要为了拆目录改变 protobuf 字段或 gRPC 方法名;这是结构整理,不是协议重构。

View File

@ -2,7 +2,7 @@
本文只定义后台/Admin 侧实现政策配置、工资周期、工资单、审核、钱包入账、调整单、后台审计和运营干预。App 侧的主播申请、Agency 成员、BD 邀请和用户可见查询见 [Host Agency BD App Architecture](./主播公会BD架构.md)。 本文只定义后台/Admin 侧实现政策配置、工资周期、工资单、审核、钱包入账、调整单、后台审计和运营干预。App 侧的主播申请、Agency 成员、BD 邀请和用户可见查询见 [Host Agency BD App Architecture](./主播公会BD架构.md)。
这里的工资统一指发放到主播侧 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 的业务奖励。提现审核和人工打款仍由 `wallet-service` 拥有Admin 只负责把审核通过的工资单入账到工资余额,不直接处理线下打款。 这里的工资统一指发放到主播侧 `USD_BALANCE` 的业务奖励。提现审核和人工打款仍由 `wallet-service` 拥有Admin 只负责把审核通过的工资单入账到工资余额,不直接处理线下打款。
本文不新增独立 `host-service` 微服务。host、Agency、BD、政策和工资后台能力属于 user-services代码落在当前仓库的 `services/user-service` host domain。 本文不新增独立 `host-service` 微服务。host、Agency、BD、政策和工资后台能力属于 user-services代码落在当前仓库的 `services/user-service` host domain。
@ -11,7 +11,7 @@
- 后台可以配置 host、agency、bd 三类工资政策,并按区域生效。 - 后台可以配置 host、agency、bd 三类工资政策,并按区域生效。
- 后台可以选择日结、周结、半月结、月结,不把结算周期写死在代码里。 - 后台可以选择日结、周结、半月结、月结,不把结算周期写死在代码里。
- 后台可以生成工资周期,查看每个 host/Agency/BD/BD Leader 的明细来源。 - 后台可以生成工资周期,查看每个 host/Agency/BD/BD Leader 的明细来源。
- 后台审批后,`user-service` host domain 以幂等方式调用 `wallet-service``HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 入账。 - 后台审批后,`user-service` host domain 以幂等方式调用 `wallet-service``USD_BALANCE` 入账。
- 后台可以对迟到事件、人工修正、争议处理创建调整单,不能直接修改已入账工资项。 - 后台可以对迟到事件、人工修正、争议处理创建调整单,不能直接修改已入账工资项。
- 后台可以创建或停用 BD Leader、BD、Agency也可以关闭 Agency后台可以查看和新增币商 `coin_seller`;所有动作必须有审计记录。 - 后台可以创建或停用 BD Leader、BD、Agency也可以关闭 Agency后台可以查看和新增币商 `coin_seller`;所有动作必须有审计记录。
@ -49,7 +49,7 @@ graph LR
| `hyapp-admin-server` | Admin HTTP 入口、后台鉴权、request envelope、后台操作审计使用独立 `hyapp_admin` 库 | | `hyapp-admin-server` | Admin HTTP 入口、后台鉴权、request envelope、后台操作审计使用独立 `hyapp_admin` 库 |
| `user-service` | 用户、区域、账号状态查询host/Agency/BD/coin_seller 政策、周期、工资单、关系管理、统计聚合、幂等结果、领域 outboxApp/客户端不能提交区域,后台关系命令的 `region_id` 只能来自 `hyapp-admin-server` 鉴权审计入口 | | `user-service` | 用户、区域、账号状态查询host/Agency/BD/coin_seller 政策、周期、工资单、关系管理、统计聚合、幂等结果、领域 outboxApp/客户端不能提交区域,后台关系命令的 `region_id` 只能来自 `hyapp-admin-server` 鉴权审计入口 |
| `room-service` | 只产出上麦和礼物事件,不接收后台工资命令 | | `room-service` | 只产出上麦和礼物事件,不接收后台工资命令 |
| `wallet-service` | `COIN``COIN_SELLER_COIN``HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 入账、提现冻结、提现审核状态和钱包流水 | | `wallet-service` | `COIN``COIN_SELLER_COIN``USD_BALANCE` 入账、提现冻结、提现审核状态和钱包流水 |
## Admin Permission Model ## Admin Permission Model
@ -79,8 +79,6 @@ graph LR
当前 `user_id` 是内部稳定主键,后台仍必须在所有 host/Agency/BD 查询、幂等和审计 detail 中带上 `app_code`。如果某些现有表还使用全局 `PRIMARY KEY(user_id)``PRIMARY KEY(command_id)``PRIMARY KEY(event_id)`,这只能说明当前实现暂时要求这些 ID 全局唯一;不能据此省略 `app_code`,也不能在多 App 写入前声称同一个 `command_id` 可以跨 App 复用。 当前 `user_id` 是内部稳定主键,后台仍必须在所有 host/Agency/BD 查询、幂等和审计 detail 中带上 `app_code`。如果某些现有表还使用全局 `PRIMARY KEY(user_id)``PRIMARY KEY(command_id)``PRIMARY KEY(event_id)`,这只能说明当前实现暂时要求这些 ID 全局唯一;不能据此省略 `app_code`,也不能在多 App 写入前声称同一个 `command_id` 可以跨 App 复用。
经理中心所有目标用户操作都必须从已验证的 App token 取得 `app_code`不接受客户端自报租户。App 差异统一由 user-service 的 `manager_operations` 范围策略解析Huwaa 的基础范围是 `global`;其他 App 的基础范围是 `country`,后台开启“跨区经理”后有效范围扩展为同一有效 `region_id`仍禁止跨区域。头像框、座驾、徽章、VIP、升级等级、BD Leader/Admin/Superadmin、封禁和转移国家的搜索与写入口共用同一个范围匹配器不能各自判断具体 App 或让 `scope=all` 绕过数据范围;各动作原有功能权限仍独立校验。
```sql ```sql
admin_operation_logs( admin_operation_logs(
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
@ -190,7 +188,7 @@ flowchart LR
E --> F E --> F
F --> G["approve"] F --> G["approve"]
G --> H["wallet CreditSalaryBalance"] G --> H["wallet CreditSalaryBalance"]
H --> I["identity salary wallet"] H --> I["USD_BALANCE"]
``` ```
```sql ```sql
@ -355,7 +353,7 @@ Leader 直接拥有 Agency 下线时,推荐快照同时设置 `bd_user_id = le
1. Admin 审批 cycle。 1. Admin 审批 cycle。
2. `user-service` host domain 查询所有 `approved` salary items。 2. `user-service` host domain 查询所有 `approved` salary items。
3. 对每个 item 调用 `wallet-service CreditSalaryBalance`,幂等键为 `salary:{salary_item_id}` 3. 对每个 item 调用 `wallet-service CreditSalaryBalance`,幂等键为 `salary:{salary_item_id}`
4. `wallet-service` credit `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 并返回 `wallet_transaction_id` 4. `wallet-service` credit `USD_BALANCE` 并返回 `wallet_transaction_id`
5. `user-service` host domain 保存 `wallet_transaction_id`,把 item 标记为 `posted` 5. `user-service` host domain 保存 `wallet_transaction_id`,把 item 标记为 `posted`
6. 如果中途失败,只重试未 posted item幂等键保持不变。 6. 如果中途失败,只重试未 posted item幂等键保持不变。
@ -434,9 +432,8 @@ service UserHostAdminService {
} }
``` ```
`CreateBDLeaderRequest` 不接受 `region_id`。user-service 锁定目标用户行,读取其当前 active `users.region_id`,再使用该区域创建或恢复 BD Leader 事实;不会因创建角色而迁移用户国家/区域。同一个 `command_id` 重试只返回已创建的 BD Leader 事实。 `CreateBDLeaderRequest` 必须携带 `region_id`。user-service 校验该区域 active锁定目标用户行必要时更新 `users.region_id`,再创建 `bd_profiles`;同一个 `command_id` 重试只返回已创建的 BD Leader 事实,不重复迁移。
这属于允许改变用户区域的后台关系管理路径之一;完整来源见 [User Region Change Sources](./用户区域与国家开发.md#user-region-change-sources)。
Agency 邀请、Host 主动申请、BD 邀请的区域例外也由 `user-service``app_code` 执行:仅 Huwaa 在邀请/申请创建及接受/审核阶段允许跨区,其他 App 保留区域校验。Agency 主动邀请 Host 不在例外内。Coin Seller 列表、子币商申请/审批和转账授权对所有 App 仍为同区域。
当前项目仍在开发阶段proto 可以按当前事实调整;修改 `api/proto` 后必须运行 `make proto` 并提交生成文件。 当前项目仍在开发阶段proto 可以按当前事实调整;修改 `api/proto` 后必须运行 `make proto` 并提交生成文件。
@ -448,7 +445,7 @@ Agency 邀请、Host 主动申请、BD 邀请的区域例外也由 `user-service
- Cycle calculation 使用 deterministic item key。重跑同一个 `draft` cycle 可以替换 draft items 或返回现有结果,不能重复插入。 - Cycle calculation 使用 deterministic item key。重跑同一个 `draft` cycle 可以替换 draft items 或返回现有结果,不能重复插入。
- Cycle approve 必须锁 cycle 当前状态为 `draft` - Cycle approve 必须锁 cycle 当前状态为 `draft`
- Posting 使用 `salary_item_id` 作为钱包幂等键。`post_failed` 重试只处理未 posted item。 - Posting 使用 `salary_item_id` 作为钱包幂等键。`post_failed` 重试只处理未 posted item。
- Relationship admin action 要锁目标用户的 `users``host_profiles``agencies``bd_profiles`,避免和 App 邀请接受并发造成身份冲突。创建 BD Leader 使用目标用户当前 `users.region_id`,不在角色事务内改写用户区域 - Relationship admin action 要锁目标用户的 `users``host_profiles``agencies``bd_profiles`,避免和 App 邀请接受并发造成身份冲突。后台创建 BD Leader 需要在同一事务内更新 `users.region_id``bd_profiles.region_id`
## Event And Outbox ## Event And Outbox
@ -491,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 | | 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 | | 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 | | Wallet posting partially fails | Retry unposted items with same idempotency key |
| Admin accidentally posts twice | Wallet idempotency must prevent duplicate `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` credit | | Admin accidentally posts twice | Wallet idempotency must prevent duplicate `USD_BALANCE` credit |
| Agency closed mid-cycle | Work before close remains attributable; future App joins/search stop immediately | | 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 | | 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 | | Negative adjustment exceeds balance | Move to manual handling; do not force wallet negative unless wallet explicitly supports debt |
@ -510,21 +507,17 @@ 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 | | 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 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 | | BD under Leader owns Agency | Host salary contributes to BD and leader according to snapshot |
| Cycle posted twice | Wallet idempotency prevents duplicate `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` credit | | Cycle posted twice | Wallet idempotency prevents duplicate `USD_BALANCE` credit |
| Posting fails after partial success | Retry posts only unposted items | | Posting fails after partial success | Retry posts only unposted items |
| Posted item needs correction | Adjustment item created; original item remains immutable | | Posted item needs correction | Adjustment item created; original item remains immutable |
| Admin closes Agency | Agency hidden from App search; existing facts remain auditable | | Admin closes Agency | Agency hidden from App search; existing facts remain auditable |
| Huwaa Manager creates a BD Leader in another country/region | Creation succeeds; target keeps its current region |
| Other App Manager creates a BD Leader in another country | Request is rejected before `CreateBDLeader` is called |
| Huwaa Coin Seller creates or approves a cross-region sub-seller relation | Request is rejected with the existing region boundary |
## Critical Rules ## Critical Rules
- Admin owns policies and salary cycles; App owns user-initiated relationship flows. - Admin owns policies and salary cycles; App owns user-initiated relationship flows.
- Wallet is the only owner of `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` and withdrawal state. - Wallet is the only owner of `USD_BALANCE` and withdrawal state.
- Posted salary items are immutable. - Posted salary items are immutable.
- BD/BD Leader base is downstream host salary only. - BD/BD Leader base is downstream host salary only.
- Leader direct Agency salary source must be de-duplicated. - Leader direct Agency salary source must be de-duplicated.
- Every admin mutation must be audited and idempotent. - Every admin mutation must be audited and idempotent.
- Huwaa 跨区例外适用于 Agency 邀请、Host 主动申请、BD 邀请和全部经理中心目标用户操作Coin Seller 仍为同区域。
- Redis cannot be the source of truth for policies, salary, relationships, or audit. - Redis cannot be the source of truth for policies, salary, relationships, or audit.

View File

@ -142,7 +142,7 @@
- Agency salary基于下属 host salary item 或明确配置的 agency 指标。 - Agency salary基于下属 host salary item 或明确配置的 agency 指标。
- BD/BD Leader salary基于 downstream host salary sum且按 source host salary item 去重。 - BD/BD Leader salary基于 downstream host salary sum且按 source host salary item 去重。
- 后台审核工资周期。 - 后台审核工资周期。
- 调用 `wallet-service``HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 入账,幂等键为 `salary:{salary_item_id}` - 调用 `wallet-service``USD_BALANCE` 入账,幂等键为 `salary:{salary_item_id}`
- 支持调整单:迟到事件、人工修正、争议处理都走补差或冲正。 - 支持调整单:迟到事件、人工修正、争议处理都走补差或冲正。
- Salary posted 和 adjustment posted 事件接入 App message inbox通知接收人查看收益明细通知失败不影响钱包入账事实。 - Salary posted 和 adjustment posted 事件接入 App message inbox通知接收人查看收益明细通知失败不影响钱包入账事实。
@ -182,7 +182,7 @@
| M3 | Phase 3 | 用户申请、审核、踢出、重新申请、邀请链路完整跑通 | | M3 | Phase 3 | 用户申请、审核、踢出、重新申请、邀请链路完整跑通 |
| M3.5 | Phase 3.5 | 申请、邀请、后台创建关系结果能进入 App 系统消息 | | M3.5 | Phase 3.5 | 申请、邀请、后台创建关系结果能进入 App 系统消息 |
| M4 | Phase 4 | 有效上麦和礼物统计按关系快照落库 | | M4 | Phase 4 | 有效上麦和礼物统计按关系快照落库 |
| M5 | Phase 5 | 后台生成工资单,审批后入账 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` | | M5 | Phase 5 | 后台生成工资单,审批后入账 `USD_BALANCE` |
| M6 | Phase 6 | 失败补偿、导出、重建和监控可用 | | M6 | Phase 6 | 失败补偿、导出、重建和监控可用 |
## Stop Conditions ## Stop Conditions
@ -191,7 +191,7 @@
- Phase 2 没有完成前,不做 App Agency 搜索上线,因为没有可信组织种子数据。 - Phase 2 没有完成前,不做 App Agency 搜索上线,因为没有可信组织种子数据。
- Phase 3 没有完成前,不做工资发放,因为 host/Agency/BD 归属还不稳定。 - Phase 3 没有完成前,不做工资发放,因为 host/Agency/BD 归属还不稳定。
- Phase 4 没有完成前,不做真实工资入账,因为缺少可审计统计输入。 - Phase 4 没有完成前,不做真实工资入账,因为缺少可审计统计输入。
- Phase 5 没有完成审批和幂等 posting 前,不允许写 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`。 - Phase 5 没有完成审批和幂等 posting 前,不允许写 `USD_BALANCE`。
## Verification Commands ## Verification Commands

View File

@ -9,14 +9,14 @@
## Goals ## Goals
- 用户申请加入 Agency 并通过后成为主播Agency 踢出用户只解除 Agency 归属,不取消主播身份。 - 用户申请加入 Agency 并通过后成为主播Agency 踢出用户只解除 Agency 归属,不取消主播身份。
- Huwaa 用户可搜索并申请加入本 App 内的跨区域 active Agency其他 App 保留现有国家/区域限制 - 用户只能搜索并申请加入自己所属区域的 active Agency
- Agency 归属于 BDBD 归属于 BD Leader。BD Leader 也是 BD只是具备邀请 BD 的能力。 - Agency 归属于 BDBD 归属于 BD Leader。BD Leader 也是 BD只是具备邀请 BD 的能力。
- BD 可以邀请用户成为 AgencyBD Leader 可以邀请用户成为 BD也可以直接邀请用户成为 Agency。 - BD 可以邀请用户成为 AgencyBD Leader 可以邀请用户成为 BD也可以直接邀请用户成为 Agency。
- 用户被邀请成为 Agency 时自动成为主播;但已是主播的用户不能再成为 Agency。 - 用户被邀请成为 Agency 时自动成为主播;但已是主播的用户不能再成为 Agency。
- BD 默认不是主播BD 可以加入别的 Agency 成为主播,也可以在未成为主播时邀请自己成为自己的 Agency。 - BD 默认不是主播BD 可以加入别的 Agency 成为主播,也可以在未成为主播时邀请自己成为自己的 Agency。
- `coin_seller` 是独立币商身份,可以和 Agency、BD、BD Leader 身份并存;币商拥有专用金币账户,可以给玩家转普通金币。 - `coin_seller` 是独立币商身份,可以和 Agency、BD、BD Leader 身份并存;币商拥有专用金币账户,可以给玩家转普通金币。
- App 侧可以展示 host 统计、Agency 成员、申请、邀请和后台已经生成的收益结果。 - App 侧可以展示 host 统计、Agency 成员、申请、邀请和后台已经生成的收益结果。
- App 侧不能创建政策、计算工资周期、审核工资单或直接给 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` 入账。 - App 侧不能创建政策、计算工资周期、审核工资单或直接给 `USD_BALANCE` 入账。
## Non-Goals ## Non-Goals
@ -67,7 +67,7 @@ graph LR
| `gateway-service` | HTTP 入口、鉴权、request envelope、调用内部 gRPC | | `gateway-service` | HTTP 入口、鉴权、request envelope、调用内部 gRPC |
| `user-service` | 用户主数据、国家、区域、账号状态host 身份、Agency、BD、coin_seller 层级、申请邀请、关系快照、App 查询读模型 | | `user-service` | 用户主数据、国家、区域、账号状态host 身份、Agency、BD、coin_seller 层级、申请邀请、关系快照、App 查询读模型 |
| `room-service` | 房间事件:上麦、确认发流、下麦、送礼、离房 | | `room-service` | 房间事件:上麦、确认发流、下麦、送礼、离房 |
| `wallet-service` | `COIN``COIN_SELLER_COIN``HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`、收益余额、提现冻结/审核/出账 | | `wallet-service` | `COIN``COIN_SELLER_COIN``USD_BALANCE`、收益余额、提现冻结/审核/出账 |
`user-service` host domain 读取 `room-service``wallet-service` 事件但不反向修改房间状态。App 查询只读取当前身份、关系、申请、邀请、统计和后台已生成的收益结果;结算写入链路见 Admin 文档。 `user-service` host domain 读取 `room-service``wallet-service` 事件但不反向修改房间状态。App 查询只读取当前身份、关系、申请、邀请、统计和后台已生成的收益结果;结算写入链路见 Admin 文档。
@ -279,20 +279,22 @@ agency_memberships(
## Region Rules ## Region Rules
用户所属区域来自 `user-service.users.region_id`,不能由客户端提交。`app_code` 来自 gateway 已验证的 token 并写入 `RequestMeta`;客户端不能通过请求体或请求头伪造 Huwaa 例外。 用户所属区域来自 `user-service.users.region_id`,不能由客户端提交。
国家/区域后台管理边界在 `hyapp-admin-server`:创建国家、启用/禁用国家、创建区域、启用/禁用区域不属于 App 侧 user-service RPC。App 侧只消费 user-service 已计算好的 `region_id` 和区域投影。 国家/区域后台管理边界在 `hyapp-admin-server`:创建国家、启用/禁用国家、创建区域、启用/禁用区域不属于 App 侧 user-service RPC。App 侧只消费 user-service 已计算好的 `region_id` 和区域投影。
用户区域允许变化的完整来源以 [User Region Change Sources](./用户区域与国家开发.md#user-region-change-sources) 为准Host/Agency/BD 关系创建后保留目标用户当前区域Huwaa 跨区组织关系不会顺带迁移 `users.region_id` 后台创建 BD Leader 是关系管理命令,不是客户端区域提交:`hyapp-admin-server` 负责权限和审计,`user-service` 在同一事务里把目标用户的 `users.region_id` 更新为后台选择的区域,并用同一个区域创建 `bd_profiles`
用户区域允许变化的完整来源以 [User Region Change Sources](./用户区域与国家开发.md#user-region-change-sources) 为准Host/Agency/BD 关系只消费当前 `users.region_id` 和各关系表里的区域快照。
| Action | Region Rule | | Action | Region Rule |
| --- | --- | | --- | --- |
| 经理创建 BD Leader | Huwaa 经理可搜索并创建其他国家/区域的 BD Leader其他 App 保持经理与目标用户同国家。创建后 BD Leader 使用目标用户已有 `users.region_id`,不迁移用户国家/区域 | | 后台创建 BD Leader | 后台必须选择 active 区域;目标用户当前区域可以不同,创建成功后目标用户 `users.region_id` 和 BD Leader `bd_profiles.region_id` 都变为所选区域 |
| 搜索 Agency | Huwaa 返回本 App 内所有 active/join_enabled Agency其他 App 保留按当前用户国家过滤 | | 搜索 Agency | 只返回 `agency.region_id == user.region_id` 且 active/join_enabled |
| 申请加入 Agency | Huwaa 申请创建和 Agency 审核通过均允许跨区;其他 App 要求申请人与 Agency 同区域 | | 申请加入 Agency | 申请人 `region_id` 必须等于 Agency `region_id` |
| Agency 主动邀请 Host | 所有 App 仍要求目标用户与 Agency 同区域Huwaa 例外只适用于 Host 主动申请 | | BD 邀请 Agency | BD 和被邀请人必须在同一区域,除非后台显式跨区授权 |
| BD/Leader 邀请 Agency | Huwaa 的邀请创建和接受均允许跨区;其他 App 要求邀请人与目标用户同区域 | | Leader 邀请 BD | Leader 和被邀请人必须在同一区域,除非后台显式跨区授权 |
| Leader 邀请 BD | Huwaa 的邀请创建和接受均允许跨区;其他 App 要求 Leader 与目标用户同区域 | | Leader 邀请 Agency | Leader 和被邀请人必须在同一区域,除非后台显式跨区授权 |
| Coin Seller | 所有 App 继续按区域返回 active 币商;子币商申请创建、审批建立关系和转账前授权均要求父子币商同区域 | | 用户改国家导致区域变化 | 不自动迁移 Agency/BD 归属;新申请按新区域限制,历史统计按事件快照归属 |
| 用户改国家导致区域变化 | 不自动迁移 Agency/BD 归属;新申请或邀请按 `app_code` 对应规则处理,历史统计按事件快照归属 |
如果后续业务需要跨区 Agency 或跨区 BD必须增加显式 `region_scope` 和后台审批,不能绕过区域规则直接查全局 Agency。
## Application And Invitation Flows ## Application And Invitation Flows
@ -307,16 +309,16 @@ sequenceDiagram
C->>G: GET /agencies/search C->>G: GET /agencies/search
G->>U: SearchAgenciesForUser(user_id) G->>U: SearchAgenciesForUser(user_id)
U->>H: load app_code policy, user country/region and active agencies U->>H: load user region and active agencies
H-->>U: Huwaa all active agencies; other App same-country agencies H-->>U: active agencies in region
U-->>G: app-scoped agency list U-->>G: active agencies in region
G-->>C: agency list G-->>C: agency list
C->>G: POST /host/applications C->>G: POST /host/applications
G->>U: ApplyToAgency(user_id, agency_id) G->>U: ApplyToAgency(user_id, agency_id)
U->>H: load user region and agency U->>H: load user region and agency
H->>H: lock user host profile and agency H->>H: lock user host profile and agency
H->>H: enforce app-scoped region policy and create pending application H->>H: create pending application
H-->>U: application H-->>U: application
U-->>G: application U-->>G: application
``` ```
@ -351,7 +353,7 @@ Kick must not delete `host_profiles` and must not delete relationship history. T
BDInviteAgency(target_user_id) BDInviteAgency(target_user_id)
-> target must not be active host -> target must not be active host
-> target must not own active agency -> target must not own active agency
-> Huwaa allows cross-region; other App target region must match BD region -> target region must match BD region
-> create role_invitation(type=agency, inviter_bd_user_id) -> create role_invitation(type=agency, inviter_bd_user_id)
-> target accepts -> target accepts
-> create agency owned by target -> create agency owned by target
@ -372,7 +374,7 @@ BD can invite self only when:
LeaderInviteBD(target_user_id) LeaderInviteBD(target_user_id)
-> inviter must be active bd_leader -> inviter must be active bd_leader
-> target must not be active BD -> target must not be active BD
-> Huwaa allows cross-region; other App target region must match leader region -> target region must match leader region
-> create role_invitation(type=bd) -> create role_invitation(type=bd)
-> target accepts -> target accepts
-> create bd_profiles(role=bd, parent_leader_user_id=leader) -> create bd_profiles(role=bd, parent_leader_user_id=leader)
@ -608,7 +610,7 @@ Host/Agency/BD 的 App 可见通知统一进入 App `消息` tab 的 `system`
| BD joins another Agency | BD profile remains active; host membership is separate | | BD joins another Agency | BD profile remains active; host membership is separate |
| BD wants own Agency after joining another Agency | Must first leave or be removed from active host membership; then self-invite can create Agency | | BD wants own Agency after joining another Agency | Must first leave or be removed from active host membership; then self-invite can create Agency |
| Agency owner wants to leave own Agency | Not allowed through normal leave; Agency close or transfer is backend ability | | Agency owner wants to leave own Agency | Not allowed through normal leave; Agency close or transfer is backend ability |
| User region changes | Existing relationships remain; new search/application/invitation follows the current App policy; historical stats use event snapshots | | User region changes | Existing relationships remain; new search/application uses new region; historical stats use event snapshots |
| App earning summary and wallet balance differ | Wallet balance is authority for withdrawable amount; earning summary is business explanation | | App earning summary and wallet balance differ | Wallet balance is authority for withdrawable amount; earning summary is business explanation |
| Client retries application or invitation action | Same `command_id` returns existing result; different `command_id` must hit uniqueness guard | | Client retries application or invitation action | Same `command_id` returns existing result; different `command_id` must hit uniqueness guard |
@ -620,8 +622,7 @@ Host/Agency/BD 的 App 可见通知统一进入 App `消息` tab 的 `system`
| Scenario | Expected | | Scenario | Expected |
| --- | --- | | --- | --- |
| Huwaa user searches Agency | All active joinable agencies in Huwaa are returned, including other regions | | User searches Agency | Only same-region active joinable agencies returned |
| Other App user searches Agency | Existing country/region restriction remains in force |
| User applies and Agency approves | User becomes active host and active Agency member | | User applies and Agency approves | User becomes active host and active Agency member |
| Agency kicks host | Membership ends, host profile remains active | | Agency kicks host | Membership ends, host profile remains active |
| Kicked host reapplies to another Agency | New active membership created | | Kicked host reapplies to another Agency | New active membership created |
@ -629,9 +630,6 @@ Host/Agency/BD 的 App 可见通知统一进入 App `消息` tab 的 `system`
| BD invites self as Agency while not host | Agency created, BD also becomes host and Agency owner | | BD invites self as Agency while not host | Agency created, BD also becomes host and Agency owner |
| BD joins another Agency | BD remains BD and becomes host under that Agency | | BD joins another Agency | BD remains BD and becomes host under that Agency |
| BD Leader invites BD | Target gets BD profile, not host profile | | BD Leader invites BD | Target gets BD profile, not host profile |
| Huwaa cross-region Agency/BD invitation is accepted | Target keeps its own region and the invited relationship is created |
| Huwaa manager creates cross-region BD Leader | Target keeps its own region and receives active BD Leader identity |
| Huwaa user lists or invites Coin Seller | Only same-region sellers or sub-seller targets are allowed |
| Host works across two Agencies in one day | Stats split by relation snapshot | | Host works across two Agencies in one day | Stats split by relation snapshot |
| App reads earning summary before backend settlement | Shows pending/unsettled state, not withdrawable money | | App reads earning summary before backend settlement | Shows pending/unsettled state, not withdrawable money |
| Same application command retried | Returns the original pending application | | Same application command retried | Returns the original pending application |
@ -642,7 +640,7 @@ Host/Agency/BD 的 App 可见通知统一进入 App `消息` tab 的 `system`
- Agency owner is a host, but ordinary active host cannot become Agency. - Agency owner is a host, but ordinary active host cannot become Agency.
- BD and host identities can coexist; BD role alone does not imply host. - BD and host identities can coexist; BD role alone does not imply host.
- BD Leader is a BD with extra invitation rights. - BD Leader is a BD with extra invitation rights.
- Region policy is selected by authenticated `app_code`: Huwaa only exempts Agency invitation, Host application, BD invitation and manager-created BD Leader; other App paths and all Coin Seller relations retain server-side region restrictions. - Region constraints are enforced by server-side `region_id`.
- App cannot configure policies, approve salary, post wallet balance, or mutate withdrawals. - App cannot configure policies, approve salary, post wallet balance, or mutate withdrawals.
- Wallet is the only owner of salary balance and withdrawal state. - Wallet is the only owner of salary balance and withdrawal state.
- All relationship changes must be auditable and idempotent. - All relationship changes must be auditable and idempotent.

View File

@ -1,81 +0,0 @@
# 全站机器人产品文档
## 目标
全站机器人用于自研游戏自动补位。后台批量创建机器人资料号后,游戏服务可以在玩家等待超时后从机器人池选择一个可用机器人参与对局。
## 后台入口
- 菜单位置:游戏管理 / 全站机器人
- 菜单编码:`game-robots`
- 页面地址:`/games/robots`
- 权限:查看使用 `game:view`,创建使用 `game:create`,启停使用 `game:update`,删除使用 `game:delete``game:update`
## 机器人规则
- 机器人以 App 用户资料形式保存,有 `user_id`、短 ID、昵称、头像、国家、语言等基础资料。
- 机器人注册来源固定为 `game_robot`
- 机器人不是自然新增用户,不进入新增用户统计、注册 cohort、留存统计。
- 机器人不能通过 App 登录接口登录。
- 机器人创建时不生成 access token、refresh token也不写登录成功审计。
- 机器人不导入腾讯 IM 登录账号;它只用于游戏资料展示和游戏服务补位。
- 删除机器人只会从全站机器人池移除,用户资料和历史对局记录保留。
## 创建流程
1. 运营在后台选择创建数量、国家、性别和语言元数据。
2. admin-server 从 likei 线上 `user_base_info` 随机抽取正常用户的 `user_nickname``user_avatar`
3. admin-server 调 user-service 创建 `source=game_robot` 的资料号,昵称和头像直接使用 likei 资料。
4. user-service 写入用户资料、短 ID 和密码身份,但不创建登录 session。
5. admin-server 调 game-service 把这些 user_id 注册到机器人池。
6. game-service 后续只从启用状态的机器人中选择补位。
## 统计口径
- 实时统计:`source=game_robot` 不写 `UserRegistered` outbox。
- 补数统计:统计补数按 `users.created_at_ms` 扫描时排除 `source=game_robot`
- 后台创建 100 个全站机器人,不会增加“新增用户数”,也不会进入留存 cohort。
## 登录限制
- 密码登录:即使知道短 ID 和密码,也会被 user-service 拦截。
- 三方登录:机器人没有三方身份绑定,不能通过三方接口登录成机器人。
- refresh token新机器人不会生成 refresh token历史上如果存在机器人 session续期时也会被用户登录判断拦截。
- access token 重签:会复用同一登录判断,机器人不能续签。
## 后台接口
### 列表
- 地址:`GET /admin/game/robots`
- 参数:`gameId``status``pageSize``cursor`
- 返回值:机器人列表、下一页游标、服务器时间
- 相关 IM
### 批量创建
- 地址:`POST /admin/game/robots/generate`
- 参数:`count`(默认 20最多 1000`nicknameLanguage`(只作为账号语言元数据)、`gender``country`
- 返回值:创建数量、机器人列表
- 相关 IM机器人不导入 IM 登录账号
### 启停
- 地址:`PATCH /admin/game/robots/{user_id}/status`
- 参数:`status`,可传 `active``disabled`;可选 `gameId`
- 返回值:机器人当前状态
- 相关 IM
### 删除
- 地址:`DELETE /admin/game/robots/{user_id}`
- 参数:可选 `gameId`
- 返回值:`deleted`
- 相关 IM
## 游戏补位规则
- 自研游戏配置里开启机器人补位后玩家等待超过配置时间game-service 会从当前 App 下所有启用且未加入本局的机器人里完全随机抽取一个加入对局。
- 机器人被选中后会记录最近使用时间和累计使用次数;这些字段只用于后台观察命中分布,不影响下一次随机抽取。
- 机器人对局资金由游戏奖池结算,不从机器人钱包扣款。
- 机器人禁用后不会再被新对局选中。

View File

@ -10,9 +10,7 @@
| package_name | platform | app_code | status | | package_name | platform | app_code | status |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `com.org.laluparty` | 任意或空 | `lalu` | `active` | | `com.org.laluparty` | `android` | `lalu` | `active` |
| `com.app.huwaa` | 任意或空 | `huwaa` | `active` |
| `com.chat.fami` | 任意或空 | `fami` | `active` |
新增 App 时,只新增 `apps` 注册表记录和对应 App 的基础配置/种子数据,不新建一套服务,也不复制业务代码。 新增 App 时,只新增 `apps` 注册表记录和对应 App 的基础配置/种子数据,不新建一套服务,也不复制业务代码。

View File

@ -18,7 +18,7 @@
| `room-service` | stale presence cleanup | `services/room-service/internal/room/service/presence.go` | 留在 `room-service` | | `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` | 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 outbox relay to direct publishers / RocketMQ | `services/room-service/internal/room/service/outbox_worker.go` | 留在 `room-service` 或后续拆 `room-worker`,不进通用 cron |
| `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 命令链路 | | `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 命令链路 |
| `user-service` | region rebuild task | `UserCronService.ProcessRegionRebuildBatch` | 已迁移到 cron-service 调度,旧内置 ticker 已删除 | | `user-service` | region rebuild task | `UserCronService.ProcessRegionRebuildBatch` | 已迁移到 cron-service 调度,旧内置 ticker 已删除 |
| `user-service` | login IP risk job | `UserCronService.ProcessLoginIPRiskBatch` | 已迁移到 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 或事件总线 | | `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 执行命令链路 | | Room Cell stale presence cleanup | 依赖本节点已装载 Room Cell 和 Redis lease必须由 `room-service` owner 执行命令链路 |
| Mic publish timeout cleanup | 必须对当前 Room Cell 状态做二次校验并执行 `MicDown`,不能让 cron 直接清麦 | | Mic publish timeout cleanup | 必须对当前 Room Cell 状态做二次校验并执行 `MicDown`,不能让 cron 直接清麦 |
| room outbox relay | 这是 room-service 的事件投递补偿,不是通用定时任务;需要和 direct publisher、RocketMQ 发布、outbox 状态语义绑定 | | room outbox relay | 这是 room-service 的事件投递补偿,不是通用定时任务;需要和 direct publisher、RocketMQ 发布、outbox 状态语义绑定 |
| room rocket launch timer | 火箭状态属于 Room Cell延迟消息只负责唤醒发射必须由 room-service 重新校验 `rocket_id/launch_at_ms/UTC reset` 后执行命令 | | room treasure open timer | 宝箱状态属于 Room Cell延迟消息只负责唤醒开箱必须由 room-service 重新校验 `box_id/open_at_ms/UTC reset` 后执行命令 |
| wallet ledger transaction | 金币、钻石、积分、余额的加减和冻结必须在 `wallet-service` 事务内完成 | | wallet ledger transaction | 金币、钻石、积分、余额的加减和冻结必须在 `wallet-service` 事务内完成 |
| wallet outbox publish | 属于 wallet-service outbox relay 或 MQ 投递,不应该由通用 cron 直接扫表改状态 | | wallet outbox publish | 属于 wallet-service outbox relay 或 MQ 投递,不应该由通用 cron 直接扫表改状态 |
| invite recharge outbox consumer | 当前使用 in-memory cursor 扫 wallet outbox迁移前必须先做 durable offset 或正式事件总线 | | invite recharge outbox consumer | 当前使用 in-memory cursor 扫 wallet outbox迁移前必须先做 durable offset 或正式事件总线 |
@ -152,21 +152,21 @@ tasks:
timeout: "3s" timeout: "3s"
lock_ttl: "30s" lock_ttl: "30s"
batch_size: 100 batch_size: 100
app_codes: ["lalu","fami","huwaa"] app_codes: ["lalu"]
user_region_rebuild: user_region_rebuild:
enabled: true enabled: true
interval: "5s" interval: "5s"
timeout: "5s" timeout: "5s"
lock_ttl: "30s" lock_ttl: "30s"
batch_size: 500 batch_size: 500
app_codes: ["lalu","fami","huwaa"] app_codes: ["lalu"]
message_fanout: message_fanout:
enabled: true enabled: true
interval: "1s" interval: "1s"
timeout: "10s" timeout: "10s"
lock_ttl: "30s" lock_ttl: "30s"
batch_size: 500 batch_size: 500
app_codes: ["lalu","fami","huwaa"] app_codes: ["lalu"]
mic_open_session_compensation: mic_open_session_compensation:
enabled: true enabled: true
interval: "60s" interval: "60s"
@ -175,7 +175,7 @@ tasks:
batch_size: 100 batch_size: 100
pending_publish_max_age: "2m" pending_publish_max_age: "2m"
publishing_session_max_age: "12h" publishing_session_max_age: "12h"
app_codes: ["lalu","fami","huwaa"] app_codes: ["lalu"]
``` ```
## Cron Database ## Cron Database
@ -269,7 +269,7 @@ cron-service 可以直接开启多个实例;调度层通过 `cron_task_leases`
- activity room outbox consumer。 - activity room outbox consumer。
- notice room outbox consumer。 - notice room outbox consumer。
- room IM bridge MQ consumer。 - room IM bridge MQ consumer。
- room rocket delayed launch consumer。 - room treasure delayed open consumer。
这些需要的是事件消费基础设施RocketMQ、Redis Stream、或 MySQL durable offset。cron-service 不能用“每隔几秒从头扫 outbox”的方式替代事件消费者。即使某些消费者内部有低频扫描补偿也必须留在业务 owner service 中,因为它们需要理解 outbox 状态、消费幂等和领域状态校验。 这些需要的是事件消费基础设施RocketMQ、Redis Stream、或 MySQL durable offset。cron-service 不能用“每隔几秒从头扫 outbox”的方式替代事件消费者。即使某些消费者内部有低频扫描补偿也必须留在业务 owner service 中,因为它们需要理解 outbox 状态、消费幂等和领域状态校验。

View File

@ -14,7 +14,7 @@
- `USDT进货`:平台收到币商线下 USDT 后,后台给币商发放 `COIN_SELLER_COIN` 库存;这笔记录计入币商进货/充值统计。 - `USDT进货`:平台收到币商线下 USDT 后,后台给币商发放 `COIN_SELLER_COIN` 库存;这笔记录计入币商进货/充值统计。
- `金币补偿`:平台因为异常、活动、人工修正等原因给币商补库存;没有充值金额,不计入币商进货/充值统计。 - `金币补偿`:平台因为异常、活动、人工修正等原因给币商补库存;没有充值金额,不计入币商进货/充值统计。
- 两种类型都不能给币商发放 `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD`,也不能直接影响玩家普通 `COIN` - 两种类型都不能给币商发放 `USD_BALANCE`,也不能直接影响玩家普通 `COIN`
## 服务边界 ## 服务边界

View File

@ -1,307 +0,0 @@
# 幸运礼物 · 必爆隐藏奖池产品方案6.25 版)
## 文档信息
| 项 | 内容 |
| --- | --- |
| 版本 | v1.0 |
| 日期 | 2026-06-25 |
| 范围 | 语音房幸运礼物的新一版产品与算法方案 |
| 状态 | 产品方案,不能直接等同于当前代码已上线能力 |
目标是在语音房送礼场景里提供一个极简、高方差、长期自平衡的幸运礼物系统。用户侧只感知“送礼 -> 开奖 -> 结果表现”,后台通过隐藏奖池完成持续喂池和必然爆池。平台先抽水,剩余资金进入小奖反馈层和隐藏奖池层,所有开奖必须可审计、可重放。
## 1. 项目背景与目标
### 1.1 背景
用户发送幸运礼物后,需要马上获得强反馈。传统固定倍数或简单随机容易出现长时间无大奖、低波动、低传播的问题,用户会认为玩法“干等”或“不刺激”。新方案用三层反馈叠加:
- 高频小奖让每次送礼都有反馈。
- 稀有大奖制造黑天鹅惊喜。
- 必爆隐藏奖池保证在可控贡献量内一定出现大额 payout。
### 1.2 核心目标
| 目标 | 说明 |
| --- | --- |
| 用户侧 | 每次送礼都有独立开奖体验,高频小奖、稀有大奖和必爆大奖叠加。 |
| 平台侧 | 每笔先抽水,剩余奖池自平衡,不允许平台倒贴。 |
| 长期循环 | 通过“持续喂池 -> 必然爆池 -> 留底滚动”形成可持续戏剧性循环。 |
| 认知成本 | 用户端不展示复杂池子、爆点和概率,只触发送礼并接收结果。 |
| 审计公平 | 每次开奖记录 before/after、随机因子、trigger 状态和结果,支持事后重放。 |
### 1.3 成功标准
- 单次 100 金礼物的玩家侧期望返还约为 75-78 金。
- 必爆机制保证在可控贡献量内一定出现大额 payout。
- 大奖释放后隐藏奖池仍保留 52%-68% 底池,继续滚雪球。
- 内部账号、机器人和主播测试流量不能真实消耗 J Fund。
## 2. 核心设计理念
### 2.1 三层爽感
| 层级 | 机制 | 产品意义 |
| --- | --- | --- |
| 高频层 S Fund | 超过 58% 概率获得正返还。 | 维持连续发送行为,避免每次都空。 |
| 稀有大奖层 J Fund | 1% 概率按隐藏池百分比释放。 | 形成不可预测的大额惊喜。 |
| 必然爆发层 Must-Hit | 隐藏爆点强制释放 32%-48% 奖池。 | 消除长期干等,完成喂池到爆池的闭环。 |
### 2.2 隐藏与独立
- 公共奖池状态、爆点和当前周期不对客户端展示。
- S Fund 与 J Fund 独立 roll小奖层 Miss 不影响大奖层触发。
- 每次送礼都先形成完整开奖事实,再由表现层展示结果。
### 2.3 平台零风险
- 每笔送礼先扣平台抽水。
- J Fund payout 只能按比例释放,并受留底、单次上限和风控上限约束。
- 奖池余额不足时不能透支,必须降级为可支付结果或 Miss。
## 3. 资金流与数学模型
每送 100 金单位礼物,资金拆分如下:
| 去向 | 金额 | 占比 |
| --- | ---: | ---: |
| 平台抽水 | 22 金 | 22% |
| S Fund 小奖反馈 | 28 金 | 28% |
| J Fund 隐藏奖池 | 50 金 | 50% |
长期口径:
- 平台固定收益22 金。
- S Fund 期望返还:约 22.84 金,利用率约 81.6%。
- J Fund payout 长期受控在总贡献 50% 以内,依赖分段释放比例、必爆上限和留底。
- 玩家侧综合期望返还约为 75.84 金,符合社交礼物玩法的健康区间。
## 4. 开奖算法
### 4.1 S Fund 小奖反馈层
S Fund 使用 Alias Method 做 O(1) 采样。金额使用连续均匀分布,避免固定值被用户感知为机械规律。
| Tier | 概率 | 返还金额 | 期望贡献 |
| --- | ---: | ---: | ---: |
| Miss | 42% | 0 | 0 |
| 小回血 | 32% | Uniform[8, 18] | 4.16 |
| 中回血 | 16% | Uniform[25, 45] | 5.60 |
| 好奖 | 7% | Uniform[60, 120] | 6.30 |
| 大回血 | 3% | Uniform[150, 280] | 6.45 |
期望值:
```text
E[S] = 0.32 * 13 + 0.16 * 35 + 0.07 * 90 + 0.03 * 215
= 22.51
```
说明:原始方案中的 `22.84` 与表格均值直接计算不完全一致。按当前表格区间中点计算是 `22.51`。如果产品口径必须锁定 `22.84`,需要微调某一档金额区间或概率后再进入实现。
### 4.2 J Fund 基础隐藏奖池层
每次送礼原子增加 50 金到 J Fund。J Fund 与 S Fund 独立开奖。
基础触发:
- 触发概率1.0%。
- 获奖金额:`win_J = J * r`
- 释放后:`J_new = J - win_J`,不清空奖池,保留自然底池。
释放比例按当前 J Fund 规模分段:
| J Fund 当前规模 | r 范围 |
| --- | --- |
| J < 200000 | Uniform[0.05, 0.10] |
| 200000 <= J < 500000 | Uniform[0.08, 0.15] |
| J >= 500000 | Uniform[0.12, 0.22] |
### 4.3 Must-Hit 必爆机制
周期初始化:
```text
T = J_current + Uniform[280000, 720000]
```
每次送礼按顺序执行:
1. `J = J + 50`
2. 如果 `J >= T` 且当前周期未触发,强制触发超级爆池。
3. 超级爆池金额为 `win_J = J * r`,其中 `r = Uniform[0.32, 0.48]`
4. 释放后 `J_new = J - win_J`,通常保留 52%-68% 底池。
5. 新周期爆点为 `T_new = J_new + Uniform[300000, 800000]`
参数依据:
- 28 万到 72 万爆点贡献区间用于覆盖热门房间约 40-120 分钟的心理窗口。
- 32%-48% 强制释放比例保证单次大奖冲击力,同时保留底池继续滚动。
## 5. 完整开奖流程
```text
on_lucky_gift(user_id, amount = 100):
platform_rake += 22
s_fund += 28
j_before = j_fund
j_fund += 50
s_win = roll_s_fund()
credit_user(user_id, s_win)
j_win = 0
forced = false
if j_fund >= current_trigger and current_cycle_not_triggered:
r = uniform(0.32, 0.48)
j_win = j_fund * r
j_win = apply_jackpot_caps(j_win, j_fund)
j_fund -= j_win
current_trigger = j_fund + uniform(300000, 800000)
forced = true
else if random() < 0.01:
r = get_r_by_pool_size(j_fund)
j_win = j_fund * r
j_win = apply_jackpot_caps(j_win, j_fund)
j_fund -= j_win
if j_win > 0:
credit_user(user_id, j_win)
write_audit_log(...)
```
必须注意:真实实现不能在抽奖事务里直接调用钱包入账。应先写开奖事实和 outbox再由 worker 幂等发奖,避免数据库事务和外部 RPC 混在一起。
## 6. 安全、公平与风控规则
| 规则 | 要求 |
| --- | --- |
| 单次上限 | `win_J = min(win_J, j_fund * 0.48, 500000)`。 |
| 个人累计上限 | 可选日/周 jackpot 累计封顶,防止洗钱和异常套利。 |
| 内部账号隔离 | internal、bot、host 测试账号只能走 S Fund 和模拟 J Fund不真实消耗 J Fund。 |
| 随机性 | 使用密码学安全 RNG每次记录 seed 或可验证随机材料,支持重放。 |
| 并发安全 | J Fund 更新必须在数据库事务内完成,使用行锁、版本号或分布式锁防止并发透支。 |
| 初始种子 | 新周期可注入 8 万到 15 万启动 J Fund计入营销成本或后期回收。 |
| 审计完整性 | 每笔记录 gift_id、user_id、room_id、j_before、j_after、s_win、j_win、forced_hit、random_seed、trigger、ts。 |
## 7. 参数配置与动态调优
### 7.1 推荐 v1.0 参数
| 参数 | 推荐值 | 调优方向 |
| --- | --- | --- |
| 平台抽水 | 22% | 不可低于 18%。 |
| S Fund 贡献 | 28% | 维持 E[S] 约 22.5-23.5。 |
| J Fund 贡献 | 50% | 核心滚雪球资金来源。 |
| 基础池奖概率 | 1.0% | 可在 0.6%-1.5% 调整。 |
| 必爆释放比例 | Uniform[0.32, 0.48] | 越高越激进,越低越耐久。 |
| 初始爆点范围 | Uniform[280000, 720000] | 礼物量越大,范围可越大。 |
| 新周期爆点范围 | Uniform[300000, 800000] | 大奖后重建周期,避免连爆过密。 |
### 7.2 动态微调
当 J Fund 过大时,可以轻微抬高基础大奖概率:
```text
p_base = 0.01 + min(0.004, (J - 500000) / 75000000)
```
动态调参必须写入规则版本或配置变更记录,不能只改运行内存,否则无法审计和复盘。
## 8. 预期效果与 KPI
| 维度 | 预期 |
| --- | --- |
| 用户行为 | 高频小奖维持发送频率,稀有大奖和必爆大奖制造传播峰值。 |
| 经济指标 | 平台抽水稳定J Fund 不透支,大额释放后仍有底池恢复能力。 |
| 风险指标 | 单用户单次/累计上限、内部账号隔离、完整审计降低套利和质疑。 |
| 长期循环 | 数学上保证喂池后一定爆发,形成持续正反馈循环。 |
## 9. 技术实施建议
### 9.1 后端落地顺序
1. 先实现资金原子更新、J Fund 周期状态、开奖事实和完整审计日志。
2. 再接入 outbox worker由钱包服务幂等入账。
3. 最后接入房间 IM、区域播报、后台配置、后台查询和监控告警。
### 9.2 服务边界
| 服务 | 职责 |
| --- | --- |
| room-service | 只负责送礼主链路、房间状态、Room Cell 和房间 outbox不拥有奖池。 |
| activity-service | 拥有幸运礼物规则、S Fund、J Fund、开奖事实、风控和发奖 outbox。 |
| wallet-service | 负责送礼扣费和幸运礼物返奖入账,保证幂等账务。 |
| statistics-service | 只消费事件写聚合表,不回查 owner service 明细表。 |
| server/admin | 提供参数配置、规则版本、开奖明细和统计查询入口。 |
### 9.3 必须支持热更新
以下参数需要后台配置并落规则版本:
- 平台抽水比例。
- S Fund 分布、概率和金额区间。
- J Fund 贡献比例。
- 基础大奖概率和分段释放比例。
- Must-Hit 爆点范围和释放比例。
- 单次、单用户、设备、房间、主播风控上限。
- 内部账号和机器人隔离规则。
### 9.4 监控告警
- J Fund 余额异常波动。
- 实际 win rate 与配置概率长期偏离。
- 单用户、单设备、单房间大额中奖集中。
- outbox 发奖积压或失败。
- 钱包返奖幂等冲突。
- 必爆周期长时间未重置或重复触发。
### 9.5 合规
上线前需要法务确认虚拟礼物概率玩法边界。客户端可以公开必要规则说明,但不能暴露隐藏奖池余额、当前爆点和触发进度。
## 10. 审计日志字段
每笔开奖至少记录:
| 字段 | 说明 |
| --- | --- |
| gift_id | 本次送礼或抽奖唯一 ID。 |
| command_id | 幂等命令 ID重试必须复用。 |
| user_id | 送礼用户。 |
| room_id | 房间 ID。 |
| target_user_id | 收礼用户。 |
| amount | 本次礼物真实扣费金额。 |
| s_before / s_after | S Fund 变更快照。 |
| j_before / j_after | J Fund 变更快照。 |
| s_win | S Fund 中奖金额。 |
| j_win | J Fund 中奖金额。 |
| forced_hit | 是否 Must-Hit 强制触发。 |
| trigger_before / trigger_after | 周期爆点变更。 |
| random_seed | 可验证随机材料。 |
| random_rolls | 本次所有随机 roll 明细。 |
| status | pending、granted、failed。 |
| created_at_ms | UTC epoch milliseconds。 |
## 11. 当前实现差距
本方案与当前仓库中的幸运礼物 v2 配置化 RTP 奖档模型不是同一个口径。进入技术实现前,需要明确是否替换现有 v2 主链路,或作为新 `pool_id` / 新规则版本并行上线。
必须确认的差距:
- 当前实现按 `pool_id`、规则版本、阶段奖档和 RTP 窗口抽奖;新方案按 S Fund、J Fund 和 Must-Hit 周期抽奖。
- 当前实现已有三层基础奖池和风控计数;新方案需要新增隐藏 J Fund 周期状态和强制爆点。
- 当前实现 worker 已支持异步发奖;新方案必须复用这个账务边界,不能把伪代码中的 `credit_user` 做成事务内 RPC。
- 当前统计只能消费事实事件写聚合表;新方案的 S/J payout 需要补充事件字段或独立统计读模型。
## 12. 版本迭代计划
| 版本 | 内容 |
| --- | --- |
| v1.1 | 增加多档幸运礼物,高价礼物贡献更高 J Fund 比例。 |
| v1.2 | 按房间活跃度自适应 Must-Hit 爆点范围。 |
| v1.3 | 可选展示个人累计 jackpot 贡献,默认仍保持极简模式。 |

View File

@ -1,230 +0,0 @@
# 幸运礼物外接接口对接文档
> 文档版本2026-07-15
> 接入方式服务端对服务端Server-to-Server
> 金额单位:对接 App 的金币最小整数单位
## 1. 接口用途
外部 App 在自己的账务系统完成幸运礼物扣费后,调用本接口获取一次最终抽奖结果。
平台负责:
- 按 `app_code` 隔离规则、奖池、RTP 和抽奖流水。
- 按 `app_code + request_id` 保证抽奖幂等。
- 返回最终 `reward_amount` 和倍率。
对接 App 负责:
- 调用前完成用户扣费。
- 成功收到结果后,在自己的账务系统中向用户发放 `reward_amount`
- 对超时或网络失败使用原 `request_id` 重试,不得重新生成业务请求 ID。
HyApp 不会操作外部 App 的用户钱包。`reward_status=granted` 表示抽奖结果已经生成并交给调用方,不表示奖励已在外部 App 入账。
## 2. 接口地址
| 类型 | 地址 |
|---|---|
| Base URL | `https://lucky-api.global-interaction.com` |
| 送礼接口 | `POST https://lucky-api.global-interaction.com/api/v1/lucky-gifts/send` |
| 就绪检查 | `GET https://lucky-api.global-interaction.com/healthz/ready` |
就绪检查:
```text
GET https://lucky-api.global-interaction.com/healthz/ready
```
2026-07-15 实测就绪检查返回 HTTP 200送礼请求可到达幸运礼物网关。
## 3. 调用前置条件
1. 向平台申请唯一 `app_code`,并加入幸运礼物网关白名单。
2. 确认该 App 的幸运礼物规则已启用。
3. 对接 App 必须先扣费,再调用抽奖接口。
4. 本接口只允许对接 App 服务端调用,禁止将请求能力下发到 App 或 Web 客户端。
当前版本的 HTTP 入口使用 `app_code` 白名单,尚未启用 Authorization Token 或请求签名,因此必须仅由已开通的对接 App 服务端调用。
## 4. 发送幸运礼物
### 4.1 请求
```http
POST /api/v1/lucky-gifts/send HTTP/1.1
Host: lucky-api.global-interaction.com
Content-Type: application/json
X-Request-Id: trace-20260715-000001
```
`X-Request-Id` 为可选的链路追踪 ID。如果不传平台会自动生成。它只用于日志排查不是抽奖幂等键。
### 4.2 请求体
```json
{
"app_code": "partner_app",
"request_id": "gift-order-20260715-000001",
"external_user_id": "user-10001",
"gift_count": 3,
"unit_amount": 100,
"currency": "COIN",
"metadata": {
"external_order_id": "order-1000001",
"gift_id": "lucky-rose",
"room_id": "room-20001"
}
}
```
### 4.3 字段说明
| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---:|---|---|
| `app_code` | string | 是 | 去除首尾空格后 132 字符 | 平台分配的 App 编码;按小写存储和校验。 |
| `request_id` | string | 是 | 去除首尾空格后 1128 字符 | 对接 App 生成的业务幂等 ID建议直接使用送礼订单号或 UUID。 |
| `external_user_id` | string | 是 | 去除首尾空格后 1128 字符 | 用户在对接 App 中的稳定唯一 ID。 |
| `gift_count` | int64 | 是 | `> 0` | 本次已扣费的礼物数量。 |
| `unit_amount` | int64 | 是 | `> 0` | 单个礼物的金币整数价格。 |
| `currency` | string | 否 | 只支持 `COIN` | 缺省或空字符串时按 `COIN` 处理。 |
| `metadata` | object | 否 | JSON object | 对接方的展示或排查元数据;不参与金额、规则或幂等判定。 |
服务端按下式计算本次金额:
```text
total_amount = gift_count * unit_amount
```
请求体不得传入 `total_amount``pool_id``paid_at_ms`。奖池由平台规则选择,扣费完成时间由网关按 UTC 服务端时间写入。接口会拒绝所有未定义的顶层字段。
一个请求只返回一个聚合抽奖结果;`gift_count` 不会产生多个独立响应。
### 4.4 成功响应
HTTP Status`200 OK`
```json
{
"code": 0,
"message": "ok",
"request_id": "trace-20260715-000001",
"data": {
"draw_id": "external_lucky_draw_xxxxxxxxxx",
"request_id": "gift-order-20260715-000001",
"app_code": "partner_app",
"external_user_id": "user-10001",
"gift_count": 3,
"unit_amount": 100,
"total_amount": 300,
"reward_amount": 600,
"multiplier_ppm": 2000000,
"reward_status": "granted",
"rule_version": 1,
"created_at_ms": 1784073600000
}
}
```
### 4.5 响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | int | 业务结果码;`0` 表示请求成功。 |
| `message` | string | 结果说明。 |
| 外层 `request_id` | string | HTTP 链路追踪 ID来自 `X-Request-Id` 或由平台生成。 |
| `data.draw_id` | string | 平台生成的唯一抽奖 ID对账时使用。 |
| `data.request_id` | string | 请求体中的业务幂等 ID。 |
| `data.app_code` | string | 归一化后的 App 编码。 |
| `data.external_user_id` | string | 对接 App 的用户 ID。 |
| `data.gift_count` | int64 | 礼物数量。 |
| `data.unit_amount` | int64 | 单件礼物金币价格。 |
| `data.total_amount` | int64 | 服务端计算的已扣费总额。 |
| `data.reward_amount` | int64 | 对接 App 应向用户发放的金币整数金额;以该字段为最终结算依据。 |
| `data.multiplier_ppm` | int64 | 倍率 ppm`1000000` 表示 1x`2000000` 表示 2x。 |
| `data.reward_status` | string | 当前为 `granted`,表示抽奖已完成且结果已交给对接 App 处理。 |
| `data.rule_version` | int64 | 本次抽奖命中的规则版本。 |
| `data.created_at_ms` | int64 | 抽奖结果创建时间Unix epoch millisecondsUTC。 |
未中奖时 `reward_amount``multiplier_ppm``0`。当前 JSON 序列化会省略数值为 `0` 的结果字段,对接方必须将缺失的 `reward_amount``multiplier_ppm``0` 处理,不得将字段缺失视为请求失败。
## 5. 幂等与重试
幂等键固定为:
```text
app_code + request_id
```
- 首次请求成功后,平台保存抽奖结果。
- 使用相同 `app_code + request_id` 重试时,直接返回首次结果,不会重新抽奖。
- 同一 `request_id` 不得对应不同用户、数量或金额。平台在幂等命中时会返回首次结果,不会用新参数覆盖。
- 超时、连接中断或 HTTP 5xx 时,使用原请求体和原 `request_id` 重试。
- 客户端超时建议设置为大于网关内部 3 秒处理超时,并对重试使用有上限的退避。
如果多次重试后仍无法确认结果,请保留 `app_code``request_id`、外层 `request_id` 和扣费流水,由平台根据抽奖记录对账。
## 6. 错误响应
### 6.1 统一格式
```json
{
"code": 40000,
"message": "request_id is required",
"request_id": "trace-20260715-000001"
}
```
### 6.2 错误码
| HTTP Status | `code` | 场景 | 对接方动作 |
|---:|---:|---|---|
| 400 | 40000 | JSON 无效、出现未定义字段、必填字段缺失、金额非正数或币种不支持 | 修正请求,不要原样重试。 |
| 403 | 40300 | `app_code` 未加入当前环境白名单 | 联系平台开通或检查环境。 |
| 404 | 无统一业务码 | 请求路径或 HTTP 方法错误 | 检查 Base URL、路径和请求方法。 |
| 500 | 50000 | 抽奖服务超时或内部处理失败 | 保持原 `request_id` 重试;不得重新扣费。 |
业务成功必须同时满足 HTTP Status 为 `200` 且响应 `code``0`
## 7. cURL 示例
```bash
curl --request POST \
'https://lucky-api.global-interaction.com/api/v1/lucky-gifts/send' \
--header 'Content-Type: application/json' \
--header 'X-Request-Id: trace-20260715-000001' \
--data '{
"app_code": "partner_app",
"request_id": "gift-order-20260715-000001",
"external_user_id": "user-10001",
"gift_count": 3,
"unit_amount": 100,
"currency": "COIN",
"metadata": {
"external_order_id": "order-1000001",
"gift_id": "lucky-rose",
"room_id": "room-20001"
}
}'
```
## 8. 标准处理流程
1. 对接 App 生成全局唯一的 `request_id`
2. 对接 App 在自有账务系统扣除 `gift_count * unit_amount`
3. 对接 App 调用幸运礼物接口。
4. 平台按 `app_code + request_id` 创建或返回已有抽奖结果。
5. 对接 App 在响应成功后按 `reward_amount` 入账,并保存 `draw_id`
6. 任何未知结果都使用原 `request_id` 重试;禁止重复扣费或重新生成抽奖业务号。
## 9. 联调检查清单
- [ ] `app_code` 已加入幸运礼物网关白名单。
- [ ] 规则已启用,并确认金币单位一致。
- [ ] 正常送礼可获得 HTTP 200 / `code=0`
- [ ] 重复发送相同 `request_id` 可获得相同 `draw_id` 和结果。
- [ ] 模拟超时后只重试抽奖请求,不重复扣费。
- [ ] 未中奖响应中缺失的数值字段可正确按 `0` 处理。
- [ ] 奖励按 `reward_amount` 在外部 App 入账,并持久化 `draw_id`
- [ ] 上线前完成小额真实扣费、抽奖、返奖和幂等重试验收。

View File

@ -1,6 +1,6 @@
# 幸运礼物实现文档 # 幸运礼物实现文档
本文档描述当前代码里的幸运礼物服务端实现。旧的产品方案、重构分步文档和早期 activity-service 开发文档已经下线;服务端实现以后续本文档为准Flutter App 对接以 [幸运礼物 Flutter 对接](flutter对接/幸运礼物Flutter对接.md) 为准 本文档描述当前代码里的幸运礼物实现。旧的产品方案、重构分步文档、Flutter 单独对接文档和早期 activity-service 开发文档已经下线;后续以本文档为唯一实现说明
当前实现不兼容旧玩法假设。幸运礼物以 `pool_id` 为规则维度,以 `activity-service` 为规则、抽奖、奖池、RTP 窗口、风控、抽奖事实和 outbox owner扣费和返奖入账属于 `wallet-service`;房间状态、热度、礼物榜和房间 outbox 属于 `room-service` 当前实现不兼容旧玩法假设。幸运礼物以 `pool_id` 为规则维度,以 `activity-service` 为规则、抽奖、奖池、RTP 窗口、风控、抽奖事实和 outbox owner扣费和返奖入账属于 `wallet-service`;房间状态、热度、礼物榜和房间 outbox 属于 `room-service`
@ -8,12 +8,12 @@
| 功能点 | 当前规则 | 代码函数实现 | 验收 | | 功能点 | 当前规则 | 代码函数实现 | 验收 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 服务边界 | `activity-service` 只做幸运礼物规则和开奖结果,不改 Room Cell不直接改钱包表;返奖必须通过 wallet-service gRPC | `services/activity-service/internal/service/luckygift.Service.Draw``services/activity-service/internal/service/luckygift.Service.processDrawOutbox``services/room-service/internal/room/service.Service.SendGift` | 已验收:抽奖在扣费后执行,正常返奖走同步 wallet gRPC 快路径outbox 只做补偿和表现 | | 服务边界 | `activity-service` 只做幸运礼物规则和开奖结果,不改 Room Cell不直接改钱包余额 | `services/activity-service/internal/service/luckygift.Service.Draw``services/activity-service/internal/service/luckygift.Service.processDrawOutbox``services/room-service/internal/room/service.Service.SendGift` | 已验收:抽奖在扣费后执行,返奖通过 wallet gRPC房间表现通过 IM/outbox |
| 规则维度 | 线上规则按 `pool_id` 读取最新版本,不按真实 `gift_id` 读取旧规则 | `Repository.GetLuckyGiftRuleConfig``Repository.getLuckyGiftRuleConfig``luckyPoolID` | 已验收:代码读取 `lucky_gift_rule_versions``lucky_gift_stage_tiers` | | 规则维度 | 线上规则按 `pool_id` 读取最新版本,不按真实 `gift_id` 读取旧规则 | `Repository.GetLuckyGiftRuleConfig``Repository.getLuckyGiftRuleConfig``luckyPoolID` | 已验收:代码读取 `lucky_gift_rule_versions``lucky_gift_stage_tiers` |
| 返奖率控制 | 后台发布时校验阶段期望 RTP 落在 `target_rtp_ppm ± control_band_ppm`,运行时不按 RTP 缺口改概率 | `validateRuleConfig``validateRuleStageExpectedRTP``selectLuckyCandidate` | 已验收:发布校验函数存在,候选随机只过滤不可支付档 | | 返奖率控制 | 后台发布时校验阶段期望 RTP 落在 `target_rtp_ppm ± control_band_ppm`,运行时不按 RTP 缺口改概率 | `validateRuleConfig``validateRuleStageExpectedRTP``selectLuckyCandidate` | 已验收:发布校验函数存在,候选随机只过滤不可支付档 |
| 用户体验 | 用户阶段按累计金币流水折算等价抽数,避免不同价格礼物用真实抽数误判阶段 | `luckyEquivalentDraws``luckyExperiencePool``getLuckyUserStateForUpdate` | 已验收:状态表维护 `cumulative_wager_coins``equivalent_draws``loss_streak` | | 用户体验 | 用户阶段按累计金币流水折算等价抽数,避免不同价格礼物用真实抽数误判阶段 | `luckyEquivalentDraws``luckyExperiencePool``getLuckyUserStateForUpdate` | 已验收:状态表维护 `cumulative_wager_coins``equivalent_draws``loss_streak` |
| 批量抽奖 | `gift_count=N` 表示抽 N 次;每一抽按单份金币计算奖励,响应返回总中奖倍数和总奖励 | `executeLuckyGiftDrawBatch``luckyDrawUnitSpend``stageLuckyBatchDraw``luckyAggregateDrawResults` | 已验收:单测覆盖 10 抽返回总倍数 11x 的聚合逻辑 | | 批量抽奖 | `gift_count=N` 表示抽 N 次;每一抽按单份金币计算奖励,响应返回总中奖倍数和总奖励 | `executeLuckyGiftDrawBatch``luckyDrawUnitSpend``stageLuckyBatchDraw``luckyAggregateDrawResults` | 已验收:单测覆盖 10 抽返回总倍数 11x 的聚合逻辑 |
| 聚合发放 | 批量抽奖可以有 N 条抽奖明细,但正常路径只同步给钱包加币 1 次outbox 保留 1 条聚合事件用于补偿和表现 | `insertLuckyAggregateDrawOutbox``creditLuckyGiftRewardFastPath`、`decodeDrawOutboxPayload`、`MarkLuckyGiftDrawsGranted` | 已验收:批量只按聚合 draw 发放worker 遇到已 granted 会跳过钱包 | | 聚合发放 | 批量抽奖可以有 N 条抽奖明细,但只写 1 条 `LuckyGiftDrawn` outbox钱包只加币 1 次 | `insertLuckyAggregateDrawOutbox``decodeDrawOutboxPayload`、`MarkLuckyGiftDrawsGranted` | 已验收:worker 单测覆盖 `draw_ids` 批量 granted |
## 二、配置模型 ## 二、配置模型
@ -22,7 +22,7 @@
| 功能点 | 字段或 RPC | 代码函数实现 | 验收 | | 功能点 | 字段或 RPC | 代码函数实现 | 验收 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| App/房间抽奖元信息 | `LuckyGiftMeta`,包含 `command_id``pool_id``gift_count``coin_spent``paid_at_ms``visible_region_id` | `api/proto/activity/v1/activity.proto` `LuckyGiftMeta``LuckyGiftServer.ExecuteLuckyGiftDraw` | 已验收:房间送礼只把 wallet 已结算 `coin_spent` 传给 activity | | App/房间抽奖元信息 | `LuckyGiftMeta`,包含 `command_id``pool_id``gift_count``coin_spent``paid_at_ms``visible_region_id` | `api/proto/activity/v1/activity.proto` `LuckyGiftMeta``LuckyGiftServer.ExecuteLuckyGiftDraw` | 已验收:房间送礼只把 wallet 已结算 `coin_spent` 传给 activity |
| 抽奖结果 | `LuckyGiftDrawResult`,包含 `multiplier_ppm``effective_reward_coins``reward_status``wallet_transaction_id``coin_balance_after`RTP 窗口和阶段字段 | `luckyDrawResultToProto``luckyGiftResultFromProto` | 已验收room 返回 `LuckyGift` 字段给送礼用户,中奖同步成功时可直接刷新余额 | | 抽奖结果 | `LuckyGiftDrawResult`,包含 `multiplier_ppm``effective_reward_coins``reward_status`、RTP 窗口和阶段字段 | `luckyDrawResultToProto``luckyGiftResultFromProto` | 已验收room 返回 `LuckyGift` 字段给送礼用户 |
| 后台规则 | `LuckyGiftRuleConfig``LuckyGiftRuleStage``LuckyGiftRuleTier` | `luckyRuleConfigFromProto``luckyRuleConfigToProto``configToProto``configFromProto` | 已验收:后台百分比和倍率在 handler 内转成 ppm | | 后台规则 | `LuckyGiftRuleConfig``LuckyGiftRuleStage``LuckyGiftRuleTier` | `luckyRuleConfigFromProto``luckyRuleConfigToProto``configToProto``configFromProto` | 已验收:后台百分比和倍率在 handler 内转成 ppm |
| 后台审计 | `ListLuckyGiftDraws``GetLuckyGiftDrawSummary` | `AdminLuckyGiftServer.ListLuckyGiftDraws``AdminLuckyGiftServer.GetLuckyGiftDrawSummary` | 已验收:按 `pool_id/gift_id/user_id/room_id/status` 查询 | | 后台审计 | `ListLuckyGiftDraws``GetLuckyGiftDrawSummary` | `AdminLuckyGiftServer.ListLuckyGiftDraws``AdminLuckyGiftServer.GetLuckyGiftDrawSummary` | 已验收:按 `pool_id/gift_id/user_id/room_id/status` 查询 |
@ -69,7 +69,7 @@
| 先扣费 | 房间链路先调用 wallet 扣费,成功后才调用 activity 抽奖 | `Service.SendGift``wallet.DebitGift` 调用段 | 已验收:抽奖使用 `billing.GetCoinSpent()`,不信任客户端价格 | | 先扣费 | 房间链路先调用 wallet 扣费,成功后才调用 activity 抽奖 | `Service.SendGift``wallet.DebitGift` 调用段 | 已验收:抽奖使用 `billing.GetCoinSpent()`,不信任客户端价格 |
| 抽奖命令 | 使用房间命令 ID 作为幂等根 ID | `Service.SendGift``LuckyGiftServer.ExecuteLuckyGiftDraw``Service.Draw` | 已验收:重复命令由抽奖记录幂等返回 | | 抽奖命令 | 使用房间命令 ID 作为幂等根 ID | `Service.SendGift``LuckyGiftServer.ExecuteLuckyGiftDraw``Service.Draw` | 已验收:重复命令由抽奖记录幂等返回 |
| 设备和主播 scope | 设备优先取 session_id主播优先取房主 ID | `luckyGiftDeviceID``luckyGiftAnchorID` | 已验收scope 不为空,风控计数可落表 | | 设备和主播 scope | 设备优先取 session_id主播优先取房主 ID | `luckyGiftDeviceID``luckyGiftAnchorID` | 已验收scope 不为空,风控计数可落表 |
| 同步响应 | 当前送礼用户立即拿到抽奖结果;中奖快路径成功时同时拿到返奖交易 ID 和账后余额 | `creditLuckyGiftRewardFastPath`、`luckyGiftResultFromProto`、`roomv1.SendGiftResponse.LuckyGift` | 已验收room response 带 `lucky_gift.wallet_transaction_id` 和 `lucky_gift.coin_balance_after` | | 同步响应 | 当前送礼用户立即拿到抽奖结果 | `luckyGiftResultFromProto`、`roomv1.SendGiftResponse.LuckyGift` | 已验收room response 带 `lucky_gift` |
| 房间事实 | 送礼本身仍写 Room Cell、热度、榜单和 room outbox | `Service.SendGift``outbox.Build` | 已验收:幸运礼物不替代普通送礼事实 | | 房间事实 | 送礼本身仍写 Room Cell、热度、榜单和 room outbox | `Service.SendGift``outbox.Build` | 已验收:幸运礼物不替代普通送礼事实 |
## 四、抽奖事务 ## 四、抽奖事务
@ -111,17 +111,16 @@
| 聚合响应 | 批量响应把金额、倍数、阶段反馈和高倍标记聚合 | `luckyAggregateDrawResults``luckyAggregateBudgetSourcesJSON` | 已验收:总倍数为各命中倍数相加,不是平均倍率 | | 聚合响应 | 批量响应把金额、倍数、阶段反馈和高倍标记聚合 | `luckyAggregateDrawResults``luckyAggregateBudgetSourcesJSON` | 已验收:总倍数为各命中倍数相加,不是平均倍率 |
| 聚合发放 | 整批只写一条 outboxpayload 里有聚合 `draw_id` 和全部 `draw_ids` | `insertLuckyAggregateDrawOutbox` | 已验收:待发放队列按送礼命令增长,不按中奖子抽增长 | | 聚合发放 | 整批只写一条 outboxpayload 里有聚合 `draw_id` 和全部 `draw_ids` | `insertLuckyAggregateDrawOutbox` | 已验收:待发放队列按送礼命令增长,不按中奖子抽增长 |
## 五、返奖快路径和房间表现 ## 五、返奖和房间表现
| 功能点 | 行为 | 代码函数实现 | 验收 | | 功能点 | 行为 | 代码函数实现 | 验收 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 同步返奖快路径 | 抽奖事务提交后,如果 `effective_reward_coins > 0`,立即调用 wallet-service `CreditLuckyGiftReward`;成功后把 draw 标记为 `granted` 并把交易 ID、账后余额写回响应 | `Service.Draw``creditLuckyGiftRewardFastPath``creditLuckyGiftReward``MarkLuckyGiftDrawsGranted` | 已验收:不在 activity MySQL 事务里调用 wallet成功响应可直接给 App 刷余额 | | worker 循环 | 持续 claim `LuckyGiftDrawn` outbox | `Service.RunWorker``Service.ProcessPendingDrawOutbox``Repository.ClaimPendingLuckyGiftOutbox` | 已验收claim 使用 `delivering` 锁和 lock TTL |
| worker 循环 | 持续 claim `LuckyGiftDrawn` outbox但它现在是补偿和表现投递路径不是正常发币主路径 | `Service.RunWorker``Service.ProcessPendingDrawOutbox``Repository.ClaimPendingLuckyGiftOutbox` | 已验收claim 使用 `delivering` 锁和 lock TTL |
| payload 兼容 | 新 payload 支持 `draw_ids`,老 payload 只有单个 `draw_id` 时回填 | `decodeDrawOutboxPayload` | 已验收:单抽旧 payload 仍能发放 | | payload 兼容 | 新 payload 支持 `draw_ids`,老 payload 只有单个 `draw_id` 时回填 | `decodeDrawOutboxPayload` | 已验收:单抽旧 payload 仍能发放 |
| 钱包补偿 | worker 先检查 draw 是否已 `granted`;已发奖则跳过 wallet未发奖才用聚合 `draw_id` 生成幂等 command 补发一次 | `Service.processDrawOutbox``GetLuckyGiftDrawRewardState`、`walletv1.CreditLuckyGiftReward` 调用 | 已验收:wallet 成功但 activity 标记失败时worker 可用同一 command 补标;已 granted 不重复加币 | | 钱包返奖 | 用聚合 `draw_id` 生成钱包幂等 command只加币一次 | `Service.processDrawOutbox``walletv1.CreditLuckyGiftReward` 调用 | 已验收:批量 worker 单测覆盖一次钱包请求 |
| 房间开奖结果 IM | worker 投递 `lucky_gift_drawn` 房间群消息 | `publishLuckyGiftDrawn` | 已验收:发布失败时 outbox 可重试;已返奖的 draw 不会因为 IM 失败被标 failed | | 房间开奖结果 IM | worker 投递 `lucky_gift_drawn` 房间群消息 | `publishLuckyGiftDrawn` | 已验收:发布失败时 outbox 可重试,不提前 granted |
| 区域播报 | 符合条件的中奖写区域播报 outbox | `shouldPublishLuckyGiftRegionBroadcast``publishLuckyGiftRegionBroadcast``luckyGiftRegionBroadcastPayload` | 已验收:当前阈值为有区域、有效奖励、聚合倍数不低于 1x | | 区域播报 | 符合条件的中奖写区域播报 outbox | `shouldPublishLuckyGiftRegionBroadcast``publishLuckyGiftRegionBroadcast``luckyGiftRegionBroadcastPayload` | 已验收:当前阈值为有区域、有效奖励、聚合倍数不低于 1x |
| 成功收敛 | 钱包成功后 draw records 先收敛`granted`;区域播报和房间 IM 成功后 outbox 标记 delivered | `MarkLuckyGiftDrawsGranted``markLuckyGiftDraws``MarkLuckyGiftOutboxDelivered` | 已验收:批量更新按 500 个 draw_id 分片 | | 成功收敛 | 钱包、区域播报、房间 IM 都成功后批量标记 draw records 为 granted | `MarkLuckyGiftDrawsGranted``markLuckyGiftDraws``MarkLuckyGiftOutboxDelivered` | 已验收:批量更新按 500 个 draw_id 分片 |
| 失败收敛 | 未返奖且达到最大重试时,批量标记 pending draw failed | `markOutboxFailed``MarkLuckyGiftDrawsFailed` | 已验收:已返奖但 IM 失败不会覆盖账务事实 | | 失败收敛 | 未返奖且达到最大重试时,批量标记 pending draw failed | `markOutboxFailed``MarkLuckyGiftDrawsFailed` | 已验收:已返奖但 IM 失败不会覆盖账务事实 |
| 重试退避 | outbox 失败按指数退避重试 | `luckyGiftBackoff``MarkLuckyGiftOutboxRetryable` | 已验收:错误保存在 `activity_outbox.last_error` | | 重试退避 | outbox 失败按指数退避重试 | `luckyGiftBackoff``MarkLuckyGiftOutboxRetryable` | 已验收:错误保存在 `activity_outbox.last_error` |
@ -149,7 +148,7 @@
| `lucky_user_states` | 用户阶段、累计流水、等价抽数和连亏 | `getLuckyUserState``getLuckyUserStateForUpdate``persistLuckyBatchDraw``applyLuckyDraw` | 已验收:包含 `cumulative_wager_coins``equivalent_draws``loss_streak` | | `lucky_user_states` | 用户阶段、累计流水、等价抽数和连亏 | `getLuckyUserState``getLuckyUserStateForUpdate``persistLuckyBatchDraw``applyLuckyDraw` | 已验收:包含 `cumulative_wager_coins``equivalent_draws``loss_streak` |
| `lucky_risk_counters` | 风控窗口累计返奖 | `getLuckyRiskCounters``getOrCreateLuckyRiskCounter``updateLuckyRiskCounters` | 已验收:用户小时、用户日、设备日、房间小时、主播日五类 scope | | `lucky_risk_counters` | 风控窗口累计返奖 | `getLuckyRiskCounters``getOrCreateLuckyRiskCounter``updateLuckyRiskCounters` | 已验收:用户小时、用户日、设备日、房间小时、主播日五类 scope |
| `lucky_draw_records` | 单抽审计事实 | `insertLuckyDrawRecords``applyLuckyDraw``ListLuckyGiftDraws` | 已验收:批量送礼仍保留单抽粒度 | | `lucky_draw_records` | 单抽审计事实 | `insertLuckyDrawRecords``applyLuckyDraw``ListLuckyGiftDraws` | 已验收:批量送礼仍保留单抽粒度 |
| `activity_outbox` | 幸运礼物钱包补偿和表现副作用,不再承载正常发币主路径 | `insertLuckyAggregateDrawOutbox``ClaimPendingLuckyGiftOutbox``MarkLuckyGiftOutboxDelivered` | 已验收:批量送礼只写一条 `LuckyGiftDrawn` outbox,已 granted 时 worker 跳过 wallet | | `activity_outbox` | 幸运礼物返奖和表现副作用 | `insertLuckyAggregateDrawOutbox``ClaimPendingLuckyGiftOutbox``MarkLuckyGiftOutboxDelivered` | 已验收:批量送礼只写一条 `LuckyGiftDrawn` outbox |
## 八、运行边界 ## 八、运行边界
@ -160,7 +159,7 @@
| 小额礼物 | 阶段按累计流水折算,单抽奖励按单份金额缩放,风控上限不被缩小 | `luckyRuntimeConfig``luckyEquivalentDraws``luckyRiskCapacity` | 已验收:单测覆盖大额后小额仍可中小奖 | | 小额礼物 | 阶段按累计流水折算,单抽奖励按单份金额缩放,风控上限不被缩小 | `luckyRuntimeConfig``luckyEquivalentDraws``luckyRiskCapacity` | 已验收:单测覆盖大额后小额仍可中小奖 |
| 高倍奖 | 发布校验要求高倍档 `HighWaterOnly`,运行时仍受奖池和风控过滤 | `validateRuleStage``selectLuckyCandidate` | 已验收:高倍率未标高水位会拒绝发布 | | 高倍奖 | 发布校验要求高倍档 `HighWaterOnly`,运行时仍受奖池和风控过滤 | `validateRuleStage``selectLuckyCandidate` | 已验收:高倍率未标高水位会拒绝发布 |
| 批量 777 份 | 抽 777 次,保留 777 条明细,状态写入和 outbox 聚合 | `executeOptimizedLuckyGiftDrawBatch``stageLuckyBatchDraw``persistLuckyBatchDraw` | 已验收:代码结构已移出循环内 SQL 状态写入 | | 批量 777 份 | 抽 777 次,保留 777 条明细,状态写入和 outbox 聚合 | `executeOptimizedLuckyGiftDrawBatch``stageLuckyBatchDraw``persistLuckyBatchDraw` | 已验收:代码结构已移出循环内 SQL 状态写入 |
| worker 重试 | 钱包补偿用聚合 `draw_id` 幂等outbox 失败可重试;已 `granted` 的 draw 只补 IM/播报,不重复加币 | `processDrawOutbox`、`GetLuckyGiftDrawRewardState`、`MarkLuckyGiftOutboxRetryable` | 已验收:成功后 delivered不重复加币 | | worker 重试 | 钱包用聚合 `draw_id` 幂等outbox 失败可重试 | `processDrawOutbox`、`MarkLuckyGiftOutboxRetryable` | 已验收:成功后 delivered不重复加币 |
| 已返奖但 IM 失败 | 不把 draw 标 failed只让 outbox failed 并保留告警空间 | `markOutboxFailed` | 已验收:`alreadyCredited` 分支不覆盖 draw 状态 | | 已返奖但 IM 失败 | 不把 draw 标 failed只让 outbox failed 并保留告警空间 | `markOutboxFailed` | 已验收:`alreadyCredited` 分支不覆盖 draw 状态 |
## 九、代码验收清单 ## 九、代码验收清单

View File

@ -27,8 +27,7 @@ Authorization: Bearer <access_token>
"avatar": "https://cdn.example.com/avatar.png", "avatar": "https://cdn.example.com/avatar.png",
"language": "en", "language": "en",
"profile_completed": true, "profile_completed": true,
"onboarding_status": "completed", "onboarding_status": "completed"
"profile_bg_img": "https://cdn.example.com/profile-bg.png"
}, },
"profile_stats": { "profile_stats": {
"visitors_count": 12, "visitors_count": 12,
@ -98,7 +97,6 @@ Authorization: Bearer <access_token>
| Field | Source | Rule | | Field | Source | Rule |
| --- | --- | --- | | --- | --- | --- |
| `profile` | `user-service.GetUser` | 只取头像、昵称、展示 ID、语言等基础资料 | | `profile` | `user-service.GetUser` | 只取头像、昵称、展示 ID、语言等基础资料 |
| `profile.profile_bg_img` | `user-service.GetUser` | 取用户资料里的主页背景图;和 `profile_card` 装扮资源无关,未设置返回空字符串 |
| `profile_stats` | `user-service.GetMyProfileStats` | 读 `user_profile_stats` 计数表,不实时扫访问、关注、好友大表 | | `profile_stats` | `user-service.GetMyProfileStats` | 读 `user_profile_stats` 计数表,不实时扫访问、关注、好友大表 |
| `wallet` | `wallet-service.GetWalletValueSummary` | 首屏只返回金币余额,不返回钻石和美元余额 | | `wallet` | `wallet-service.GetWalletValueSummary` | 首屏只返回金币余额,不返回钻石和美元余额 |
| `vip` | `wallet-service.GetMyVip` | 只取等级、名称、有效期和 active 状态 | | `vip` | `wallet-service.GetMyVip` | 只取等级、名称、有效期和 active 状态 |
@ -121,7 +119,7 @@ Authorization: Bearer <access_token>
| `agency_center` | `roles.is_agency == true` | | `agency_center` | `roles.is_agency == true` |
| `manager_center` | `roles.is_manager == true` | | `manager_center` | `roles.is_manager == true` |
| `bd_center` | `roles.is_bd == true` | | `bd_center` | `roles.is_bd == true` |
| `superadmin_center` | `roles.is_bd_leader == true` | | `bd_leader_center` | `roles.is_bd_leader == true` |
| `coin_seller_center` | `roles.is_coin_seller == true` | | `coin_seller_center` | `roles.is_coin_seller == true` |
| `settings` | always true | | `settings` | always true |
| `language` | always true | | `language` | always true |
@ -136,8 +134,8 @@ Authorization: Bearer <access_token>
| --- | --- | --- | | --- | --- | --- |
| 消息 tab、系统消息、活动消息 | 我的页设计稿不展示消息红点 | 消息 tab 页面接口 | | 消息 tab、系统消息、活动消息 | 我的页设计稿不展示消息红点 | 消息 tab 页面接口 |
| 邀请码入口和邀请统计 | 截图首屏没有邀请码入口 | 邀请页或活动入口 | | 邀请码入口和邀请统计 | 截图首屏没有邀请码入口 | 邀请页或活动入口 |
| `wallet.recharge_enabled` / `diamond_exchange_enabled` | 首屏卡片不需要操作开关 | `GET /api/v1/wallet/me/overview` | | `wallet.recharge_enabled` / `withdraw_enabled` / `diamond_exchange_enabled` | 首屏卡片不需要操作开关 | `GET /api/v1/wallet/me/overview` |
| `DIAMOND` / `HOST_SALARY_USD`, `AGENCY_SALARY_USD`, `BD_SALARY_USD`, `ADMIN_SALARY_USD` | 首屏钱包卡片只展示金币 | 钱包二级页余额接口 | | `DIAMOND` / `USD_BALANCE` | 首屏钱包卡片只展示金币 | 钱包二级页余额接口 |
| 邀请用户列表、钱包流水、工资明细、团队成员、背包完整列表、任务列表 | 都需要分页或聚合 | 对应二级页接口 | | 邀请用户列表、钱包流水、工资明细、团队成员、背包完整列表、任务列表 | 都需要分页或聚合 | 对应二级页接口 |
## Performance Budget ## Performance Budget
@ -182,6 +180,7 @@ sequenceDiagram
| `GET /api/v1/wallet/me/overview` | 钱包二级页余额和操作开关 | | `GET /api/v1/wallet/me/overview` | 钱包二级页余额和操作开关 |
| `GET /api/v1/wallet/recharge/products` | 当前区域充值渠道和档位 | | `GET /api/v1/wallet/recharge/products` | 当前区域充值渠道和档位 |
| `GET /api/v1/wallet/diamond-exchange/config` | 钻石兑换金币/余额配置 | | `GET /api/v1/wallet/diamond-exchange/config` | 钻石兑换金币/余额配置 |
| `POST /api/v1/wallet/withdrawals/apply` | 创建待审核提现申请并冻结 `USD_BALANCE` |
| `GET /api/v1/wallet/transactions` | 钱包流水分页 | | `GET /api/v1/wallet/transactions` | 钱包流水分页 |
| `GET /api/v1/vip/me` | 当前 VIP 状态 | | `GET /api/v1/vip/me` | 当前 VIP 状态 |
| `GET /api/v1/vip/packages` | 可购买 VIP 包 | | `GET /api/v1/vip/packages` | 可购买 VIP 包 |

View File

@ -1,83 +0,0 @@
# 房内猜拳接口文档
本文只记录房内猜拳 App 端接口。独立猜拳本阶段不接入。Flutter 详细字段看 [房内猜拳 Flutter 对接](flutter对接/房内猜拳Flutter对接.md)。
## 查询配置
- 地址:`GET /api/v1/games/room-rps/config`
- 参数:无
- 返回值:
- `config.status``active` / `disabled`
- `config.challenge_timeout_ms`:发起后无人应战的超时时间,默认 10 分钟
- `config.reveal_countdown_ms`:双方出拳后的揭晓倒计时,默认 3 秒
- `config.stake_gifts`:礼物下注档位
- 相关 IM
## 查询房间挑战单
- 地址:`GET /api/v1/games/room-rps/challenges`
- 参数:
- `room_id`:房间 ID必填
- `status`:状态,可选
- `page_size`:页大小,默认 20
- `cursor`:下一页游标
- 返回值:
- `challenges`:挑战单列表
- `challenges[].initiator.user.nickname`:发起人昵称
- `challenges[].initiator.user.avatar`:发起人头像
- `challenges[].challenger.user.nickname`:应战人昵称;未应战时为空
- `challenges[].challenger.user.avatar`:应战人头像;未应战时为空
- `next_cursor`:下一页游标
- `page_size`:本次页大小
- 相关 IM无。客户端漏掉 IM 后可以用这个接口补拉当前房间挑战单。
## 发起挑战
- 地址:`POST /api/v1/games/room-rps/challenges/create`
- 参数:
- `room_id`:房间 ID
- `gift_id`:后台配置的礼物档位 ID
- `gesture``rock` / `paper` / `scissors`
- 返回值:
- `challenge`:挑战单
- 相关 IM
- `room_rps_challenge_created`:服务端向房间群发 `[PK]` 消息,带 `challenge_id`、发起人头像昵称、礼物档位和超时时间。
## 应战
- 地址:`POST /api/v1/games/room-rps/challenges/:challenge_id/accept`
- 参数:
- `challenge_id`:挑战单 ID
- `gesture``rock` / `paper` / `scissors`
- 返回值:
- `challenge`:应战后的挑战单
- 相关 IM
- `room_rps_challenge_accepted`:服务端通知房间挑战已被锁定,带发起人和应战人头像昵称。
- `room_rps_reveal_countdown`:服务端通知双方展示 3 秒倒计时,带发起人和应战人头像昵称。
- `room_rps_finished`:服务端通知胜负、退款或礼物结算结果,带发起人和应战人头像昵称。
## 查询挑战详情
- 地址:`GET /api/v1/games/room-rps/challenges/:challenge_id`
- 参数:
- `challenge_id`:挑战单 ID
- 返回值:
- `challenge.status``pending` / `matched` / `revealing` / `finished` / `timeout` / `settlement_failed` / `cancelled`
- `challenge.initiator`:发起人
- `challenge.initiator.user.nickname`:发起人昵称
- `challenge.initiator.user.avatar`:发起人头像
- `challenge.challenger`:应战人
- `challenge.challenger.user.nickname`:应战人昵称;未应战时为空
- `challenge.challenger.user.avatar`:应战人头像;未应战时为空
- `challenge.winner_user_id`:胜者,平局为空或 0
- `challenge.settlement_status`:结算状态
- 相关 IM无。客户端收到 IM 后可以用这个接口刷新挑战事实。
## 相关 IM 字段
- 所有房内猜拳 IM 都是腾讯 IM `TIMCustomElem``data` 是 JSON 字符串。
- 通用字段:`event_id``event_type``room_id``challenge_id``server_time_ms``challenge`
- `challenge.initiator.user.nickname`:发起人昵称,必填。
- `challenge.initiator.user.avatar`:发起人头像,必填。
- `challenge.challenger.user.nickname`:应战人昵称;`room_rps_challenge_accepted``room_rps_reveal_countdown``room_rps_finished` 必填。
- `challenge.challenger.user.avatar`:应战人头像;`room_rps_challenge_accepted``room_rps_reveal_countdown``room_rps_finished` 必填。

View File

@ -1,47 +0,0 @@
# 房内猜拳本地真实 IM 测试
测试文件:`tests/smoke/room_rps_real_im_flow_test.go`
## 作用
- 用本地 gateway 真实 HTTP 接口跑房内猜拳流程。
- 通过 `/api/v1/auth/account/quick-create` 自动创建发起人和应战人。
- 用真实腾讯 IM REST 确保房间群、导入两个账号、发送一条 `TIMCustomElem` 探针消息。
- 不使用 fake client不伪造 access token不伪造 IM 成功。
## 前置
- 本地 gateway 地址默认:`http://127.0.0.1:13000/api/v1`
- 本地 gateway、game-service、user-service 已启动。
- game-service 启动时需要带 `TENCENT_IM_SDK_APP_ID``TENCENT_IM_SECRET_KEY``TENCENT_IM_ADMIN_IDENTIFIER`,这样创建和应战接口会真实投递 IM。
- 不传 `ROOM_RPS_ROOM_ID` 时,测试会自动创建 `room-rps-real-im-时间戳` 临时群。
- 不传 `ROOM_RPS_GIFT_ID` 时,使用默认已开启礼物 `10001`
- 腾讯 IM 配置必须和本地 gateway 使用同一个 SDKAppID。
## 命令
```bash
ROOM_RPS_REAL_IM_FLOW_TEST=1 \
ROOM_RPS_BASE_URL=http://127.0.0.1:13000/api/v1 \
ROOM_RPS_APP_CODE=lalu \
TENCENT_IM_SDK_APP_ID=1400000000 \
TENCENT_IM_SECRET_KEY='<secret_key>' \
TENCENT_IM_ADMIN_IDENTIFIER=administrator \
TENCENT_IM_ENDPOINT=adminapisgp.im.qcloud.com \
go test ./tests/smoke -run TestRoomRPSLocalHTTPFlowWithRealTencentIM -count=1 -v
```
## 返回判断
- 通过:本地 HTTP 流程完成,真实腾讯 IM 探针消息发送成功game-service 创建和应战 IM 投递成功,发起人和应战人返回头像昵称。
- 失败并提示腾讯 IM 错误:先检查 SDKAppID、SecretKey、管理员账号、endpoint 和 GroupID。
## 清理
默认会清理自动创建的 `room-rps-real-im-` 临时群。
如果传入真实房间群,不要开启清理;可以显式加:
```bash
ROOM_RPS_CLEANUP_IM_GROUP=0
```

View File

@ -9,7 +9,7 @@
| 状态 owner | Room Cell 和 MySQL snapshot/command log 是房间状态权威来源 | | 状态 owner | Room Cell 和 MySQL snapshot/command log 是房间状态权威来源 |
| 持久事实 | `room_outbox` 是房间外事件的持久事实源,和房间命令提交链路一起落 MySQL | | 持久事实 | `room_outbox` 是房间外事件的持久事实源,和房间命令提交链路一起落 MySQL |
| 发布模式 | `outbox_worker.publish_mode` 支持 `direct``mq``dual` | | 发布模式 | `outbox_worker.publish_mode` 支持 `direct``mq``dual` |
| MQ 职责 | RocketMQ 只做事件分发和火箭延迟唤醒,不保存房间核心状态 | | MQ 职责 | RocketMQ 只做事件分发和宝箱延迟唤醒,不保存房间核心状态 |
| 下游语义 | 下游服务按 `event_id` 幂等消费,失败由自己的位点、重试和死信处理 | | 下游语义 | 下游服务按 `event_id` 幂等消费,失败由自己的位点、重试和死信处理 |
| 主链路 | HTTP/gRPC 请求不等待 IM REST、activity、notice、push 或 inbox 投递完成 | | 主链路 | HTTP/gRPC 请求不等待 IM REST、activity、notice、push 或 inbox 投递完成 |
@ -37,7 +37,7 @@ gateway -> room-service command
outbox worker outbox worker
-> claim pending/retryable rows -> claim pending/retryable rows
-> if RoomRocketIgnited, schedule rocket launch delayed wakeup when configured -> if RoomTreasureCountdownStarted, schedule treasure open delayed wakeup when configured
-> publish by mode: -> publish by mode:
direct: Tencent IM publisher + activity gRPC publisher direct: Tencent IM publisher + activity gRPC publisher
mq: RocketMQ room_outbox 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-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-notice-room-outbox` | notice-service 消费私有通知相关事实,例如 `RoomUserKicked` |
| `hyapp_room_outbox` | `room-service` outbox worker | `hyapp-room-im-bridge` | room-service IM bridge 消费房间群系统消息和群成员控制事实 | | `hyapp_room_outbox` | `room-service` outbox worker | `hyapp-room-im-bridge` | room-service IM bridge 消费房间群系统消息和群成员控制事实 |
| `hyapp_room_rocket_launch` | `room-service` rocket scheduler | `hyapp-room-rocket-open` | 火箭倒计时到点唤醒 Room Cell 发射命令 | | `hyapp_room_treasure_open` | `room-service` treasure scheduler | `hyapp-room-treasure-open` | 宝箱倒计时到点唤醒 Room Cell 开箱命令 |
`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 重置边界。 `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 重置边界。
## 配置 ## 配置
@ -96,8 +96,6 @@ rocketmq:
namespace: "" namespace: ""
send_timeout: "3s" send_timeout: "3s"
retry: 2 retry: 2
# 安全默认值;全部 room consumer 完成 legacy||typed 兼容订阅后才切 event_type。
room_event_tag_mode: "legacy"
room_outbox: room_outbox:
enabled: false enabled: false
topic: "hyapp_room_outbox" topic: "hyapp_room_outbox"
@ -105,11 +103,11 @@ rocketmq:
tencent_im_consumer_enabled: false tencent_im_consumer_enabled: false
tencent_im_consumer_group: "hyapp-room-im-bridge" tencent_im_consumer_group: "hyapp-room-im-bridge"
consumer_max_reconsume_times: 16 consumer_max_reconsume_times: 16
rocket_launch: treasure_open:
enabled: false enabled: false
topic: "hyapp_room_rocket_launch" topic: "hyapp_room_treasure_open"
producer_group: "hyapp-room-rocket-open-producer" producer_group: "hyapp-room-treasure-open-producer"
consumer_group: "hyapp-room-rocket-open" consumer_group: "hyapp-room-treasure-open"
consumer_max_reconsume_times: 16 consumer_max_reconsume_times: 16
outbox_worker: outbox_worker:
@ -129,13 +127,12 @@ outbox_worker:
| 字段 | 规则 | | 字段 | 规则 |
| --- | --- | | --- | --- |
| `rocketmq.enabled` | 控制 RocketMQ client 是否启用;本地默认关闭,线上示例开启 | | `rocketmq.enabled` | 控制 RocketMQ client 是否启用;本地默认关闭,线上示例开启 |
| `rocketmq.room_event_tag_mode` | `legacy` 是无损迁移默认值;`event_type` 只在所有 consumer 已双订阅后显式开启 |
| `rocketmq.room_outbox.enabled` | 控制 room outbox 是否可发布到 MQ`publish_mode=mq/dual` 时必须开启 | | `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.room_outbox.tencent_im_consumer_enabled` | 控制 room-service 是否作为 IM bridge consumer 消费 `hyapp_room_outbox` |
| `rocketmq.rocket_launch.enabled` | 控制火箭点火后是否发布和消费延迟发射唤醒消息 | | `rocketmq.treasure_open.enabled` | 控制宝箱倒计时是否发布和消费延迟唤醒消息 |
| `outbox_worker.publish_mode` | 本地默认 `direct`,线上建议 `mq`,迁移期可用 `dual` | | `outbox_worker.publish_mode` | 本地默认 `direct`,线上建议 `mq`,迁移期可用 `dual` |
| `poll_interval` | worker 空轮询间隔,必须大于 0避免 busy loop | | `poll_interval` | worker 空轮询间隔,必须大于 0避免 busy loop |
| `batch_size` | 每轮最多处理条数;同步 publisher 逐条 claim避免批尾尚未发送 lease 就已过期 | | `batch_size` | 每轮最大抢占条数,必须大于 0避免全表扫描 |
| `publish_timeout` | 单条 outbox 发布 deadline避免外部系统拖死 worker | | `publish_timeout` | 单条 outbox 发布 deadline避免外部系统拖死 worker |
| `max_retry_count` | 达到次数后标记 `failed`,防止 poison event 永久刷日志 | | `max_retry_count` | 达到次数后标记 `failed`,防止 poison event 永久刷日志 |
@ -146,7 +143,7 @@ outbox_worker:
| RocketMQ room outbox publisher | 把 protobuf outbox 包装成统一 `room_outbox_event`,发布到 `hyapp_room_outbox` | | RocketMQ room outbox publisher | 把 protobuf outbox 包装成统一 `room_outbox_event`,发布到 `hyapp_room_outbox` |
| Tencent IM direct publisher | `RoomCreated` 对已同步创建的房间群执行幂等 EnsureGroup房间系统事件发送腾讯云 IM 群自定义消息 | | Tencent IM direct publisher | `RoomCreated` 对已同步创建的房间群执行幂等 EnsureGroup房间系统事件发送腾讯云 IM 群自定义消息 |
| Activity direct publisher | 本地或未接 MQ 时直接调用 activity-service room event 消费入口 | | Activity direct publisher | 本地或未接 MQ 时直接调用 activity-service room event 消费入口 |
| Rocket launch scheduler | 在 `RoomRocketIgnited` 后发布到点延迟消息,唤醒 room-service 发射 | | Treasure open scheduler | 在 `RoomTreasureCountdownStarted` 后发布到点延迟消息,唤醒 room-service 开箱 |
| Activity MQ consumer | 独立 consumer group 消费 `hyapp_room_outbox`,按 `event_id` 幂等推进活动/播报逻辑 | | Activity MQ consumer | 独立 consumer group 消费 `hyapp_room_outbox`,按 `event_id` 幂等推进活动/播报逻辑 |
| Notice MQ consumer | 独立 consumer group 消费 `hyapp_room_outbox`,写 `notice_delivery_events` 后投 C2C 私有通知 | | Notice MQ consumer | 独立 consumer group 消费 `hyapp_room_outbox`,写 `notice_delivery_events` 后投 C2C 私有通知 |
| Room IM bridge MQ consumer | 独立 consumer group 消费 `hyapp_room_outbox`,投递房间群系统消息和群成员控制 | | Room IM bridge MQ consumer | 独立 consumer group 消费 `hyapp_room_outbox`,投递房间群系统消息和群成员控制 |
@ -196,6 +193,6 @@ RocketMQ consumer 失败时不修改 `room_outbox`。consumer 应依赖 RocketMQ
| MQ 不可用,`mq/dual` | 房间写命令仍成功outbox 进入重试MQ 恢复后补发 | | MQ 不可用,`mq/dual` | 房间写命令仍成功outbox 进入重试MQ 恢复后补发 |
| activity consumer 不可用,`mq` | `room_outbox` 不回退RocketMQ 和 activity 消费位点负责积压与重试 | | activity consumer 不可用,`mq` | `room_outbox` 不回退RocketMQ 和 activity 消费位点负责积压与重试 |
| poison event | 达到 `max_retry_count` 后进入 `failed`,不再每秒刷日志 | | poison event | 达到 `max_retry_count` 后进入 `failed`,不再每秒刷日志 |
| 火箭倒计时 | `RoomRocketIgnited` 发布延迟唤醒;到点后 room-service 重新校验状态再发射 | | 宝箱倒计时 | `RoomTreasureCountdownStarted` 发布延迟唤醒;到点后 room-service 重新校验状态再开箱 |
| 正常投递 | 同一 `event_id` 可被多个 consumer group 独立消费且重复投递不产生重复副作用 | | 正常投递 | 同一 `event_id` 可被多个 consumer group 独立消费且重复投递不产生重复副作用 |
| 真实链路 | create/join/mic/sendGift/leave 均有 `room_command_timing` | | 真实链路 | create/join/mic/sendGift/leave 均有 `room_command_timing` |

View File

@ -1,329 +1,46 @@
# 房间置顶与房间列表排序产品开发文档 # 房间区域置顶
## 目标 房间置顶是公共房间发现列表的运营排序能力。房间列表按 `visible_region_id` 隔离,所以置顶也按区域生效:后台选择一个房间后,后端使用该房间当前 `visible_region_id` 建立 `room_region_pins` 记录。
后台在「房间管理」下增加二级菜单「房间置顶」,用于运营配置公共房间发现页的置顶顺序。 ## 边界
置顶只影响 App 房间发现列表排序,不改变房间状态、不改变房间区域、不影响进房权限。 - `room-service` 仍然是房间状态 owner置顶不进入 Room Cell、snapshot 或 command log。
- 置顶只影响 `GET /api/v1/rooms?tab=hot/new` 的排序,不影响进房权限。
- 有效置顶条件为 `status=active``expires_at_ms > now_ms`
- 房间改区域后,旧区域置顶不会自动迁移到新区域;后台需要取消旧置顶并在新区域重新置顶。
## 后台页面 ## 排序
菜单:房间管理 / 房间置顶 同一区域公共列表排序:
列表展示: 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` 前缀。 后台接口使用后台登录 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 ```json
{ {
"code": 0, "roomId": "room_123",
"message": "ok",
"data": {
"items": [
{
"id": 1,
"pinType": "global",
"status": "active",
"weight": 100, "weight": 100,
"pinnedAtMs": 1780665600000, "durationDays": 30
"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` 房间。
地址:`GET /api/v1/admin/rooms` 列表返回的 `room` 包含房间长短 ID、昵称、头像和区域`user` 包含房主头像、长短 ID 和昵称;`remainingMs` 是当前 UTC 业务时间下的剩余毫秒数。
参数:
| 参数 | 说明 |
| --- | --- |
| `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

@ -18,9 +18,6 @@
| GET | `/api/v1/countries` | countries | `listRegistrationCountries` | 获取 App 注册页可选国家 | | GET | `/api/v1/countries` | countries | `listRegistrationCountries` | 获取 App 注册页可选国家 |
| GET | `/api/v1/app/bootstrap` | app-config | `getAppBootstrap` | 获取 App 启动配置摘要 | | GET | `/api/v1/app/bootstrap` | app-config | `getAppBootstrap` | 获取 App 启动配置摘要 |
| GET | `/api/v1/app/h5-links` | app-config | `listAppH5Links` | 获取 App H5 入口地址 | | GET | `/api/v1/app/h5-links` | app-config | `listAppH5Links` | 获取 App H5 入口地址 |
| GET | `/api/v1/app/splash-screens` | app-config | `listSplashScreens` | 获取 App 开屏配置 |
| GET | `/api/v1/app/popups` | app-config | `listAppPopups` | 获取 App 弹窗配置 |
| POST | `/api/v1/app/heartbeat` | app | `appHeartbeat` | 刷新当前 App 登录会话心跳 |
| GET | `/api/v1/im/usersig` | tencent-im | `issueTencentIMUserSig` | 获取腾讯云 IM UserSig | | GET | `/api/v1/im/usersig` | tencent-im | `issueTencentIMUserSig` | 获取腾讯云 IM UserSig |
| POST | `/api/v1/rtc/token` | tencent-rtc | `issueTencentRTCToken` | 获取腾讯 RTC 进房 UserSig | | POST | `/api/v1/rtc/token` | tencent-rtc | `issueTencentRTCToken` | 获取腾讯 RTC 进房 UserSig |
| POST | `/api/v1/tencent-rtc/callback` | tencent-rtc | `handleTencentRTCCallback` | 腾讯 RTC 服务端事件回调 | | POST | `/api/v1/tencent-rtc/callback` | tencent-rtc | `handleTencentRTCCallback` | 腾讯 RTC 服务端事件回调 |
@ -38,9 +35,7 @@
| GET | `/api/v1/users/me/identity` | users | `getMyIdentity` | 查询当前用户展示短号状态 | | GET | `/api/v1/users/me/identity` | users | `getMyIdentity` | 查询当前用户展示短号状态 |
| GET | `/api/v1/users/me/host-identity` | users | `getMyHostIdentity` | 查询当前用户 Host/Agency/BD 身份 | | GET | `/api/v1/users/me/host-identity` | users | `getMyHostIdentity` | 查询当前用户 Host/Agency/BD 身份 |
| GET | `/api/v1/users/me/overview` | users | `getMyOverview` | 我的页首屏聚合摘要 | | GET | `/api/v1/users/me/overview` | users | `getMyOverview` | 我的页首屏聚合摘要 |
| GET | `/api/v1/users/me/invite-overview` | users | `getMyInviteOverview` | 查询当前用户邀请码和邀请计数 |
| GET | `/api/v1/users/me/gift-wall` | users | `getMyGiftWall` | 查询当前用户收到的礼物墙聚合 | | GET | `/api/v1/users/me/gift-wall` | users | `getMyGiftWall` | 查询当前用户收到的礼物墙聚合 |
| GET | `/api/v1/users/{user_id}/gift-wall` | users | `getUserGiftWall` | 查询资料卡用户收到的礼物墙聚合 |
| GET | `/api/v1/users/me/role-summary` | users | `getMyRoleSummary` | 查询当前用户角色摘要 | | GET | `/api/v1/users/me/role-summary` | users | `getMyRoleSummary` | 查询当前用户角色摘要 |
| POST | `/api/v1/users/me/onboarding/complete` | users | `completeMyOnboarding` | 完成注册页必填资料 | | POST | `/api/v1/users/me/onboarding/complete` | users | `completeMyOnboarding` | 完成注册页必填资料 |
| POST | `/api/v1/users/me/profile/update` | users | `updateMyProfile` | 修改当前用户基础资料 | | POST | `/api/v1/users/me/profile/update` | users | `updateMyProfile` | 修改当前用户基础资料 |
@ -57,8 +52,7 @@
| GET | `/api/v1/users/me/friend-requests` | users | `listMyFriendApplications` | 我的好友申请列表 | | GET | `/api/v1/users/me/friend-requests` | users | `listMyFriendApplications` | 我的好友申请列表 |
| GET | `/api/v1/users/me/friends` | users | `listMyFriends` | 我的好友列表 | | GET | `/api/v1/users/me/friends` | users | `listMyFriends` | 我的好友列表 |
| DELETE | `/api/v1/users/{user_id}/friend` | users | `deleteFriend` | 双向删除好友关系 | | DELETE | `/api/v1/users/{user_id}/friend` | users | `deleteFriend` | 双向删除好友关系 |
| GET | `/api/v1/rooms/filters` | rooms | `listRoomFilters` | Flutter 首页区域/国家筛选目录,下发可原样回传的 `region_id/country_code` | | GET | `/api/v1/rooms` | rooms | `listRooms` | 房间发现列表,支持 `tab=hot/new`,卡片返回 `im_group_id` |
| GET | `/api/v1/rooms` | rooms | `listRooms` | 房间发现列表Huwaa 默认 All支持互斥的 `region_id/country_code``tab=hot/new`,卡片返回 `im_group_id` |
| GET | `/api/v1/rooms/me` | rooms | `getMyRoom` | 查询 Mine 顶部我的房间卡片 | | GET | `/api/v1/rooms/me` | rooms | `getMyRoom` | 查询 Mine 顶部我的房间卡片 |
| GET | `/api/v1/rooms/feeds` | rooms | `listRoomFeeds` | Mine 页 Visited/Friend/Following/Followed 房间流 | | GET | `/api/v1/rooms/feeds` | rooms | `listRoomFeeds` | Mine 页 Visited/Friend/Following/Followed 房间流 |
| GET | `/api/v1/rooms/current` | rooms | `getCurrentRoom` | 查询当前可恢复房间 | | GET | `/api/v1/rooms/current` | rooms | `getCurrentRoom` | 查询当前可恢复房间 |
@ -81,7 +75,6 @@
| POST | `/api/v1/rooms/mic/down` | rooms | `micDown` | 下麦 | | POST | `/api/v1/rooms/mic/down` | rooms | `micDown` | 下麦 |
| POST | `/api/v1/rooms/mic/change` | rooms | `changeMicSeat` | 调整麦位 | | POST | `/api/v1/rooms/mic/change` | rooms | `changeMicSeat` | 调整麦位 |
| POST | `/api/v1/rooms/mic/publishing/confirm` | rooms | `confirmMicPublishing` | 确认 RTC 音频发布成功 | | POST | `/api/v1/rooms/mic/publishing/confirm` | rooms | `confirmMicPublishing` | 确认 RTC 音频发布成功 |
| POST | `/api/v1/rooms/mic/heartbeat` | rooms | `micHeartbeat` | 刷新当前麦位会话心跳 |
| POST | `/api/v1/rooms/mic/mute` | rooms | `setMicMute` | 设置服务端可见麦克风静音状态 | | POST | `/api/v1/rooms/mic/mute` | rooms | `setMicMute` | 设置服务端可见麦克风静音状态 |
| POST | `/api/v1/rooms/mic/lock` | rooms | `setMicSeatLock` | 锁定或解锁麦位 | | POST | `/api/v1/rooms/mic/lock` | rooms | `setMicSeatLock` | 锁定或解锁麦位 |
| POST | `/api/v1/rooms/chat/enabled` | rooms | `setChatEnabled` | 开启或关闭房间公屏 | | POST | `/api/v1/rooms/chat/enabled` | rooms | `setChatEnabled` | 开启或关闭房间公屏 |
@ -106,6 +99,7 @@
| GET | `/api/v1/wallet/me/balances` | wallet | `getMyBalances` | 查询当前用户钱包余额 | | GET | `/api/v1/wallet/me/balances` | wallet | `getMyBalances` | 查询当前用户钱包余额 |
| GET | `/api/v1/wallet/recharge/products` | wallet | `listRechargeProducts` | 查询充值商品和渠道 | | GET | `/api/v1/wallet/recharge/products` | wallet | `listRechargeProducts` | 查询充值商品和渠道 |
| GET | `/api/v1/wallet/diamond-exchange/config` | wallet | `getDiamondExchangeConfig` | 查询钻石兑换配置 | | 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/coin-transactions` | wallet | `listCoinTransactions` | 分页查询当前用户金币流水 |
| GET | `/api/v1/wallet/transactions` | wallet | `listWalletTransactions` | 分页查询钱包流水 | | GET | `/api/v1/wallet/transactions` | wallet | `listWalletTransactions` | 分页查询钱包流水 |
| POST | `/api/v1/wallet/coin-seller/transfer` | wallet | `transferCoinFromSeller` | 币商给玩家转金币 | | POST | `/api/v1/wallet/coin-seller/transfer` | wallet | `transferCoinFromSeller` | 币商给玩家转金币 |
@ -127,10 +121,6 @@
| POST | `/api/v1/admin/app-config/banners` | admin | `createBanner` | createBanner | | POST | `/api/v1/admin/app-config/banners` | admin | `createBanner` | createBanner |
| DELETE | `/api/v1/admin/app-config/banners/{banner_id}` | admin | `deleteBanner` | deleteBanner | | DELETE | `/api/v1/admin/app-config/banners/{banner_id}` | admin | `deleteBanner` | deleteBanner |
| PUT | `/api/v1/admin/app-config/banners/{banner_id}` | admin | `updateBanner` | updateBanner | | PUT | `/api/v1/admin/app-config/banners/{banner_id}` | admin | `updateBanner` | updateBanner |
| GET | `/api/v1/admin/app-config/splash-screens` | admin | `listSplashScreens` | listSplashScreens |
| POST | `/api/v1/admin/app-config/splash-screens` | admin | `createSplashScreen` | createSplashScreen |
| DELETE | `/api/v1/admin/app-config/splash-screens/{splash_id}` | admin | `deleteSplashScreen` | deleteSplashScreen |
| PUT | `/api/v1/admin/app-config/splash-screens/{splash_id}` | admin | `updateSplashScreen` | updateSplashScreen |
| GET | `/api/v1/admin/app-config/h5-links` | admin | `listH5Links` | listH5Links | | GET | `/api/v1/admin/app-config/h5-links` | admin | `listH5Links` | listH5Links |
| PUT | `/api/v1/admin/app-config/h5-links` | admin | `updateH5Links` | updateH5Links | | PUT | `/api/v1/admin/app-config/h5-links` | admin | `updateH5Links` | updateH5Links |
| GET | `/api/v1/admin/apps` | admin | `listApps` | listApps | | GET | `/api/v1/admin/apps` | admin | `listApps` | listApps |

View File

@ -1,96 +0,0 @@
# 数据大屏统计口径
本文档描述 `statistics-service` 提供给后台数据大屏的字段口径。查询入口是 `server/admin` 转发到 `statistics-service``/internal/v1/statistics/overview`,返回字段同时用于 `overview``daily_series``country_breakdown``daily_country_breakdown`
## 通用规则
- 时间范围统一是 `[start_ms, end_ms)`,包含开始毫秒,不包含结束毫秒。
- 默认统计时区是 `UTC`;传 `stat_tz=Asia/Shanghai` 时按中国自然日查询统计读模型。
- 国家和区域只使用事件里的国家/区域字段或统计服务保存的用户维度不用服务器所在地、IP 或本地时区推断。
- 查询只读 `statistics-service` 聚合表,不回查 wallet、room、game、user owner 明细表。
- 运营宽表的 DAU 统一读取 `stat_social_user_day`,再用 `stat_social_identity_day` 把同一登录会话内的 `d:<device_id>` 临时归并到 `u:<user_id>`;维度筛选和国家/区域明细均在归并后计算。
- `coin_total` 为兼容前端保留 JSON 字段名,展示名改为“用户金币数”。
## 字段口径
| 字段 | 展示名 | 口径 |
| --- | --- | --- |
| `app_code` | 应用编码 | 当前查询的租户编码。 |
| `stat_tz` | 统计时区 | `UTC``Asia/Shanghai`。 |
| `start_ms` / `end_ms` | 查询时间 | 本次查询的毫秒时间范围。 |
| `country_id` / `region_id` | 国家/区域 | 聚合维度;未筛选国家时返回国家分解。 |
| `new_users` | 新增用户 | `UserRegistered` 注册 cohort 去重用户数。 |
| `active_users` | 活跃用户 | `stat_social_user_day.active_user` 经设备日级 canonical 身份归并后的用户日去重数;多日汇总为活跃人天。 |
| `paid_users` | 付费用户 | `WalletRechargeRecorded` 写入 `stat_recharge_day_payers` 后按用户去重。 |
| `new_paid_users` | 新增付费用户 | 付费用户中注册 cohort 也落在查询周期内的用户数。 |
| `recharge_usd_minor` | 总充值 | `WalletRechargeRecorded.usd_minor_amount` 累加,单位是美元最小单位。 |
| `new_user_recharge_usd_minor` | 新增充值 | `recharge_sequence=1` 的充值金额累加。 |
| `coin_seller_recharge_usd_minor` | 币商充值 | 币商进货 USDT 和未识别为 Google/MifaPay/USDT-TRC20/币商转账的充值美元最小单位。 |
| `coin_seller_stock_coin` | 币商充值金币 | `WalletCoinSellerStockPurchased.coin_amount` 累加。 |
| `mifapay_recharge_usd_minor` | 三方充值 | `recharge_type=mifapay/v5pay` 的充值美元最小单位。 |
| `google_recharge_usd_minor` | Google 充值 | `recharge_type=google/google_play` 的充值美元最小单位。 |
| `coin_seller_transfer_coin` | 币商出货金币 | 币商向用户转入普通 COIN 金币数量。 |
| `coin_total` | 用户金币数 | 用户普通 COIN 钱包 `available+frozen` 余额总和;历史日取当天 23:59:59.999 前最后一次余额,今日按 `WalletBalanceChanged` 增量滚动更新;多日总览只取结束日快照,日序列展示每天快照。 |
| `consumed_coin` | 消耗金币 | 游戏投入 + 普通礼物投入 + lucky 礼物投入 + 转盘投入 + 发红包。游戏来自 `GameOrderSettled debit/bet`,房间礼物来自 `RoomGiftSent.coin_spent`,钱包只补 `direct_gift_debit``wheel_draw_debit``red_packet_create`。 |
| `output_coin` | 产出金币 | 游戏产出 + 收礼产出 + lucky 礼物产出 + 转盘奖励金币 + 红包领取和退回。游戏来自 `GameOrderSettled credit/payout`,收礼来自 `WalletGiftIncomeCredited`lucky 来自 `WalletLuckyGiftRewardCredited`,转盘金币来自 `WalletWheelRewardCredited`,红包来自 `red_packet_claim/refund`。 |
| `consume_output_ratio` | 消耗产出比 | `output_coin / consumed_coin`;消耗为 0 时返回 0。 |
| `consume_output_delta` | 消耗产出差额 | `consumed_coin - output_coin`。 |
| `platform_grant_coin` | 平台发放金币 | 排行榜奖励、活动奖励、签到/任务奖励、火箭奖励中的 COIN 金币发放;包括对应 `Wallet*RewardCredited``resource_grant` COIN 入账不含人工发放、装扮价值、lucky 返奖、转盘产出。 |
| `manual_grant_coin` | 人工发放金币 | `WalletBalanceChanged biz_type=manual_credit` 的普通 COIN 正向发放。 |
| `salary_usd_minor` | 工资总和(主播/代理) | 主播和代理工资钱包 `HOST_SALARY_USD + AGENCY_SALARY_USD``available+frozen` 余额总和;历史日取当天 23:59:59.999 前最后一次余额,今日由补数任务按当前已有分录刷新;多日总览只取结束日快照,日序列展示每天快照;不包含 BD/Admin 工资和 residual。 |
| `salary_transfer_coin` | 工资兑换金币 | 工资转给币商时币商 `COIN_SELLER_COIN` 入账金币数量,维度按发起转账用户。 |
| `mic_online_ms` | 麦上总时长 | `user_mic_daily_stats.mic_online_ms` 按日、国家、区域聚合。 |
| `mic_online_users` | 麦上用户数 | 当日 `mic_online_ms > 0` 的去重用户数。 |
| `avg_mic_online_ms` | 平均麦上时长 | `mic_online_ms / mic_online_users`。 |
| `gift_coin_spent` | 礼物流水 | 非机器人真实 `RoomGiftSent.coin_spent`,历史事件无 `coin_spent` 时回退 `gift_value`。 |
| `real_room_robot_normal_gift_coin` | 真人房机器人普通礼物 | `real_room_robot_gift=true` 且非 lucky/super lucky 的礼物金币。 |
| `real_room_robot_lucky_gift_coin` | 真人房机器人 lucky 礼物 | 真人房机器人 lucky 礼物金币。 |
| `real_room_robot_super_gift_coin` | 真人房机器人 super lucky 礼物 | 真人房机器人 super lucky 礼物金币。 |
| `real_room_robot_total_gift_coin` | 真人房机器人总礼物 | 上面三类机器人礼物金币合计。 |
| `real_room_robot_gift_room_count` | 真人房机器人投放房间数 | 有真人房机器人礼物的房间去重数。 |
| `real_room_robot_avg_room_gift_coin` | 真人房机器人房均投放 | `real_room_robot_total_gift_coin / real_room_robot_gift_room_count`。 |
| `lucky_gift_turnover` | lucky 礼物流水 | `RoomGiftSent.pool_id` 非空的送礼金币。 |
| `lucky_gift_payout` | lucky 礼物产出 | `WalletLuckyGiftRewardCredited.amount`UTC 查询会优先用 activity 的 lucky pool 日表覆盖旧统计行。 |
| `lucky_gift_payers` | lucky 礼物付费用户 | 发送 lucky 礼物的用户按日去重。 |
| `lucky_gift_profit` | lucky 礼物利润 | `lucky_gift_turnover - lucky_gift_payout`。 |
| `lucky_gift_payout_rate` | lucky 礼物返奖率 | `lucky_gift_payout / lucky_gift_turnover`。 |
| `lucky_gift_profit_rate` | lucky 礼物利润率 | `lucky_gift_profit / lucky_gift_turnover`。 |
| `game_turnover` | 游戏流水 | `GameOrderSettled op_type=debit/bet` 的金币。 |
| `game_payout` | 游戏返奖 | `GameOrderSettled op_type=credit/payout` 的金币。 |
| `game_refund` | 游戏退款 | `GameOrderSettled op_type=refund/reverse` 的金币。 |
| `game_players` | 游戏玩家数 | 有游戏投入的用户按日、游戏平台、游戏去重后汇总。 |
| `game_profit` | 游戏利润 | `game_turnover - game_payout - game_refund`。 |
| `game_profit_rate` | 游戏利润率 | `game_profit / game_turnover`。 |
| `super_lucky_gift_turnover` | super lucky 流水 | `pool_id` 包含 `super_lucky` 的奖池流水。 |
| `super_lucky_gift_payout` | super lucky 返奖 | `pool_id` 包含 `super_lucky` 的奖池返奖。 |
| `super_lucky_gift_profit` | super lucky 利润 | `super_lucky_gift_turnover - super_lucky_gift_payout`。 |
| `super_lucky_gift_profit_rate` | super lucky 利润率 | `super_lucky_gift_profit / super_lucky_gift_turnover`。 |
| `arpu_usd_minor` | ARPU | `recharge_usd_minor / canonical active_users`。 |
| `arppu_usd_minor` | ARPPU | `recharge_usd_minor / paid_users`。 |
| `up_value_usd_minor` | UP 值 | 当前与 ARPPU 同口径。 |
## 对比字段
以下字段是当前周期与上一等长周期的变化率:`recharge_delta_rate``new_user_recharge_delta_rate``active_users_delta_rate``paid_users_delta_rate``new_paid_users_delta_rate``arpu_delta_rate``arppu_delta_rate``gift_coin_spent_delta_rate``coin_seller_transfer_coin_delta_rate``lucky_gift_turnover_delta_rate``lucky_gift_payout_delta_rate``lucky_gift_profit_delta_rate``game_turnover_delta_rate``game_profit_delta_rate`
变化率公式是 `(当前值 - 上期值) / abs(上期值)`;上期为 0 且当前大于 0 时返回 1。
## 明细对象
| 字段 | 口径 |
| --- | --- |
| `retention` | `registered_users` 为注册 cohort`day1/day7/day30_users` 为注册后第 1/7/30 天有活跃的用户rate 为对应用户数除以注册用户数。 |
| `daily_series` | 按日返回本页主要字段DAU 使用 canonical Social`coin_total``salary_usd_minor` 是每天余额快照,其它流水字段按当天累加。 |
| `country_breakdown` | 按国家返回本页主要字段DAU 维度在 canonical 归并后确定,`coin_total``salary_usd_minor` 是查询结束日该国家余额快照,其它字段按查询周期累加。 |
| `daily_country_breakdown` | 按日 + 国家返回主要字段DAU 与顶部/按日使用同一 canonical Social 数据源。 |
| `game_ranking` | 按 `platform_code/game_id` 聚合游戏流水、返奖、退款、玩家数、利润和利润率。 |
| `lucky_gift_pools` | 按 `pool_id` 聚合 lucky 礼物流水、返奖、利润、返奖率和利润率。 |
| `report_metric_sources` | 返回字段来源和口径说明,供前端展示或排查。 |
## 当前边界
- 转盘礼物/道具的 RTP 价值在 activity wheel 记录里有 `rtp_value_coins`,但 wallet 资源 outbox 当前不是稳定 MQ 统计输入;统计服务现在只计转盘金币奖励,礼物价值需要资源奖励事件可稳定发布后接入。
- 平台发放的礼物价值同理,需要 `ResourceGranted/ResourceGroupGranted` 事件携带可统计的资源金币价值和来源;当前只可靠计入 COIN 入账和已存在的 `Wallet*RewardCredited` 金币奖励。
- 装扮价值不进入 `platform_grant_coin``output_coin`
- 后台人工发放只进入 `manual_grant_coin`,不进入 `platform_grant_coin`
- `retention` 仍是注册 cohort 对 `stat_user_day_activity` 的回看口径,不属于运营宽表 DAU 分母Social 数据需求页的用户留存则使用 canonical Social 用户日口径。

View File

@ -1,351 +0,0 @@
# 新 App 上线配置检查清单
本文档定义 HyApp 新 App 从租户注册到正式运营的配置边界、业务 owner、开启条件和验收标准。它不是“把旧 App 数据全量复制一遍”的脚本说明;每项数据必须按 owner service 和业务键管理,不能跨租户复用原始自增 ID。
## 1. 使用方式
每次新 App 上线前,先建立一份当次上线记录,填写:
| 字段 | 说明 |
| --- | --- |
| `app_code` | 后端租户编码,全链路使用同一个小写值 |
| App 名称 | 用户可见品牌名 |
| Android package | Android 包名,必须和 Firebase、Google Play 一致 |
| iOS bundle ID | iOS bundle identifier必须和 Firebase、Apple 后台一致 |
| 对照 App | 只用于确定初始产品口径,不代表可以直接复制原始 ID |
| 计划开服时间 | UTC epoch milliseconds用于活动、版本和内容生效时间 |
| 支持国家/区域 | 国家、区域、内容和价格配置的权威范围 |
| 负责人 | 后端、客户端、运营、支付、IM/RTC 各自验收人 |
状态只使用以下四种:
- `NOT_STARTED`:尚未配置。
- `CONFIGURED`:已写入,但未经过真实请求验证。
- `VERIFIED`:已用新 `app_code` 走通生产链路并保留证据。
- `BLOCKED`:存在会阻断上线的问题,必须写明 owner 和解决动作。
## 2. 上线硬性原则
1. 新 App 数据必须使用新 `app_code`;不能依赖代码中的 `lalu` 默认值维持运行。
2. 复制配置必须按 `resource_code``group_code``region_code``product_code` 等业务键重建引用,不能复制旧 App 的自增 ID。
3. 礼物、活动、VIP、游戏等只有主配置表不算完成所有价格、区域、资源、奖励和 worker 必须一起验证。
4. 敏感值只能写入线上环境变量或已忽略的运行时 env不能写入可提交 YAML、SQL、文档或代码。
5. 全部时间使用 UTC范围统一使用 `[start_ms, end_ms)`
6. 共享 RocketMQ topic 不代表多 App 已经就绪;必须确认消息 payload、consumer context、幂等表和 outbox claim 都保留 `app_code`
7. 只有完成“配置写入 → 服务加载 → 真实 API/IM/RTC/MQ 请求 → 日志与数据回查”才能标记 `VERIFIED`
## 3. App 注册与租户基础
Owner`user-service``gateway-service`
| 配置项 | 数据/配置入口 | 开启条件 | 验收标准 |
| --- | --- | --- | --- |
| App 注册 | `hyapp_user.apps` | `status=active``app_code` 和包名唯一 | 按 `X-App-Code` 和 package 都能解析到同一 App |
| Android/iOS 包信息 | `apps` 及客户端 flavor | 与应用商店、Firebase 完全一致 | 真实安装包登录成功 |
| Firebase 项目 | `user-service.third_party.firebase.projects.<app_code>` | 项目 ID、凭据和签名指纹都属于新 App | Google/Firebase token 在新 App 成功,不被其他 App 接受 |
| JWT/请求元数据 | gateway → gRPC `RequestMeta.app_code` | 所有需要租户隔离的 RPC 透传 | 下游日志和数据行均为新 `app_code` |
| 登录风控 | `auth_risk_configs``login_risk_country_blocks` | 明确复制或新建策略 | 允许国家可登录,拦截国家返回预期错误 |
## 4. 国家、区域与数据可见性
Owner`user-service`
| 配置项 | 主表 | 开启条件 | 验收标准 |
| --- | --- | --- | --- |
| 国家 | `countries` | 运营国家 `enabled=1` | `/api/v1/countries` 只返回新 App 允许的国家 |
| 区域 | `regions` | `status=active` | 后台和 App 能查到预期区域 |
| 国家归属 | `region_countries` | 每个启用国家有且只有一个有效业务区域 | 新注册用户的 `region_id` 正确 |
| 存量用户重建 | `user_region_rebuild_tasks` | 只在映射变更或补历史数据时执行 | task 完成且 `UserRegionChanged` outbox 已投递 |
不能根据服务器所在地、IP 或容器时区推断业务国家/区域。
## 5. 管理后台与 App 内容入口
Owner`server/admin`
| 配置项 | 主表 | 开启条件 | 验收标准 |
| --- | --- | --- | --- |
| App 通用配置 | `admin_app_configs` | 新 App 必须有自己的 `group + key` | H5 入口和功能跳转不读取其他 App 值 |
| Banner | `admin_app_banners` | 至少一个符合首页/区域/国家口径的 active 内容 | `/api/v1/app/banners` 在目标国家返回预期内容 |
| 弹窗 | `admin_app_popups` | 按产品需求设为 active并配置生效时间 | `/api/v1/app/popups` 可见且跳转正确 |
| 开屏 | `admin_app_splash_screens` | 按 platform/国家/区域生效 | `/api/v1/app/splash-screens` 返回预期素材 |
| 版本控制 | `admin_app_versions` | Android/iOS 当前生产版本均有记录 | `/api/v1/app/version` 返回正确版本、build 和强更策略 |
| Explore Tab | `admin_app_explore_tabs` | 显式启用并配置预期顺序 | App 探索入口与后台一致 |
内容 API 返回 `200``items=[]` 只证明接口可用,不代表运营配置已完成。
## 6. 腾讯云 IM、RTC 与播报
Owner`gateway-service``activity-service``room-service`;客户端长连属于腾讯云 IM。
| 配置项 | 配置/链路 | 开启条件 | 验收标准 |
| --- | --- | --- | --- |
| IM UserSig | `gateway-service.tencent_im` | SDKAppID、secret、admin identifier 与线上 IM 应用一致 | 新 App 真实用户能登录 IM |
| IM callback | gateway callback | callback 能从合法 GroupID/请求中确定 `app_code` | Fami/Lalu 回调不会互相落库 |
| RTC UserSig | `gateway-service.tencent_rtc` | SDKAppID、secret、room ID 类型与客户端一致 | 真实设备进房、上麦、发流成功 |
| RTC callback | `tencent_rtc.callback_url` | 不能把 `app_code` 写死为其他 App | 回调日志与房间租户一致 |
| 全局/区域播报群 | `activity-service.broadcast` | 为新 App 和所有 active region 建立群 | 群 ID 含新 `app_code`,客户端能加入 |
| 播报 outbox | `im_broadcast_outbox` | worker 必须按记录 `app_code` claim、发送和 mark | 新 App 消息从 pending 到 delivered无跨 App 更新 |
至少实测一条全局播报、一条区域播报和一条房间系统消息。
## 7. 房间配置
Owner`room-service`。Room Cell 仍然是房间核心状态 owner新 App 上线不得把房间状态复制到其他服务。
| 配置项 | 主表 | 开启条件 | 验收标准 |
| --- | --- | --- | --- |
| 麦位 | `room_seat_configs` | 有新 App 配置,否则明确接受代码默认值 | 创房后麦位数量与客户端一致 |
| 火箭 | `room_rocket_configs` | `enabled=1` 且奖励、油箱、频控已设定 | 完成加油、发射、奖励和 MQ 验证 |
| 宝箱 | `room_treasure_configs` | 产品需要时显式开启 | 开关与 App 入口一致 |
| 人机房 | `room_human_robot_configs` | 只有配好机器人账号和策略后才能开启 | 机器人加入/离开不污染真人 presence |
| 区域置顶 | `room_region_pins` | 按新 App 运营需求单独配置 | 房间变更区域后重新建立置顶 |
## 8. 礼物、资源与装扮
Owner`wallet-service`
礼物上线是一组不可拆分的配置:
| 配置项 | 主表 | 开启条件 |
| --- | --- | --- |
| 资源定义 | `resources` | 依赖资源必须 `status=active`,素材 URL 可访问 |
| 资源组 | `resource_groups``resource_group_items` | 所有活动/VIP/成就引用必须映射到新 App 资源 |
| 资源商店 | `resource_shop_items` | 价格、有效期和上架状态已审核 |
| 礼物类型 | `gift_type_configs` | 礼物分类已按产品口径启用 |
| 礼物配置 | `gift_configs` | `status=active`,生效时间正确 |
| 礼物价格 | `wallet_gift_prices` | 每个 active 礼物都有有效价格 |
| 礼物区域 | `gift_config_regions` | 地区限制和新 App 的 region 映射一致 |
| 钻石分成 | `gift_diamond_ratio_configs` | 送礼所在区域有唯一有效分成规则 |
验收必须使用新 App 真实账号执行:礼物列表 → 余额校验 → 扣款 → Room Cell 表现 → `RoomGiftSent` outbox → 统计/活动/通知下游。
## 9. 充值、支付与兑换
Owner`wallet-service`
| 配置项 | 主表/运行配置 | 开启条件 | 验收标准 |
| --- | --- | --- | --- |
| 充值商品 | `wallet_recharge_products` | 新 App 商品 active币种、金额、金币数正确 | App 返回商品和后台一致 |
| 商品区域 | `wallet_recharge_product_regions` | 用新 App `product_id``region_id` 重建 | 每个运营区域都有预期商品 |
| Google Play | `google_play` 及包名/服务账号 | 新 package 和验单凭据正确 | 测试购买、验单、发币、consume 均成功 |
| 第三方支付 | `third_party_payment_channels/methods` | 渠道、国家、币种和回调签名已配置 | 下单和回调的 `app_code` 一致 |
| USDT/币安等外部充值 | service YAML/env 的 per-App 映射 | 必须有新 App 账户/地址,不能回落到 Lalu | 创单、归集、确认数和入账正确 |
| 钻石兑换 | `wallet_diamond_exchange_rules` | active 规则已按运营口径配置 | 边界值和余额流水一致 |
| 红包 | `red_packet_configs` 及档位表 | 开关、金额、份数、过期 worker 均支持新 App | 发、抢、退、播报完整闭环 |
## 10. VIP
Owner`wallet-service`;行为唯一依据是 `vip_program_configs`
| 配置项 | 主表 | 开启条件 | 验收标准 |
| --- | --- | --- | --- |
| VIP 方案 | `vip_program_configs` | `program_type`、等级数、购买/升降级、发放模式明确 | API 返回的方案与后台一致 |
| VIP 等级 | `vip_levels` | 只有完成价格、时长和资源组配置的等级才能 active | `/api/v1/vip/packages` 返回预期等级 |
| VIP 权益 | `vip_level_benefits` | 每个 active 等级的权益和资源引用有效 | 购买/发卡后权益生效 |
| 日奖励 | cron `vip_daily_coin_rebate` | cron `app_codes` 含新 App金额和 UTC 日切口径已确认 | 当日 run、领取和钱包流水一致 |
| 进房/上线播报 | activity IM broadcast | 用户开关、VIP 级别和广播群完整 | 真实设备收到新 App 播报 |
等级处于 `disabled` 时,套餐接口返回空数组是正确保护行为,但该 App 不能因此被认为“VIP 已上线”。
## 11. 活动与奖励
Owner`activity-service`
| 能力 | 主表/配置 | 上线必须同时确认 |
| --- | --- | --- |
| 注册奖励 | `registration_reward_configs` | 开关、奖励类型、金额/资源组、每日限制 |
| 首充奖励 | `first_recharge_reward_configs/tiers` | 主开关和至少一个 active 档位 |
| 累充奖励 | `cumulative_recharge_reward_configs/tiers` | 主开关、周期口径和 active 档位 |
| 邀请奖励 | `invite_activity_reward_configs/tiers` | 邀请关系、有效邀请和充值事件消费就绪 |
| 七日签到 | `seven_day_checkin_configs/versions/rewards` | 开关、一个当前版本和 17 天完整奖励 |
| 每日任务 | `task_definitions/versions` | active 任务、周期、指标和奖励口径一致 |
| 成就 | `achievement_definitions/conditions` | active 成就引用的徽章资源属于新 App |
| 成长等级 | `growth_level_tracks/rules/tiers` | 财富/魅力/游戏轨道及展示资源完整 |
| CP 周榜 | `cp_weekly_rank_configs/rewards` | 开关、排名数、关系类型和奖励齐全 |
| 周星 | `weekly_star_cycles/gifts/rewards` | 当前 UTC 时间命中 active 周期,礼物和 Top 奖励完整 |
| 房间流水奖励 | `room_turnover_reward_configs/tiers` | 开关、周期、档位和钱包发放链路完整 |
| 转盘 | `wheel_rule_versions/prize_tiers` | 当前规则版本 active奖品和 RTP 口径已审核 |
主开关、奖励档位和奖励资源缺任何一项,该活动都不得标记上线。
## 12. 幸运礼物
Owner`lucky-gift-service``activity-service` 中的旧副本不能作为正式运行依据。
| 配置项 | 主表/链路 | 验收标准 |
| --- | --- | --- |
| 规则版本 | `lucky_gift_rule_versions` | 每个运营奖池有 active 规则 |
| 阶段档位 | `lucky_gift_stage_tiers` | 规则对应的 stage/tier 完整且 enabled |
| 独立奖池 | `lucky_pools` | 按 `app_code + pool_id` 初始化;只复制规则时不能复制对照 App 的余额、累计入池/出池或用户运行态 |
| 礼物映射 | 礼物配置与 pool 映射 | 只有新 App active 礼物才能进入抽奖 |
| owner outbox | `lucky_gift_outbox` | 抽奖事实持久化并发布到独立 MQ topic |
| 区域飘屏 | activity 独立 consumer group | 不得复用其他 consumer group且保留 `app_code` |
真实验收要覆盖普通奖、大奖、幂等重试、钱包入账、outbox 投递和区域飘屏。
## 13. 游戏
Owner`game-service`
| 配置项 | 主表 | 开启条件 |
| --- | --- | --- |
| 游戏平台 | `game_platforms` | 平台 active启动和回调参数完整 |
| 游戏目录 | `game_catalog` | 新 App 明确选择要开放的游戏,不强制与对照 App 数量相同 |
| 展示规则 | `game_display_rules` | 每个 active 游戏有可见规则 |
| 自研游戏配置 | `game_self_game_configs`、风控/新手策略 | 游戏开关、RTP、风控参数已审核 |
| 机器人 | `game_self_game_robots` | 自研对战需要补位时,必须有新 App 真实机器人用户 |
| 档位奖池 | `game_self_game_stake_pools` | 每个可玩档位有 active 奖池和初始水位 |
| 回调归属 | gateway JWT → `RequestMeta.app_code` | 厂商回调不能默认记入 Lalu |
第三方游戏少于对照 App 不一定是错误;必须由产品/运营给出明确游戏清单。但自研游戏已对用户开放时,机器人和奖池为空属于上线阻断。
## 14. cron、MQ、outbox 和通知
| 配置项 | Owner | 开启条件 | 验收标准 |
| --- | --- | --- | --- |
| cron 任务 | `cron-service` | 所有应在新 App 运行的 task `app_codes` 含新 App | `cron_task_runs``task_name + app_code` 生成成功记录 |
| owner outbox publisher | room/wallet/user/game/lucky-gift | 只有 owner 服务扫描自己的 outbox | 新 App pending 可持续转 delivered无长时间积压 |
| RocketMQ consumer group | 每个下游服务 | 不得共享 owner 的 outbox 状态或其他业务消费组 | group 存在、无异常 lag、重投幂等 |
| 统计消费 | `statistics-service` | wallet/room/user/game 事实消费器已启用 | 新 App 聚合表产生 UTC 日数据 |
| 私有通知 | `notice-service` | RocketMQ 消费器为主链路 | 钱包/CP/房间通知均使用新 App 租户 |
| 系统/活动 inbox | `activity-service` message owner | 后台通知通过 owner API/fanout job 创建 | 不直写 inbox/fanout 表,能从 App 查看并已读 |
`notice-service` 的旧轮询 worker 关闭、RocketMQ consumer 开启是正常迁移状态,不能只看一个 `enabled=false` 就判定新 App 未开通。
## 15. 统计与数据大屏
Owner`statistics-service`;后台查询入口在 `server/admin`
1. 确认 `WalletRechargeRecorded``GameOrderSettled``RoomGiftSent``RoomUserJoined``UserRegistered` 事件都带新 `app_code`
2. 确认消费幂等表已记录新 App 事件。
3. 确认充值、游戏、礼物、活跃、注册和留存聚合表在 UTC 边界正确生成。
4. 确认 admin 大屏选择新 App 时只查聚合表,不回查 owner 明细库。
5. 使用可控测试账号产生一组注册、进房、充值、游戏、送礼事实,核对次日聚合。
## 16. 客户端与发布
| 项目 | 必须验证 |
| --- | --- |
| flavor/品牌 | App 名称、图标、包名、bundle ID、深链、主题和隐私页均为新 App |
| API host | 生产包指向生产 API不得混入测试 host |
| Firebase | flavor 使用自己的 `google-services.json`/iOS 配置 |
| IM/RTC | SDKAppID、UserSig 及回调链路完整 |
| 支付 | package/product ID/签名与商店后台一致 |
| 版本 API | 首个生产 build 已写入 `admin_app_versions` |
| 埋点 | 心跳、页面、社交漏斗和游戏事件使用新 `app_code` |
必须在真实 Android/iOS 设备上执行最终验收,不能只用 curl 代替 IM、RTC、支付和客户端跳转验证。
## 17. 正式上线验收清单
### 17.1 配置一致性
- [ ] 新 App 在所有 owner 库的 `app_code` 一致。
- [ ] 所有跨表引用已按业务键重建,不存在指向对照 App ID 的记录。
- [ ] 全部敏感配置只存在运行时 env/密钥管理系统。
- [ ] 两台或多台副本的 YAML、镜像 tag 和配置 hash 一致。
### 17.2 业务冒烟
- [ ] 注册、登录、刷新 token、用户资料、国家/区域归属正常。
- [ ] IM 登录、单聊、房间群、全局/区域播报正常。
- [ ] RTC 进房、上麦、发流、回调和离开正常。
- [ ] 创房、进房、麦位、踢人、锁房、房间恢复正常。
- [ ] 礼物列表、送礼扣款、房间表现、收礼统计正常。
- [ ] 充值商品、真实测试购买、回调验单、钱包入账正常。
- [ ] VIP/活动/签到/任务/成就/周星等已宣布上线的能力全部真实验证。
- [ ] 第三方游戏启动、回调归属和余额一致;自研游戏机器人/奖池可用。
### 17.3 异步与数据
- [ ] owner outbox 无新 App 长时间 pending/failed 积压。
- [ ] RocketMQ consumer group 存在且无异常 lag。
- [ ] cron 所需 task 已产生新 App 成功运行记录。
- [ ] 通知、inbox、播报和 push 不串 App。
- [ ] 统计聚合表有新 App 真实事实UTC 边界正确。
### 17.4 发布结论
只有以下条件全部满足才能标记“可正式运营”:
1. P0 阻断项为 0。
2. 所有宣布上线的功能已完成真实设备和生产链路验证。
3. 运营明确接受未上线的可选功能,且 App 客户端不展示无配置入口。
4. 发布镜像、数据迁移、MQ、YAML/env、客户端版本和回滚方案均有记录。
## 18. Fami 生产差异快照2026-07-13
本节是对生产 YAML、生产 MySQL、线上镜像和公网 API 的只读快照,用于保留 Fami 从试运营转正式运营前的基线。后续修复后应追加新日期的验收结果,不覆盖本快照。
### 18.1 P0 阻断项
| 项目 | Fami 当前状态 | Lalu 对照 | 必须动作 |
| --- | --- | --- | --- |
| 礼物 | `gift_configs=0``wallet_gift_prices=0` | 95 个 active 礼物、103 个 active 价格 | 按业务键创建 Fami 礼物和价格,验证送礼闭环 |
| 资源 | 535 条全部 `deleted`,有效资源/资源组/商城均为 0 | 501 active 资源、56 active 资源组 | 重建 Fami 资源体系和全部引用 |
| VIP | 方案 active但 VIP1VIP10 全部 disabled | VIP1VIP8 active | 只启用已完成价格、权益和资源配置的 VIP1VIP9 |
| RTC callback | 两台 gateway 回调 URL 都写死 `app_code=lalu` | Lalu 符合当前值 | 调整回调归属,分别验证 Fami/Lalu RTC 事件 |
| IM 播报 worker | 线上 `activity-service:20260712-main-7d011ce` 仍以默认 Lalu context claim/reconcile | Lalu 已投递 71,564 条 | 发布多 App claim/建群逻辑,实测 Fami 全局和区域播报 |
| 幸运礼物 owner | `lucky-gift-service` 规则版本和档位均为 0 | 15 个规则版本、255 个档位 | 在 owner 库配置,不使用 activity 旧副本代替 |
| 首充奖励 | 开关 03 个档位全部 disabled | 开关 13 个 active | 确认奖励后启用并走真实充值事实 |
| 累充奖励 | 开关 08 个档位全部 disabled | 开关 18 个 active | 确认周期和奖励后启用 |
| 七日签到 | 开关 0无生效版本无奖励 | 开关 1有生效版本和 7 天奖励 | 创建 Fami 版本和 7 天奖励后启用 |
| CP 周榜 | 开关 0无奖励 | 开关 13 个奖励 | 确认关系类型和奖励后启用 |
| 周星 | 当前 UTC 时间无 active 周期 | Lalu 有 1 个 active 周期 | 创建 Fami 周期、指定礼物和 Top 奖励 |
| 自研游戏运行数据 | 机器人 0档位奖池 0 | dice 机器人 80dice/rock 奖池 8 | 如 Fami 对用户开放自研游戏,先初始化机器人和奖池 |
### 18.2 运营内容缺口
| 项目 | Fami 当前状态 | Lalu 对照 |
| --- | --- | --- |
| 首页 Banner | 公网 API `items=0` | 相同请求返回 10 条 |
| 弹窗 | 公网 API `items=0` | 1 条 |
| 开屏 | 公网 API `items=0` | 1 条 |
| 版本控制 | `version=""``build_number=0` | Android `1.0.9/109` |
| H5 入口 | 只有 1 条 app config缺 23 个 `h5-links` | 24 条 app config |
| 第三方游戏目录 | 34 个 active | 37 个 activeFami 少 `Rocket``TeenPattiPro``LavaSlot` |
| 成长/成就展示资源 | 6 个头像框、25 个徽章档位和 32 个成就引用资源,但 Fami 无 active 资源 | Lalu 有 active 资源 |
第三方游戏的三个差异需产品确认,不能在没有授权和产品结论时自动复制。
### 18.3 条件项
- 币安账户映射当前只有 Lalu。
- TRC20 USDT 收款地址当前只有 Lalu。
- Fami 已有 16 个 active 充值商品和对齐的第三方支付方式;上述两项只在产品要求 Fami 开放对应渠道时才是阻断。
### 18.4 已就绪项
- Fami App 注册状态为 active。
- Firebase 项目已配置。
- 16 个 cron 任务均已启用且 `app_codes` 包含 `lalu/fami/huwaa`
- Fami 和 Lalu 均有 141 个 enabled 国家,启用国家无缺口。
- Firebase、登录风控、CP 关系配置已建立。
- 红包、房间火箭、邀请奖励、注册金币奖励、房间流水奖励已开启。
- 财富/魅力/游戏成长等级的规则数量已与 Lalu 对齐,但展示资源仍受资源体系缺失阻断。
- room、wallet、user、game、statistics 的共享 MQ/outbox 主开关已开启。
### 18.5 Fami 建议收敛顺序
1. 重建 Fami 礼物、价格、资源、资源组和商城,修复成长/成就展示引用。
2. 修复 RTC callback 的 App 归属,发布 activity 多 App 播报 claim/建群逻辑。
3. 在 `lucky-gift-service` owner 库配置 Fami 规则和档位,完成独立 MQ consumer 验证。
4. 完成 VIP 价格/权益/资源后启用 VIP1VIP9。
5. 按产品范围启用首充、累充、七日签到、CP 周榜和周星。
6. 补齐 Banner、弹窗、开屏、版本和 H5 入口。
7. 确认 Fami 游戏清单;如开放自研游戏,初始化机器人和档位奖池。
8. 执行第 17 节的真实设备、业务、MQ、数据与统计验收确认 P0 为 0 后再标记可正式运营。
### 18.6 首批修复记录2026-07-13
本次只收敛 RTC/IM 业务租户隔离和幸运礼物初始化,不代表第 18.1 节其他 P0 已解除。
| 项目 | 修复结果 | 验证证据 | 状态 |
| --- | --- | --- | --- |
| RTC callback | 共享腾讯 RTC 应用不再从 URL 固定读取 `app_code=lalu`gateway 验签后按 `room_id` 调 room-service 解析真实房间租户,再提交对应 Room Cell | 两台 gateway 的 callback URL 均已改为无查询参数地址;`rooms.room_id` 无重复并已增加唯一索引gateway/room 双节点运行镜像 `20260713-fami-app-scope-1` 且 healthy | `CONFIGURED`;仍需 Fami/Lalu 各一次真实设备上麦、退流回调后升为 `VERIFIED` |
| IM 播报 worker | 同一腾讯 IM SDKAppID 下,群 ID、outbox claim、发送、mark 和 reconcile 均按记录 `app_code` 隔离;`broadcast.app_codes` 显式配置 `lalu/fami/huwaa` | 腾讯 IM 云端已存在 `hy_fami_bc_g_v2` 及区域 2430 的 7 个 `hy_fami_bc_r_v2_*`activity 双节点运行镜像 `20260713-fami-app-scope-1` 且无 reconcile 错误 | `VERIFIED`(建群与运行态);仍需客户端真实收一条区域/全局播报完成产品验收 |
| 幸运礼物规则 | 从 Lalu 复制所有规则版本和阶段档位到 Fami只变更租户键 | Fami `lucky` 为 6 个规则版本/106 个档位;`super_lucky` 为 9 个规则版本/149 个档位enabled 数量与 Lalu 一致 | `VERIFIED`(配置数据) |
| 幸运礼物奖池 | 未复制 Lalu 奖池状态Fami 的 `lucky``super_lucky` 各自独立初始化 | 两个池均为 `balance=100000``reserve_floor=21000``total_in=0``total_out=0`Fami RTP 窗口、用户状态、抽奖记录和 outbox 均为 0 | `VERIFIED`(初始化数据);仍需真实普通奖/大奖/钱包/outbox 冒烟 |
发布前已执行 `make proto``make test``docker compose config`;发布后 6 个实例均 healthy、restart count 为 0相关 CLB 后端权重均恢复为 100。

File diff suppressed because it is too large Load Diff

View File

@ -91,7 +91,7 @@ mysql_auto_migrate: false
```yaml ```yaml
jwt: jwt:
issuer: "hyapp" issuer: "hyapp"
access_token_ttl_sec: 604800 access_token_ttl_sec: 1800
refresh_token_ttl_sec: 2592000 refresh_token_ttl_sec: 2592000
signing_alg: "HS256" signing_alg: "HS256"
signing_secret: "dev-secret" signing_secret: "dev-secret"

View File

@ -1,98 +0,0 @@
# 机器人房间产品文档
## 背景
语音房冷启动阶段,房间列表和新房首屏容易显得冷清。机器人房间用于在内部可控范围内营造房间活跃氛围:机器人账号创建房间、进入房间、静音上麦,并在房间内相互送礼,触发客户端已有的礼物动效和幸运礼物展示。
机器人房间不面向真人主播发放收益,不参与真实抽奖、真实奖池、榜单和结算。
## 产品目标
- 后台可创建和控制机器人房间。
- 每个机器人房间自动拉取一批未被占用的机器人账号进房。
- 机器人可静音上麦,让房间麦位呈现活跃状态。
- 机器人之间自动送普通礼物和幸运礼物。
- 幸运礼物展示复用客户端现有飘屏/房间展示协议,但不影响真实幸运礼物奖池和流水。
- 机器人房间积分和麦位热度正常增长,用于房间内氛围展示。
- 机器人礼物不产生真人收益,不污染充值、消费、结算和排行榜数据。
## 使用入口
后台管理系统路径:
`房间管理 / 机器人房间`
权限:
- `room-robot:view`:查看机器人房间。
- `room-robot:create`:创建机器人房间。
- `room-robot:update`:启动/停止机器人房间。
## 创建机器人房间
点击 `增加机器人房间` 后配置:
- 房间名称:机器人房间对外展示名称。
- 房主机器人:从未被机器人房间占用的房间机器人账号中选择。
- 候选机器人:从未被机器人房间占用的机器人账号中选择,默认可选择全部可用机器人。
- 进房人数范围:例如 `6-10`,系统会从候选机器人中随机抽取一个人数。
- 礼物合集:普通礼物池,机器人普通送礼时随机选择。
- 幸运礼物合集:幸运礼物展示池,机器人幸运送礼时随机选择。
- 普通礼物频次:每个机器人每隔固定毫秒数送一次普通礼物。
- 幸运礼物连击范围:每轮幸运礼物随机生成连击次数。
- 幸运礼物间隔范围:一轮幸运礼物结束后,随机等待一段时间再开始下一轮。
## 送礼规则
### 普通礼物
- 每个机器人独立执行。
- 到达配置间隔后,从普通礼物合集中随机选择 1 个礼物。
- 随机选择同房间另一个机器人作为收礼人。
- 每次只送 1 个目标机器人。
### 幸运礼物
- 每个机器人独立执行。
- 从幸运礼物合集中随机选择 1 个礼物。
- 当前机器人向同房间所有其他机器人送礼。
- 按配置范围随机生成连击次数,例如 `100-10000`
- 连击完成后,按配置间隔范围随机等待,例如 `5-20 秒`,再开始下一轮。
## 状态管理
机器人房间支持:
- 运行中:机器人送礼循环运行。
- 已停止:机器人送礼循环停止,配置和房间占用保留。
后台列表中可使用状态开关快速启动或停止。
## 数据隔离
机器人房间遵循以下隔离规则:
- 机器人只给同房间机器人送礼。
- 服务端禁止机器人给真人送礼。
- 机器人礼物使用机器人专用金币 `ROBOT_COIN`
- 不消耗真人金币 `COIN`
- 不产生主播钻石和工资周期钻石。
- 不写真实礼物墙。
- 不参与真实幸运礼物奖池、抽奖、返奖和统计。
- 不参与房间排行榜、活动排行榜和结算。
## 运营建议
- 单房机器人数量建议从 `6-10` 开始。
- 普通礼物频次建议不要过低,避免房间消息过密。
- 幸运礼物连击范围建议根据活动氛围配置,测试环境可用较小范围验证。
- 礼物合集应选择低价或专门用于展示的礼物,避免用户误解真实返奖。
- 机器人房间应优先用于冷启动、活动预热和列表氛围补足。
## 风险与限制
- 机器人房间不应和真人主播收益挂钩。
- 不建议使用真人主播账号作为机器人账号。
- 如果候选机器人不足,创建会失败。
- 同一个机器人不能同时被多个 active 机器人房间占用。
- 停止房间只停止送礼循环,不删除房间配置。

Some files were not shown because too many files have changed in this diff Show More