330 lines
11 KiB
Markdown
330 lines
11 KiB
Markdown
# 房间置顶与房间列表排序产品开发文档
|
||
|
||
## 目标
|
||
|
||
后台在「房间管理」下增加二级菜单「房间置顶」,用于运营配置公共房间发现页的置顶顺序。
|
||
|
||
置顶只影响 App 房间发现列表排序,不改变房间状态、不改变房间区域、不影响进房权限。
|
||
|
||
## 后台页面
|
||
|
||
菜单:房间管理 / 房间置顶
|
||
|
||
列表展示:
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| 房间信息 | 房间头像、房间名、房间长 ID、房间短 ID |
|
||
| 房主信息 | 房主头像、昵称、用户长 ID、用户短 ID |
|
||
| 区域 | 房间当前区域 |
|
||
| 置顶时间 | `pinnedAtMs` 到 `expiresAtMs` |
|
||
| 排序 | `weight`,数值越大越靠前 |
|
||
| 置顶类型 | `global` 全区置顶;`region` 区域置顶 |
|
||
| 状态 | `active` 生效中;`expired` 已过期;`cancelled` 已取消 |
|
||
|
||
新增置顶流程:
|
||
|
||
1. 输入用户短 ID 或房间 ID 搜索房间。
|
||
2. 选择置顶类型:全区置顶或区域置顶。
|
||
3. 选择置顶开始时间和结束时间。
|
||
4. 填写排序值,数值越大越靠前。
|
||
5. 点击置顶后,后台创建置顶记录。
|
||
|
||
区域规则:
|
||
|
||
- 全区置顶对所有用户国家/区域生效。
|
||
- 区域置顶只对房间当前区域生效,不需要后台单独选择区域。
|
||
- 房间后续改区域时,置顶记录不会自动改区域;后台可以取消旧置顶后重新置顶。
|
||
|
||
过期规则:
|
||
|
||
- 当前时间不在 `[pinnedAtMs, expiresAtMs)` 内时,App 列表不再把该房间当置顶房间。
|
||
- 过期后房间自动恢复普通房间排序,不需要修改房间状态。
|
||
- 后台列表读取时会把已过期的 active 记录展示为 `expired`。
|
||
|
||
## App 房间列表排序
|
||
|
||
地址:`GET /api/v1/rooms`
|
||
|
||
参数:
|
||
|
||
| 参数 | 说明 |
|
||
| --- | --- |
|
||
| `tab` | `hot` 或 `new` |
|
||
| `limit` | 每页数量 |
|
||
| `cursor` | 下一页游标 |
|
||
| `q` | 搜索关键词,可不传 |
|
||
|
||
区域来源:
|
||
|
||
- gateway 使用登录用户 ID 调 user-service 获取 `users.region_id`。
|
||
- 客户端传的区域参数不会生效。
|
||
- room-service 按该用户区域做排序分组。
|
||
|
||
`hot` 排序:
|
||
|
||
1. 全区置顶房间,多个房间按 `weight DESC`。
|
||
2. 当前用户区域的置顶房间,多个房间按 `weight DESC`。
|
||
3. 当前用户区域有人普通房,按 `hot_score DESC`。
|
||
4. 其他区域有人普通房,按 `hot_score DESC`。
|
||
5. 当前用户区域空房,按 `hot_score DESC`。
|
||
6. 其他区域空房,按 `hot_score DESC`。
|
||
|
||
`hot_score` 规则:
|
||
|
||
```text
|
||
heat_score = log2(heat + 1)
|
||
online_score = log2(min(online_count, 50) + 1)
|
||
mic_score = min(occupied_seat_count, 10)
|
||
|
||
hot_score = heat_score * 650000
|
||
+ online_score * 300000
|
||
+ mic_score * 50000
|
||
```
|
||
|
||
说明:
|
||
|
||
- `heat` 是房间贡献值。
|
||
- `online_count = 0` 的普通房进入空房分组;只要存在有人普通房,空房不会排在有人普通房前面。
|
||
- 在线人数最多按 50 人计分,防止单个房间靠人数无限放大。
|
||
- 上麦人数最多按 10 人计分,只作为活跃气氛补充。
|
||
|
||
`new` 排序:
|
||
|
||
1. 全区置顶房间,多个房间按 `weight DESC`。
|
||
2. 当前用户区域的置顶房间,多个房间按 `weight DESC`。
|
||
3. 当前用户区域的普通房间,按创建时间 `created_at_ms DESC`。
|
||
4. 其他区域的普通房间,按创建时间 `created_at_ms DESC`。
|
||
|
||
同一个排序分组内,如果排序值相同,后端使用过期时间和房间 ID 保证分页稳定。
|
||
|
||
返回值:
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "ok",
|
||
"request_id": "request-id",
|
||
"data": {
|
||
"rooms": [
|
||
{
|
||
"room_id": "room-global-pin",
|
||
"room_short_id": "163212",
|
||
"owner_user_id": "165059",
|
||
"title": "room name",
|
||
"cover_url": "https://example.com/room.png",
|
||
"visible_region_id": 2,
|
||
"heat": 1000,
|
||
"online_count": 0,
|
||
"locked": false
|
||
}
|
||
],
|
||
"next_cursor": ""
|
||
}
|
||
}
|
||
```
|
||
|
||
相关 IM:
|
||
|
||
- 无新增 IM。
|
||
- 置顶只影响 HTTP 房间列表读模型,不发送房间群消息或用户私信。
|
||
|
||
## 房间列表在线人数更新逻辑
|
||
|
||
房间列表返回的 `online_count` 来自 room-service 的业务 presence,不直接读取腾讯 IM 在线人数,也不直接读取 RTC 在线人数。
|
||
|
||
更新入口:
|
||
|
||
| 入口 | 说明 | 列表人数变化 |
|
||
| --- | --- | --- |
|
||
| `POST /api/v1/rooms/join` | 用户进房,room-service 把用户写入 Room Cell 的 `OnlineUsers` | 增加或保持不变 |
|
||
| `POST /api/v1/rooms/heartbeat` | 用户在房间内刷新 `LastSeenAtMS` | 人数不变,只延长在线有效期 |
|
||
| `POST /api/v1/rooms/leave` | 用户主动离房,room-service 从 `OnlineUsers` 删除用户 | 减少 |
|
||
| 踢人、关房、系统驱逐 | 房间命令链路清理 presence | 减少或清零 |
|
||
| stale worker | 用户断线、杀进程、没有主动离房时,由后台扫描清理超时 presence | 延迟减少 |
|
||
|
||
投影规则:
|
||
|
||
- 每次 Room Cell 命令成功提交后,room-service 用最新快照刷新 `room_list_entries`。
|
||
- `room_list_entries.online_count = len(snapshot.online_users)`。
|
||
- `room_list_entries.occupied_seat_count` 来自当前被占用麦位数。
|
||
- `hot_score` 使用 `online_count` 和 `occupied_seat_count`,所以在线人数变化会影响 hot 普通房排序。
|
||
- 列表投影是最终一致读模型;投影失败只记录日志,不回滚已经成功的房间命令。
|
||
|
||
延迟:
|
||
|
||
| 场景 | 预计延迟 |
|
||
| --- | --- |
|
||
| 正常进房 | 同一次 `JoinRoom` 命令内刷新,通常是毫秒级到几十毫秒 |
|
||
| 主动离房 | 同一次 `LeaveRoom` 命令内刷新,通常是毫秒级到几十毫秒 |
|
||
| 踢人、关房、系统驱逐 | 同一次房间命令内刷新,通常是毫秒级到几十毫秒 |
|
||
| 心跳 | 只刷新有效期,不改变列表人数 |
|
||
| App 断线或杀进程且没有主动离房 | 当前配置 `presence_stale_after = 2m`,`presence_stale_scan_interval = 30s`,通常约 2 分钟到 2 分 30 秒后列表人数减少 |
|
||
|
||
相关 IM:
|
||
|
||
- 无新增 IM。
|
||
- 在线人数是 room-service 业务 presence,不等于腾讯 IM 长连接在线人数。
|
||
- 腾讯 IM 退群、RTC 退房不会直接删除 room-service presence;业务离房只能由 `LeaveRoom`、踢人、关房、系统驱逐或 stale worker 完成。
|
||
|
||
## 后台接口
|
||
|
||
后台接口使用后台登录 token 和 `/api/v1` 前缀。
|
||
|
||
### 查询置顶列表
|
||
|
||
地址:`GET /api/v1/admin/rooms/pins`
|
||
|
||
参数:
|
||
|
||
| 参数 | 说明 |
|
||
| --- | --- |
|
||
| `page` | 页码,默认 1 |
|
||
| `page_size` | 每页数量,默认 20,最大 100 |
|
||
| `keyword` | 房间 ID、房间短 ID、房间名关键词 |
|
||
| `status` | `active`、`expired`、`cancelled`、`all`,默认 `active` |
|
||
| `pinType` | `global` 或 `region`,不传表示全部类型 |
|
||
| `regionId` | 区域筛选,不传表示全部区域 |
|
||
|
||
返回值:
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "ok",
|
||
"data": {
|
||
"items": [
|
||
{
|
||
"id": 1,
|
||
"pinType": "global",
|
||
"status": "active",
|
||
"weight": 100,
|
||
"pinnedAtMs": 1780665600000,
|
||
"expiresAtMs": 1783257600000,
|
||
"remainingMs": 2592000000,
|
||
"room": {
|
||
"roomId": "room-global-pin",
|
||
"roomShortId": "163212",
|
||
"title": "room name",
|
||
"visibleRegionId": 2,
|
||
"regionName": "南亚区"
|
||
},
|
||
"user": {
|
||
"userId": "165059",
|
||
"displayUserId": "165059",
|
||
"username": "host name"
|
||
}
|
||
}
|
||
],
|
||
"page": 1,
|
||
"pageSize": 20,
|
||
"total": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
### 搜索房间
|
||
|
||
地址:`GET /api/v1/admin/rooms`
|
||
|
||
参数:
|
||
|
||
| 参数 | 说明 |
|
||
| --- | --- |
|
||
| `keyword` | 用户短 ID、房间短 ID、房间长 ID、房间名 |
|
||
| `status` | 建议新增置顶时传 `active` |
|
||
| `page` | 页码 |
|
||
| `page_size` | 每页数量 |
|
||
|
||
说明:
|
||
|
||
- 输入用户短 ID 时,后台会先查用户,再按房主用户 ID 精确查房间。
|
||
- 输入房间 ID 时,后台直接按 room-service 房间列表查询。
|
||
|
||
### 新增置顶
|
||
|
||
地址:`POST /api/v1/admin/rooms/pins`
|
||
|
||
参数:
|
||
|
||
| 参数 | 说明 |
|
||
| --- | --- |
|
||
| `roomId` | 房间长 ID 或房间短 ID |
|
||
| `pinType` | `global` 全区置顶;`region` 区域置顶;不传默认 `region` |
|
||
| `weight` | 排序值,不能小于 0 |
|
||
| `pinnedAtMs` | 置顶开始时间,Unix 毫秒 |
|
||
| `expiresAtMs` | 置顶结束时间,Unix 毫秒 |
|
||
| `durationDays` | 旧字段兼容;新后台优先传开始和结束时间 |
|
||
|
||
请求示例:
|
||
|
||
```json
|
||
{
|
||
"roomId": "room-global-pin",
|
||
"pinType": "global",
|
||
"weight": 100,
|
||
"pinnedAtMs": 1780665600000,
|
||
"expiresAtMs": 1783257600000
|
||
}
|
||
```
|
||
|
||
返回值:返回创建后的置顶记录,结构同列表单条记录。
|
||
|
||
### 取消置顶
|
||
|
||
地址:`DELETE /api/v1/admin/rooms/pins/{pin_id}`
|
||
|
||
参数:
|
||
|
||
| 参数 | 说明 |
|
||
| --- | --- |
|
||
| `pin_id` | 置顶记录 ID |
|
||
|
||
返回值:返回取消后的置顶记录,状态为 `cancelled`。
|
||
|
||
相关 IM:
|
||
|
||
- 无新增 IM。
|
||
- 新增、取消、过期置顶都只影响发现页排序。
|
||
|
||
## 数据规则
|
||
|
||
- `room_region_pins.pin_type = global` 表示全区置顶,落库 `visible_region_id = 0`。
|
||
- `room_region_pins.pin_type = region` 表示区域置顶,落库 `visible_region_id = 房间当前 visible_region_id`。
|
||
- 有效置顶条件:`status = active` 且 `pinned_at_ms <= now_ms` 且 `expires_at_ms > now_ms`。
|
||
- 同一个房间可以同时有全区置顶和区域置顶;App 列表优先按全区置顶分组展示。
|
||
- `hot` 普通房先按本区域/其他区域和有人/空房分组,再按 `hot_score` 排序。
|
||
- `new` 普通房只按本区域/其他区域分组,再按创建时间排序。
|
||
- 置顶不进入 Room Cell、snapshot 或 command log;它是发现页运营排序读模型。
|
||
- 房间核心状态仍由 room-service 管理,后台不直接写房间库。
|
||
|
||
## 本地真实数据验证
|
||
|
||
验证环境:
|
||
|
||
- 本地 Docker MySQL:`hyapp-mysql`,端口 `23306`,状态 healthy。
|
||
- 用真实 MySQL 临时库执行 room-service 用例,不使用 memory repository。
|
||
|
||
验证命令:
|
||
|
||
```bash
|
||
ROOM_SERVICE_MYSQL_TEST_DSN='root:root@tcp(127.0.0.1:23306)/?parseTime=true&loc=UTC&multiStatements=true' \
|
||
go test ./services/room-service/internal/room/service \
|
||
-run 'TestRoomListSortScore|TestPinnedRoomListOrdersGlobalRegionLocalThenOtherFor(Hot|New)|TestRegionalPinOrdersPublicRoomList' \
|
||
-count=1 -v
|
||
```
|
||
|
||
验证结果:
|
||
|
||
- `TestRegionalPinOrdersPublicRoomList` 通过,验证区域置顶优先于同区域普通 hot 房间。
|
||
- `TestPinnedRoomListOrdersGlobalRegionLocalThenOtherForHot` 通过,验证 hot 顺序为全区置顶、当前区域置顶、当前区域有人普通房、其他区域有人普通房、当前区域空房、其他区域空房。
|
||
- `TestPinnedRoomListOrdersGlobalRegionLocalThenOtherForNew` 通过,验证 new 顺序为全区置顶、当前区域置顶、当前区域普通房、其他区域普通房。
|
||
- `TestRoomListSortScoreUsesCompressedWeightedInputs` 通过,验证 `hot_score` 使用 65%、30%、5% 的压缩权重。
|
||
- `TestRoomListSortScoreCapsPresenceInputs` 通过,验证在线人数和上麦人数有计分上限。
|
||
- 测试过程创建了真实房间数据:`room-global-pin`、`room-region-pin`、`room-local-online`、`room-other-online`、`room-local-empty`、`room-other-empty`、`room-global-new`、`room-region-new` 等。
|
||
|
||
补充说明:
|
||
|
||
- 普通 `hyapp` MySQL 用户没有创建临时库权限,所以本地真实数据验证使用 root DSN 建临时库。
|
||
- 该验证不连接线上库,不影响线上数据。
|