hyapp-server/docs/房间区域置顶.md

330 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 房间置顶与房间列表排序产品开发文档
## 目标
后台在「房间管理」下增加二级菜单「房间置顶」,用于运营配置公共房间发现页的置顶顺序。
置顶只影响 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 建临时库。
- 该验证不连接线上库,不影响线上数据。