14 KiB
语音房房间用户列表后端技术架构设计
1. 背景
当前 room-service 已有基础能力:
GET /api/v1/rooms/{room_id}/online-users查询在线用户。POST /api/v1/rooms/admin/set设置或取消管理员。RoomAdminChangedoutbox 事件和腾讯云 IMroom_admin_changed消息。RoomGiftSentoutbox 事件和腾讯云 IMroom_gift_sent消息。
现有在线用户列表只返回 presence 信息,不包含:
- 用户在当前房间内累计送出的礼物价值。
- 用户针对当前房间的管理身份:
normal或admin。
2. 目标
本次后端开发目标:
- 在线用户列表返回所有当前在房间内的用户。
- 每个用户返回当前房间内累计送礼价值
gift_value。 - 每个用户返回是否为房主
is_owner。 - 每个用户返回当前房间管理员身份
room_role,只允许normal或admin。 - 复用同一个接口完成设置管理员和取消管理员。
- 管理员变更和送礼成功继续通过 IM 通知客户端刷新。
3. 非目标
本次不做:
- 历史房间贡献榜。
- 日榜、周榜、月榜。
- 离线预设管理员。
- 平台后台管理员权限。
- 客户端上报价格参与礼物价值计算。
- 使用
GiftRank作为完整用户礼物价值数据源。
4. 现有代码基础
4.1 Proto
文件:
api/proto/room/v1/room.proto
现有 RPC:
rpc ListRoomOnlineUsers(ListRoomOnlineUsersRequest) returns (ListRoomOnlineUsersResponse);
rpc SetRoomAdmin(SetRoomAdminRequest) returns (SetRoomAdminResponse);
现有 RoomUser.role 已用于 presence 角色,不能改成 normal/admin。
4.2 房间状态
文件:
services/room-service/internal/room/state/state.go
当前 RoomState 已包含:
| 字段 | 说明 |
|---|---|
OnlineUsers |
当前房间业务在线用户 |
AdminUsers |
当前房间管理员集合 |
GiftRank |
房间礼物榜 Top N |
注意:GiftRank 受 rank_limit 限制,不能覆盖所有用户。
4.3 设置管理员
文件:
services/room-service/internal/room/service/management.go
SetRoomAdmin 需要满足以下行为:
- 只有房主可操作。
- 目标用户必须在当前房间内。
- 房主不是管理员,不允许对房主执行设置或取消管理员。
- 管理员之间不可互相操作。
- 重复设置同一状态是 no-op。
- 状态变化时生成
RoomAdminChangedoutbox 事件。 - IM 事件类型为
room_admin_changed。
这部分服务逻辑可以复用,主要补 HTTP DTO 和文档约束。
4.4 送礼事件
文件:
services/room-service/internal/room/service/gift.go
当前 SendGift 成功后:
- 使用 wallet-service 结算结果。
- 使用
heatValue增加房间热度。 - 使用
heatValue更新本地礼物榜。 - 生成
RoomGiftSent、RoomHeatChanged、RoomRankChangedoutbox 事件。 - 对客户端只投递
room_gift_sentIM。
新增 gift_value 必须使用同一个 heatValue 口径。
5. 数据模型设计
5.1 在线来源
在线用户来源保持不变:
room_user_presence
判断条件:
app_code = ?
room_id = ?
status = active
5.2 礼物价值统计表
新增完整房间用户送礼统计表:
CREATE TABLE room_user_gift_stats (
app_code VARCHAR(64) NOT NULL,
room_id VARCHAR(128) NOT NULL,
user_id BIGINT NOT NULL,
gift_value BIGINT NOT NULL DEFAULT 0,
last_gift_at_ms BIGINT NOT NULL DEFAULT 0,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code, room_id, user_id),
KEY idx_room_user_gift_stats_room_value (
app_code,
room_id,
gift_value DESC,
last_gift_at_ms DESC,
user_id
)
);
字段说明:
| 字段 | 说明 |
|---|---|
app_code |
应用编码 |
room_id |
房间 ID |
user_id |
送礼用户 ID |
gift_value |
当前房间生命周期内累计送礼价值 |
last_gift_at_ms |
最近一次成功送礼时间 |
created_at_ms |
创建时间 |
updated_at_ms |
更新时间 |
5.3 为什么不复用 GiftRank
GiftRank 是榜单结构,不是完整统计结构:
- 受
rank_limit限制,默认只保留 Top N。 - 不包含送礼价值为
0的用户。 - 在线用户超过 Top N 时无法返回完整
gift_value。
所以在线用户列表必须读取完整统计表,并且对无统计记录的在线用户返回 gift_value = 0。
6. Proto 设计
新增在线用户展示结构,不改原 RoomUser 语义:
message RoomOnlineUser {
int64 user_id = 1;
string role = 2;
string room_role = 3;
int64 gift_value = 4;
int64 joined_at_ms = 5;
int64 last_seen_at_ms = 6;
bool is_owner = 7;
}
扩展响应:
message ListRoomOnlineUsersResponse {
repeated RoomUser users = 1;
int64 total = 2;
int32 page = 3;
int32 page_size = 4;
int64 server_time_ms = 5;
repeated RoomOnlineUser items = 6;
}
兼容策略:
users = 1保留旧客户端和旧 gateway 兼容。- 新 gateway 优先使用
items = 6。 - 不把
RoomUser.role改成normal/admin。
7. 查询链路设计
7.1 HTTP 链路
gateway-service
-> room-service ListRoomOnlineUsers
-> user-service/profile 聚合用户展示资料
-> HTTP 返回 items
HTTP 返回字段需要新增:
| 字段 | 来源 |
|---|---|
is_owner |
user_id == owner_user_id |
room_role |
room-service 基于管理员集合派生 |
gift_value |
room-service 查询统计表 |
joined_at_ms |
room_user_presence |
last_seen_at_ms |
room_user_presence |
profile |
gateway 现有聚合逻辑 |
7.2 room-service 查询流程
- 校验
room_id、viewer_user_id、分页参数。 - 校验调用方当前在该房间内。
- 读取当前房间 snapshot,拿到
owner_user_id和admin_user_ids。 - 查询
room_user_presence当前在线用户分页。 - 左连接
room_user_gift_stats获取gift_value。 - 服务层派生每个用户的
is_owner和room_role。 - 返回
items和兼容字段users。
7.3 is_owner 和 room_role 派生规则
if user_id == owner_user_id:
is_owner = true
else:
is_owner = false
if user_id in admin_user_ids:
room_role = "admin"
else:
room_role = "normal"
房主不是管理员,所以不能因为 is_owner = true 就返回 room_role = admin。
不建议把 room_role 冗余写入 room_user_presence,因为管理员身份属于房间状态,不属于 presence 状态。
如果当前代码为了兼容权限把房主写入或恢复进 AdminUsers,在线列表展示和管理员操作必须做过滤:
- 展示时
user_id == owner_user_id固定按is_owner = true返回,room_role不因房主身份变成admin。 - 设置或取消管理员时,
target_user_id == owner_user_id直接拒绝。 - 管理员集合里的 owner 不能对外表现为管理员。
7.4 排序
第一版建议保持现有在线排序,避免影响旧客户端:
ORDER BY last_seen_at_ms DESC, user_id ASC
如果要支持 sort=gift_value:
ORDER BY gift_value DESC, last_seen_at_ms DESC, user_id ASC
分页总数仍以当前在线用户数为准。
8. 写入链路设计
8.1 送礼成功累计 gift_value
在 SendGift 钱包扣款成功并准备提交房间状态时,使用结算后的 heatValue 累加:
INSERT INTO room_user_gift_stats (
app_code,
room_id,
user_id,
gift_value,
last_gift_at_ms,
created_at_ms,
updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
gift_value = gift_value + VALUES(gift_value),
last_gift_at_ms = VALUES(last_gift_at_ms),
updated_at_ms = VALUES(updated_at_ms);
写入边界:
- 只在送礼成功后写入。
- 使用
heatValue,不能使用客户端价格。 - 统计送礼用户
actor_user_id,不是收礼用户。 - 必须和现有命令幂等边界一致,重复命令不能重复累计。
8.2 幂等要求
已有房间命令链路通过 command_id 判断命令是否已应用。
新增统计写入必须满足:
- 命令第一次成功应用时累计一次。
- 同一
command_id重试时不再次累计。 - recovery replay 不能重复写统计表。
推荐做法:
- 将统计写入纳入保存 mutation 的同一事务或同一 applied 分支。
- 已 applied 的命令直接返回已有结果,不进入统计累加。
8.3 历史数据补偿
如果上线前已有房间内送礼数据,需要决定是否补偿:
- 不补偿:上线后新送礼才有
gift_value,旧房间可能显示不完整。 - 补偿:从已有
RoomGiftSentoutbox 或命令日志回放,按sender_user_id和gift_value聚合写入room_user_gift_stats。
产品如果要求老房间准确,必须执行补偿。
9. 管理员接口设计
9.1 现有 HTTP 接口
POST /api/v1/rooms/admin/set
请求:
{
"room_id": "room_10001",
"command_id": "cmd-10001",
"target_user_id": "10002",
"enabled": true
}
9.2 服务边界
保持现有 SetRoomAdmin 逻辑:
- 只有房主可设置或取消。
- 目标用户必须当前在房间内。
- 房主不是管理员,不能对房主执行设置或取消管理员。
- 管理员之间不可互相操作。
- 重复设置同一状态不生成重复 outbox 和 IM。
- 成功变更时写入
RoomAdminChangedoutbox。
9.3 Gateway DTO
建议不要直接透出完整 RoomSnapshot,新增稳定 HTTP DTO:
{
"result": {
"applied": true,
"room_version": 12
},
"target_user_id": "10002",
"room_role": "admin",
"server_time_ms": 1760000006000
}
10. IM 设计
10.1 投递形态
room-service 通过 outbox worker 向腾讯云 IM 投递 TIMCustomElem:
| Tencent 字段 | 值 |
|---|---|
MsgType |
TIMCustomElem |
MsgContent.Ext |
room_system_message |
MsgContent.Desc |
客户端事件类型,例如 room_admin_changed |
MsgContent.Data |
JSON 序列化后的 RoomEvent |
CloudCustomData |
同一份 JSON,用于服务端透传和排查 |
10.2 管理员变更
outbox 事件:
RoomAdminChanged
客户端事件:
room_admin_changed
payload:
{
"event_id": "evt-admin-10001",
"room_id": "room_10001",
"event_type": "room_admin_changed",
"actor_user_id": 10001,
"target_user_id": 10002,
"room_version": 12,
"attributes": {
"enabled": "true"
}
}
字段来源:
| 字段 | 来源 |
|---|---|
event_id |
outbox event id |
actor_user_id |
RoomAdminChanged.actor_user_id |
target_user_id |
RoomAdminChanged.target_user_id |
attributes.enabled |
RoomAdminChanged.enabled 转字符串 |
10.3 送礼成功
outbox 事件:
RoomGiftSent
客户端事件:
room_gift_sent
payload:
{
"event_id": "evt-gift-10001",
"room_id": "room_10001",
"event_type": "room_gift_sent",
"actor_user_id": 10002,
"target_user_id": 10003,
"gift_value": 5200,
"room_heat": 999999,
"room_version": 13,
"attributes": {
"gift_id": "rose",
"pool_id": "",
"gift_count": "10",
"billing_receipt_id": "receipt-10001",
"coin_spent": "5200",
"gift_point_added": "5200",
"price_version": "v1"
}
}
字段来源:
| 字段 | 来源 |
|---|---|
actor_user_id |
送礼用户 |
target_user_id |
收礼用户 |
gift_value |
本次 heatValue |
room_heat |
送礼后的房间热度 |
attributes.billing_receipt_id |
wallet-service 结算流水 |
10.4 不新增列表专用 IM
用户列表接口本身不新增 IM。
客户端刷新依赖已有事件:
| 变化 | IM |
|---|---|
| 进房 | room_user_joined |
| 离房 | room_user_left |
| 管理员变化 | room_admin_changed |
| 送礼成功 | room_gift_sent |
11. 错误边界
| 场景 | gRPC code | HTTP code |
|---|---|---|
room_id 为空 |
INVALID_ARGUMENT |
400 |
viewer_user_id 为空 |
INVALID_ARGUMENT |
400 |
| 调用方不在房间内 | PERMISSION_DENIED |
403 |
| 房间不存在 | NOT_FOUND |
404 |
| 非房主设置管理员 | PERMISSION_DENIED |
403 |
| 目标用户不在房间 | NOT_FOUND |
404 |
| 操作目标是房主 | FAILED_PRECONDITION |
409 |
page_size 超过上限 |
正常截断 | 200 |
12. 开发步骤
- 新增 MySQL migration:
room_user_gift_stats。 - 修改
api/proto/room/v1/room.proto,新增RoomOnlineUser和ListRoomOnlineUsersResponse.items。 - 重新生成 proto 代码。
- 扩展 room repository:
- 送礼统计累加方法。
- 在线用户列表查询左连接礼物统计。
- 修改
SendGift成功提交路径,累计gift_value。 - 修改
ListRoomOnlineUsersservice:- 校验 viewer 在房间内。
- 读取 snapshot 派生
is_owner和room_role。 - 返回
items。
- 修改 gateway 在线用户 DTO:
- 增加
is_owner。 - 增加
room_role。 - 增加
gift_value。 - 增加
joined_at_ms。
- 增加
- 修改 gateway 管理员设置响应 DTO,避免直接暴露完整 snapshot。
- 补充单元测试和 HTTP handler 测试。
13. 测试清单
13.1 在线用户列表
- 普通用户返回
room_role = normal。 - 房主返回
is_owner = true。 - 房主不是管理员时返回
room_role = normal。 - 被设置管理员的用户返回
room_role = admin。 - 未送礼用户返回
gift_value = 0。 - 已送礼用户返回累计
gift_value。 - 在线用户超过
rank_limit时,非 Top N 用户仍有正确gift_value。 - 不在房间内的用户查询失败。
13.2 管理员设置
- 房主设置管理员成功。
- 房主取消管理员成功。
- 非房主设置失败。
- 目标用户不在房间时失败。
- 对房主执行设置或取消管理员失败。
- 管理员之间互相设置或取消管理员失败。
- 重复设置同一状态不重复发送
RoomAdminChanged。
13.3 送礼统计
- 成功送礼累计一次。
- 钱包扣款失败不累计。
- 同一
command_id重试不重复累计。 - recovery replay 不重复累计。
13.4 IM
- 管理员变更投递
room_admin_changed。 attributes.enabled = "true"表示设置管理员。attributes.enabled = "false"表示取消管理员。- 送礼成功投递
room_gift_sent。 room_gift_sent.gift_value与统计表累加值口径一致。
建议执行:
go test ./services/room-service/... ./services/gateway-service/...