497 lines
19 KiB
Markdown
497 lines
19 KiB
Markdown
# Voice Room Region Room List Architecture
|
||
|
||
本文档定义 App 房间列表的区域化架构。目标是让同一区域内的国家看到同一套房间列表,不同区域看到不同列表,同时不破坏现有 `Room Cell` 单房间状态 owner 模型。
|
||
|
||
本文的区域隔离始终发生在单个 `app_code` 内。不同 App 即使使用相同国家、区域和 `room_id`,房间列表也必须互相不可见。
|
||
|
||
## Scope
|
||
|
||
| Capability | Owner | Required Result |
|
||
| --- | --- | --- |
|
||
| 国家和区域主数据存储 | user DB | 国家归属区域由服务端配置和计算 |
|
||
| 后台国家/区域管理 | `hyapp-admin-server` | 创建国家、启用/禁用国家、创建区域、启用/禁用区域属于后台管理后端能力 |
|
||
| App 国家/区域解析 | `user-service` | App 注册、改国家和用户投影只读 enabled 国家与 active 区域映射 |
|
||
| 用户区域归属 | `user-service` | 注册和改国家后写入 `users.region_id` |
|
||
| 房间可见区域 | `room-service` | 房间创建时绑定 `visible_region_id` |
|
||
| 房间列表读模型 | `room-service` | 按 `visible_region_id` 查询列表卡片 |
|
||
| HTTP 列表入口 | `gateway-service` | 鉴权后解析用户区域并调用 room-service |
|
||
| 实时进房校验 | `room-service` | 列表只负责发现,进入房间仍以 `JoinRoom` 为准 |
|
||
|
||
本阶段不做:
|
||
|
||
| Excluded | Reason |
|
||
| --- | --- |
|
||
| 推荐系统 | 首版先实现区域隔离和稳定排序,不做个性化推荐 |
|
||
| 跨区域混排 | 产品语义要求不同区域列表隔离 |
|
||
| 客户端提交 `region_id` | 区域必须由服务端按用户国家计算,避免伪造 |
|
||
| IP 国家决定房间列表 | `country_by_ip` 只用于审计和风控辅助,不代表用户选择国家 |
|
||
| 把列表塞进 Room Cell | Room Cell 是单房间高频状态 owner,不承担多房间列表查询 |
|
||
| 创建者改国家自动迁移房间 | 房间可见区域是创建时确定的业务属性,避免直播中列表突然漂移 |
|
||
|
||
## Core Rule
|
||
|
||
房间列表按 `region_id` 隔离,而不是按 `country` 隔离。
|
||
|
||
例如:
|
||
|
||
| Country | Region | Visible List |
|
||
| --- | --- | --- |
|
||
| `SG` | `SEA` | `room:list:region:SEA` |
|
||
| `MY` | `SEA` | `room:list:region:SEA` |
|
||
| `SA` | `MENA` | `room:list:region:MENA` |
|
||
|
||
同一区域内的国家看到相同列表;不同区域看到不同列表。
|
||
|
||
用户无区域归属时进入默认列表桶:
|
||
|
||
```text
|
||
region_id = 0
|
||
region_code = GLOBAL
|
||
```
|
||
|
||
`GLOBAL` 不是地理区域,只是兜底列表桶。后续如果产品要求“未选国家不能看列表”,gateway 可以在入口直接拒绝,不需要改 room-service 列表模型。
|
||
|
||
## Service Boundary
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
C["Client"] --> G["gateway-service"]
|
||
G --> U["user-service"]
|
||
G --> R["room-service"]
|
||
R --> DB["MySQL"]
|
||
R --> Redis["Redis"]
|
||
R --> T["Tencent Cloud IM"]
|
||
|
||
U -. "country -> region_id" .-> G
|
||
G -. "ListRooms(region_id)" .-> R
|
||
R -. "room list projection" .-> DB
|
||
R -. "hot/new sorted cache" .-> Redis
|
||
R -. "system room events" .-> T
|
||
```
|
||
|
||
边界规则:
|
||
|
||
- `hyapp-admin-server` 是后台创建/启停国家和区域的 owner,不为这些后台动作新增 user-service RPC。
|
||
- `user-service` 是 App 侧国家校验、区域解析和用户 `region_id` 投影的 owner。
|
||
- `gateway-service` 只做鉴权、用户区域解析和协议转换。
|
||
- `room-service` 是房间列表和房间状态的 owner。
|
||
- gateway 调 `user-service` 和 `room-service` 时必须透传同一个 `RequestMeta.app_code`;room-service 的房间、列表、快照、命令日志和 outbox 查询都必须带 `app_code`。
|
||
- `Room Cell` 不保存区域列表,不扫描全局房间。
|
||
- 腾讯云 IM 只负责实时消息,不参与房间列表排序和区域可见性。
|
||
|
||
## Request Flow
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as Client
|
||
participant G as gateway-service
|
||
participant U as user-service
|
||
participant R as room-service
|
||
participant S as MySQL/Redis
|
||
|
||
C->>G: GET /api/v1/rooms?tab=hot&limit=20
|
||
G->>U: GetUser(user_id)
|
||
U-->>G: region_id
|
||
G->>R: ListRooms(region_id, tab, cursor, limit)
|
||
R->>S: Read room_list_entries + active room_region_pins
|
||
S-->>R: RoomListItem[]
|
||
R-->>G: ListRoomsResponse
|
||
G-->>C: {code,message,request_id,data}
|
||
```
|
||
|
||
列表只是发现页。用户点击房间后仍然走:
|
||
|
||
```text
|
||
gateway -> room-service.JoinRoom -> Tencent IM join group callback guard
|
||
```
|
||
|
||
因此列表允许秒级最终一致;真正能否进入房间由 `JoinRoom` 和 IM 守卫决定。
|
||
|
||
区域置顶只影响当前 `visible_region_id` 的公共发现列表排序。`room_region_pins` 保存 `app_code + visible_region_id + room_id` 的运营置顶关系;查询时只 join `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`。
|
||
|
||
置顶不进入 Room Cell、command log 或 snapshot;它是房间列表读模型的运营控制面。房间改区域后,旧区域置顶不会自动跨区域生效,后台需要在新区域重新建立置顶。
|
||
|
||
## Room Region Ownership
|
||
|
||
### CreateRoom
|
||
|
||
创建房间时绑定 `visible_region_id`。
|
||
|
||
推荐链路:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as Client
|
||
participant G as gateway-service
|
||
participant U as user-service
|
||
participant R as room-service
|
||
|
||
C->>G: POST /api/v1/rooms/create(room_name, room_avatar?, room_description?)
|
||
G->>U: GetUser(actor_user_id)
|
||
U-->>G: region_id, display_user_id
|
||
G->>G: generate room_id = app_code + uuid
|
||
G->>R: CreateRoom(room_id, room_short_id=display_user_id, visible_region_id=region_id, room profile)
|
||
R->>R: Create Room Cell + room_list_entries
|
||
R-->>G: RoomSnapshot
|
||
G-->>C: room
|
||
```
|
||
|
||
约束:
|
||
|
||
- 客户端不能提交 `visible_region_id`。
|
||
- 客户端不能提交 `room_id`;gateway 按当前 `app_code` 生成 `<app_code>_<uuid>` 作为房间长 ID。
|
||
- `room_short_id` 首版直接等于创建者当前 `display_user_id`,用于搜索、分享和展示。
|
||
- 同一个登录用户只能作为 owner 创建一个房间;room-service 按 `RequestMeta.actor_user_id` 判定 owner,不能通过更换 `room_id` 创建第二个房间。
|
||
- 客户端必须提交 `room_name`;`room_avatar` 为空时由 room-service 写默认系统头像,`room_description` 只进入 `RoomExt`。
|
||
- `visible_region_id` 来自创建者当前 `users.region_id`。
|
||
- 创建者没有区域时写 `0`,进入 `GLOBAL` 列表桶。
|
||
- 房间创建成功后,创建者改国家不自动修改房间区域。
|
||
- 用户 `users.region_id` 允许变化的来源见 [User Region Change Sources](./用户区域与国家开发.md#user-region-change-sources)。无论是用户改国家、区域映射 rebuild,还是后台创建 BD Leader 导致用户区域变化,都不自动迁移已创建房间的 `visible_region_id`。
|
||
- 管理端迁移房间区域必须走独立命令,例如 `ChangeRoomVisibleRegion`,并写审计。
|
||
|
||
### Why Not Dynamic By Owner Region
|
||
|
||
不建议每次列表查询都按房主当前 `region_id` 过滤:
|
||
|
||
- 房主改国家会导致直播中房间突然从原区域消失。
|
||
- 列表查询会被迫 join user-service 或用户表,破坏 room-service 的读模型独立性。
|
||
- 历史房间的区域口径会随用户资料变化而漂移,不利于运营审计。
|
||
|
||
所以房间列表区域应该是房间属性,不是房主资料的实时派生字段。
|
||
|
||
## Data Model
|
||
|
||
### `rooms`
|
||
|
||
`rooms` 是房间恢复和基础元数据表。建议增加:
|
||
|
||
```sql
|
||
ALTER TABLE rooms
|
||
ADD COLUMN room_short_id VARCHAR(32) NOT NULL DEFAULT '' AFTER room_id,
|
||
ADD COLUMN visible_region_id BIGINT NOT NULL DEFAULT 0 AFTER status,
|
||
ADD UNIQUE KEY uk_rooms_short_id (app_code, room_short_id),
|
||
ADD KEY idx_rooms_region_status (visible_region_id, status);
|
||
```
|
||
|
||
字段语义:
|
||
|
||
| Field | Rule |
|
||
| --- | --- |
|
||
| `room_id` | 对外房间长 ID,gateway 按 `<app_code>_<uuid>` 生成 |
|
||
| `room_short_id` | 房间短 ID,首版等于创建者当前 `display_user_id` |
|
||
| `visible_region_id` | 房间列表可见区域,`0` 表示 `GLOBAL` |
|
||
| `status` | 房间生命周期状态,列表只展示 active/live 类状态 |
|
||
|
||
### `room_list_entries`
|
||
|
||
`room_list_entries` 是房间列表读模型,避免列表查询反序列化 `room_snapshots`。
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS room_list_entries (
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
room_id VARCHAR(64) NOT NULL,
|
||
room_short_id VARCHAR(32) NOT NULL DEFAULT '',
|
||
visible_region_id BIGINT NOT NULL DEFAULT 0,
|
||
owner_user_id BIGINT NOT NULL,
|
||
host_user_id BIGINT NOT NULL,
|
||
title VARCHAR(128) NOT NULL DEFAULT '',
|
||
cover_url VARCHAR(512) NOT NULL DEFAULT '',
|
||
mode VARCHAR(64) NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
locked BOOLEAN NOT NULL DEFAULT FALSE,
|
||
heat BIGINT NOT NULL DEFAULT 0,
|
||
online_count INT NOT NULL DEFAULT 0,
|
||
seat_count INT NOT NULL DEFAULT 0,
|
||
occupied_seat_count INT NOT NULL DEFAULT 0,
|
||
sort_score BIGINT NOT NULL DEFAULT 0,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY (app_code, room_id),
|
||
KEY idx_room_list_region_hot (app_code, visible_region_id, status, sort_score DESC, room_id),
|
||
KEY idx_room_list_region_new (app_code, visible_region_id, status, created_at_ms DESC, room_id),
|
||
KEY idx_room_list_owner (app_code, owner_user_id, updated_at_ms),
|
||
KEY idx_room_list_short_id (app_code, room_short_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
```
|
||
|
||
读模型字段只保存列表卡片需要的内容,不保存完整房间状态:
|
||
|
||
| Field | Source | Update Trigger |
|
||
| --- | --- | --- |
|
||
| `room_id` | CreateRoom | 创建房间 |
|
||
| `room_short_id` | owner `display_user_id` | 创建房间 |
|
||
| `visible_region_id` | creator `region_id` | 创建房间或管理端迁移 |
|
||
| `owner_user_id` | RoomMeta | 创建房间 |
|
||
| `host_user_id` | RoomState | 创建房间、TransferRoomHost |
|
||
| `title` | `RoomState.RoomExt["title"]` | 创建房间 |
|
||
| `cover_url` | `RoomState.RoomExt["cover_url"]` | 创建房间 |
|
||
| `mode` | RoomMeta/RoomState | 创建房间或模式变更 |
|
||
| `status` | RoomState | 创建、关闭、封禁、恢复 |
|
||
| `locked` | `RoomState.RoomPasswordHash != ""` | SetRoomPassword |
|
||
| `heat` | RoomState | SendGift、热度刷新 |
|
||
| `online_count` | RoomState presence | JoinRoom、LeaveRoom、stale cleanup |
|
||
| `occupied_seat_count` | RoomState mic seats | MicUp、MicDown、ChangeMicSeat |
|
||
| `sort_score` | room-service ranking policy | 任意影响排序的事件 |
|
||
|
||
## Redis Cache
|
||
|
||
MySQL 是事实来源,Redis 是列表排序和卡片缓存。
|
||
|
||
推荐 key:
|
||
|
||
```text
|
||
room:list:{visible_region_id}:hot -> ZSET(room_id, sort_score)
|
||
room:list:{visible_region_id}:new -> ZSET(room_id, created_at_ms)
|
||
room:card:{room_id} -> HASH/JSON RoomListItem
|
||
```
|
||
|
||
缓存规则:
|
||
|
||
- `CreateRoom` 写 MySQL 后写 Redis。
|
||
- `JoinRoom`、`LeaveRoom`、`SendGift`、麦位变化更新 MySQL 投影,再更新 Redis。
|
||
- Redis 写失败不回滚 Room Cell 命令。
|
||
- Redis miss 时从 MySQL 回源并修复缓存。
|
||
- Redis 中的列表成员必须定期清理非 active 房间,避免关闭房间长期残留。
|
||
|
||
首版可以先只做 MySQL 查询,等列表 QPS 上来再加 Redis ZSET。不要一开始为了缓存牺牲正确性。
|
||
|
||
## Sort Policy
|
||
|
||
首版只支持两个 tab:
|
||
|
||
| Tab | Sort | Use Case |
|
||
| --- | --- | --- |
|
||
| `hot` | `sort_score DESC, room_id ASC` | 默认热门列表 |
|
||
| `new` | `created_at_ms DESC, room_id ASC` | 最新开播列表 |
|
||
|
||
`sort_score` 建议由 room-service 计算:
|
||
|
||
```text
|
||
sort_score = heat * 1000 + online_count * 100 + occupied_seat_count * 10 + freshness_boost
|
||
```
|
||
|
||
约束:
|
||
|
||
- 排序公式只影响列表展示,不进入 RoomState。
|
||
- 排序公式调整不需要回放 Room Cell command log。
|
||
- 需要大规模重排时使用后台 rebuild job 更新 `room_list_entries.sort_score`。
|
||
|
||
## API Contract
|
||
|
||
### HTTP
|
||
|
||
```text
|
||
GET /api/v1/rooms?tab=hot&cursor=&limit=20
|
||
GET /api/v1/rooms?tab=new&cursor=&limit=20
|
||
```
|
||
|
||
响应继续使用 gateway envelope:
|
||
|
||
```json
|
||
{
|
||
"code": "OK",
|
||
"message": "ok",
|
||
"request_id": "req_xxx",
|
||
"data": {
|
||
"rooms": [
|
||
{
|
||
"room_id": "lalu_room_xxx",
|
||
"im_group_id": "lalu_room_xxx",
|
||
"title": "Live Room",
|
||
"cover_url": "https://cdn.example/room.png",
|
||
"mode": "voice",
|
||
"status": "active",
|
||
"heat": 1000,
|
||
"online_count": 26,
|
||
"seat_count": 15,
|
||
"occupied_seat_count": 2
|
||
}
|
||
],
|
||
"next_cursor": ""
|
||
}
|
||
}
|
||
```
|
||
|
||
请求规则:
|
||
|
||
- `tab` 默认 `hot`。
|
||
- `limit` 默认 `20`,最大 `50`。
|
||
- `cursor` 使用不透明字符串,客户端不能解析或拼装。
|
||
- gateway 从 access token 得到 `user_id`,再查 user-service 获取 `region_id`。
|
||
- 如果 user-service 查询失败,gateway 应返回错误,不应该用 IP 国家临时兜底。
|
||
- `im_group_id` 当前等于 `room_id`,只用于客户端准备腾讯 IM 房间群;真正入群必须等 `JoinRoom` 成功后由 IM 回调守卫校验 presence。
|
||
|
||
### gRPC
|
||
|
||
建议新增查询服务,不混入 `RoomCommandService`:
|
||
|
||
```proto
|
||
service RoomQueryService {
|
||
rpc ListRooms(ListRoomsRequest) returns (ListRoomsResponse);
|
||
}
|
||
|
||
message ListRoomsRequest {
|
||
RequestMeta meta = 1;
|
||
int64 viewer_user_id = 2;
|
||
int64 visible_region_id = 3;
|
||
string tab = 4;
|
||
string cursor = 5;
|
||
int32 limit = 6;
|
||
}
|
||
|
||
message RoomListItem {
|
||
string room_id = 1;
|
||
int64 owner_user_id = 2;
|
||
int64 host_user_id = 3;
|
||
string title = 4;
|
||
string cover_url = 5;
|
||
string mode = 6;
|
||
string status = 7;
|
||
int64 heat = 8;
|
||
int32 online_count = 9;
|
||
int32 seat_count = 10;
|
||
int32 occupied_seat_count = 11;
|
||
int64 visible_region_id = 12;
|
||
string app_code = 13;
|
||
string room_short_id = 14;
|
||
bool locked = 15;
|
||
}
|
||
|
||
message ListRoomsResponse {
|
||
repeated RoomListItem rooms = 1;
|
||
string next_cursor = 2;
|
||
}
|
||
```
|
||
|
||
兼容规则:
|
||
|
||
- 新增 proto 字段只能追加,不能复用字段号。
|
||
- `visible_region_id` 使用 `int64`,`0` 表示 `GLOBAL`。
|
||
- 不要把 `country` 传给 room-service 列表接口;国家到区域的映射不属于 room-service。
|
||
- `app_code` 只从 `RequestMeta` 来;客户端不能在列表 query/body 里自报另一个 App。
|
||
|
||
## Write Path Updates
|
||
|
||
房间命令成功后要同步更新读模型。读模型失败不能破坏 Room Cell 已提交状态,但必须可补偿。
|
||
|
||
| Command | Room Cell | List Projection |
|
||
| --- | --- | --- |
|
||
| `CreateRoom` | 创建状态、快照、outbox | 插入 `room_list_entries` |
|
||
| `JoinRoom` | 增加或刷新 presence | 更新 `online_count`、`sort_score` |
|
||
| `LeaveRoom` | 移除 presence、释放麦位 | 更新 `online_count`、`occupied_seat_count` |
|
||
| `MicUp` | 占用麦位 | 更新 `occupied_seat_count` |
|
||
| `MicDown` | 释放麦位 | 更新 `occupied_seat_count` |
|
||
| `TransferRoomHost` | 修改 host | 更新 `host_user_id` |
|
||
| `SendGift` | 更新 heat、rank | 更新 `heat`、`sort_score` |
|
||
| `CloseRoom` | 修改状态 | 从列表隐藏或标记非 active |
|
||
|
||
推荐实现:
|
||
|
||
- Room Cell 命令仍然是状态变更入口。
|
||
- 命令成功提交后调用 `RoomListProjector` 更新 MySQL 投影。
|
||
- 投影更新失败时写 `room_outbox` 或独立 `room_projection_tasks` 等待补偿。
|
||
- 列表查询只读 `room_list_entries`,不直接读取 Room Cell 内存。
|
||
- 公共列表和带 `query` 的搜索列表都从同一投影读取 `locked`;它只表达是否需要入房密码,不携带密码明文或哈希。
|
||
|
||
## Consistency Model
|
||
|
||
房间列表采用最终一致。
|
||
|
||
允许:
|
||
|
||
- 列表在线人数短暂落后。
|
||
- 房间关闭后短时间仍出现在列表。
|
||
- 热度和排序秒级延迟。
|
||
|
||
不允许:
|
||
|
||
- 用户看到其他区域专属列表。
|
||
- 客户端通过伪造 `region_id` 越权获取列表。
|
||
- 列表查询直接访问 Room Cell 全量内存。
|
||
- Redis 成为唯一数据源。
|
||
|
||
点击列表进入房间时必须再次校验:
|
||
|
||
```text
|
||
JoinRoom(room_id, user_id)
|
||
```
|
||
|
||
如果房间已关闭、用户被封禁或区域策略后续收紧,`JoinRoom` 返回拒绝,客户端刷新列表。
|
||
|
||
## Failure Handling
|
||
|
||
| Failure | Handling |
|
||
| --- | --- |
|
||
| user-service GetUser 失败 | gateway 返回错误,不用 IP 国家兜底 |
|
||
| 用户 `region_id` 为空 | 使用 `GLOBAL` 列表桶,除非产品要求强制选国家 |
|
||
| room-service Redis miss | 回源 MySQL 并修复缓存 |
|
||
| Redis 写失败 | 不回滚房间命令,记录日志或补偿任务 |
|
||
| MySQL 投影写失败 | 房间命令已提交时必须进入补偿队列 |
|
||
| 列表出现关闭房间 | `JoinRoom` 拒绝,客户端刷新列表 |
|
||
| 用户区域变化 | 包括用户改国家、区域配置 rebuild、后台创建 BD Leader 迁移用户区域;已有房间不自动迁移 |
|
||
| 区域配置变更 | 影响新用户和用户重算;已有房间不自动迁移 |
|
||
|
||
## Implementation Plan
|
||
|
||
### Phase 1: MySQL Read Model
|
||
|
||
1. `room.proto` 新增 `RoomQueryService.ListRooms` 和列表消息。
|
||
2. `CreateRoomRequest` 包含 `visible_region_id` 和房间展示资料;区域由 gateway 填充,`room_name` 由客户端必填。
|
||
3. `rooms` 增加 `visible_region_id`。
|
||
4. 新增 `room_list_entries` 表。
|
||
5. room-service 新增 `room/list` 或 `room/query` 包,封装列表 repository 和 service。
|
||
6. gateway 新增 `GET /api/v1/rooms`,先查 user-service,再调 room-service。
|
||
|
||
### Phase 2: Projection Updates
|
||
|
||
1. `CreateRoom` 插入列表投影。
|
||
2. `JoinRoom`、`LeaveRoom` 更新在线人数。
|
||
3. `MicUp`、`MicDown` 更新麦位占用。
|
||
4. `SendGift` 更新热度和排序分。
|
||
5. 增加投影补偿任务,避免命令成功但列表长期不更新。
|
||
|
||
### Phase 3: Redis Acceleration
|
||
|
||
1. 为 `hot/new` 增加 Redis ZSET。
|
||
2. 列表查询优先读 Redis room_id,再批量读 `room:card`。
|
||
3. Redis miss 或卡片缺失时回源 MySQL。
|
||
4. 增加后台 reconcile job,把 MySQL 投影定期修复到 Redis。
|
||
|
||
### Phase 4: Admin Controls
|
||
|
||
1. 新增管理端房间区域迁移命令。
|
||
2. 新增区域迁移审计表。
|
||
3. 支持按区域隐藏、封禁或置顶房间;置顶由 `room_region_pins` 按区域独立维护。
|
||
|
||
## Tests
|
||
|
||
必须覆盖:
|
||
|
||
| Case | Expected |
|
||
| --- | --- |
|
||
| `SG` 和 `MY` 属于同一区域 | 两个用户看到相同列表 |
|
||
| `SG` 和 `SA` 属于不同区域 | 两个用户看到不同列表 |
|
||
| 用户无区域 | 进入 `GLOBAL` 列表 |
|
||
| 客户端传伪造区域 | gateway 忽略客户端区域,只用 user-service 返回值 |
|
||
| 创建房间 | 写 `rooms.visible_region_id` 和 `room_list_entries` |
|
||
| 房主改国家 | 已创建房间列表区域不变 |
|
||
| Join/Leave | 列表在线人数最终更新 |
|
||
| 区域置顶 | 置顶房间排在同区域普通房间前,翻页不重复 |
|
||
| SendGift | 热度和排序分更新 |
|
||
| Redis miss | MySQL 回源成功 |
|
||
| 列表房间已关闭 | `JoinRoom` 拒绝,客户端刷新 |
|
||
|
||
## Acceptance Criteria
|
||
|
||
- App 房间列表只展示当前用户 `region_id` 对应房间。
|
||
- 同一区域内不同国家用户看到同一套列表。
|
||
- 不同区域用户不会看到彼此区域房间。
|
||
- 房间列表查询不访问 Room Cell 内存集合。
|
||
- Room Cell 状态变更仍只走命令流水线。
|
||
- MySQL 可独立恢复完整房间列表读模型。
|
||
- Redis 故障不影响创建房间、进入房间和房间状态提交。
|