# Room Entry And Reconnect Development Requirements

本文档定义下一阶段必须完成的语音房基础闭环：创建房间、进房、离房、断线重连。目标是让一个房间可以被稳定打开、进入、退出，并在客户端断线或服务短暂抖动后恢复到可继续使用的状态。

## Goal

本阶段只解决房间生命周期的最小业务闭环。

| Capability   | Required Result                                                        |
| ------------ | ---------------------------------------------------------------------- |
| 创建房间     | 用户可以创建一个 active 语音房，并获得房间快照                         |
| 进房         | 用户可以进入房间业务 presence，并获得当前房间状态                      |
| 订阅房间消息 | 用户进房成功后可以登录腾讯云 IM 并加入房间群                           |
| 进入音频频道 | `JoinRoom` 成功响应内嵌腾讯 RTC token，客户端可以立即进入 TRTC audience |
| 离房         | 用户主动退出后，房间 presence 被清理，后续进群和发言必须被业务守卫拒绝 |
| 断线重连     | 用户短暂断线后，可以重新鉴权、恢复房间状态、重新登录腾讯云 IM 并进群   |
| 防越权       | 未进房、被踢、被封禁用户不能继续进群或发公屏                           |

## Non Goals

本阶段不做这些：

| Not Included     | Reason                               |
| ---------------- | ------------------------------------ |
| 麦位完整玩法     | 放到下一阶段，避免和进房链路混在一起 |
| RTC 录制/审核    | 首版只处理进房听音频所需 token       |
| 金币和礼物       | 需要 wallet-service 先落地           |
| 推荐流和房间列表 | 先保证单房间可用                     |
| 多端复杂同步     | 首版只保证同一用户重连恢复           |
| 热门房主备热同步 | 当前仍是单房间单活 + lease 接管      |

## Ownership

| Domain            | Owner             | Rule                                       |
| ----------------- | ----------------- | ------------------------------------------ |
| HTTP 房间命令     | `gateway-service` | 只做鉴权、入参校验、协议转换               |
| 房间业务 presence | `room-service`    | 创建房间、进房、离房、封禁、恢复           |
| 实时连接和房间群  | 腾讯云 IM         | 持有客户端长连接、群成员关系、房间消息历史 |
| 身份和 token      | `user-service`    | 登录、token、用户状态                      |
| 真实语音链路      | 腾讯云 RTC        | 客户端持 JoinRoom 内嵌 token 进入音频频道  |

关键边界：

- `room-service` 不保存 socket。
- 腾讯云 IM 不拥有房间业务 presence；业务守卫仍以 `room-service` 为准。
- `gateway-service` 不信任客户端自报 `owner_user_id`、`host_user_id` 或 `actor_user_id`。
- 所有业务身份以 access token 解析出的 `user_id` 为准。
- 腾讯云 IM 登录使用 `gateway-service` 签发的 UserSig，UserSig 里的 identifier 使用不可变长 `user_id`。

## Presence Model

需要明确区分三种状态：

| State             | Owner          | Meaning                                   |
| ----------------- | -------------- | ----------------------------------------- |
| 房间业务 presence | `room-service` | 用户业务上属于这个房间                    |
| 房间群连接态      | 腾讯云 IM      | 用户当前客户端是否登录 IM、是否在房间群内 |
| RTC 在线态        | 腾讯云 RTC     | 用户是否在真实音频频道中                  |

本阶段由 room-service 维护业务 presence，通过腾讯云 IM 群承载消息，并由 gateway 在 JoinRoom 成功后签发腾讯 RTC 进房票据。RTC 在线态仍以腾讯云 RTC 为准，不写入 room-service 业务 presence。

## External API

### Create Room

```text
POST /api/v1/rooms/create
Authorization: Bearer <access_token>
```

建议请求：

```json
{
  "command_id": "cmd_create_room_550e8400",
  "mode": "voice",
  "room_name": "周末语音房",
  "room_avatar": "https://cdn.example/room.png",
  "room_description": "本房间用于轻量聊天和上麦互动"
}
```

正式客户端不应该传 `room_id`、`owner_user_id` 和 `host_user_id`。`command_id` 由客户端按创建房间动作生成，同一动作重试必须复用。gateway 必须从 access token 取当前 `user_id` 写入 `RequestMeta.actor_user_id`，并生成内部 `room_id`；room-service 使用该 actor 确定 owner 和默认 host。`seat_count` 可省略，省略或传 `0` 时使用后台默认座位数 `15`；传正数时必须命中后台启用座位数。`room_name` 和 `room_avatar` 必填，房间展示资料统一落在 `RoomExt`。

响应必须包含：

```json
{
  "result": {
    "applied": true,
    "room_version": 1,
    "server_time_ms": 1777000000000
  },
  "room": {}
}
```

### Join Room

```text
POST /api/v1/rooms/join
Authorization: Bearer <access_token>
```

请求：

```json
{
  "room_id": "room_1001",
  "command_id": "cmd_join_room_1001_10002_1",
  "role": "audience"
}
```

响应必须包含首屏房间数据、用户业务 presence、IM 房间群和 RTC token：

```json
{
  "result": {},
  "room": {
    "room_id": "room_1001",
    "im_group_id": "room_1001",
    "online_count": 26,
    "seat_count": 15,
    "version": 2
  },
  "viewer": {
    "user_id": "10001",
    "role": "audience"
  },
  "seats": [],
  "contribution_rank": [],
  "profiles": [],
  "im": {
    "group_id": "room_1001",
    "need_join_group": true
  },
  "rtc": {
    "need_token": true,
    "available": true,
    "token": {}
  },
  "server_time_ms": 1777000000000
}
```

`JoinRoom` 必须幂等。用户已经在房间时，不能重复增加在线人数或重复广播进房消息，但需要刷新 `last_seen_at_ms`。

### Leave Room

```text
POST /api/v1/rooms/leave
Authorization: Bearer <access_token>
```

请求：

```json
{
  "room_id": "room_1001",
  "command_id": "cmd_leave_room_1001_10002_1"
}
```

`LeaveRoom` 必须幂等。用户已经不在房间时，返回当前快照，不报业务错误。

### Tencent IM Login And Join Group

客户端可以在 App 启动后向 gateway 获取腾讯云 IM UserSig；但房间群必须等 `JoinRoom` 成功后再加入：

```text
GET /api/v1/im/usersig
Authorization: Bearer <access_token>
```

响应中的 `user_id` 是腾讯云 IM identifier，必须等于内部不可变长 `user_id` 的十进制字符串。

客户端随后使用腾讯云 IM SDK 登录并加入房间群：

```json
{
  "sdk_app_id": 1400000000,
  "user_id": "10001",
  "user_sig": "usersig_xxx",
  "room_id": "room_1001",
  "im_group_id": "room_1001"
}
```

进群前业务守卫仍然需要存在。首版可以由客户端先 `JoinRoom` 再进群；线上必须补腾讯云 IM 回调或 gateway 代理校验：

```text
room-service.VerifyRoomPresence(room_id, user_id, session_id)
```

只有 `present=true` 才允许进群或继续接收房间业务消息。被踢或封禁后，`room-service` 负责写 outbox 并通过腾讯云 IM 推系统事件，客户端必须按事件退房或触发重新同步。

## Required Internal Changes

当前基础接口已经存在，但本阶段需要补齐这些行为。

### gateway-service

| Requirement      | Detail                                                                                                                                                                       |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 创建房间身份收敛 | `CreateRoom` 的 owner/host 必须由 room-service 从 `RequestMeta.actor_user_id` 收敛，不接受客户端 body 自报                                                                   |
| command_id       | 写命令请求体使用客户端按用户动作生成的稳定 `command_id`，用于 room-service command log 幂等 |
| 统一错误映射     | 房间不存在、被踢、无权限、重复命令要映射成稳定 HTTP envelope                                                                                                                 |
| 请求日志         | 记录 `request_id`、`user_id`、`room_id`、command type                                                                                                                        |

### room-service

| Requirement        | Detail                                                                                         |
| ------------------ | ---------------------------------------------------------------------------------------------- |
| CreateRoom         | 创建 meta、command log、snapshot、outbox，并安装 Room Cell                                     |
| 单 owner 单房间    | 同一个 `RequestMeta.actor_user_id` 只能作为 owner 创建一个房间；已有 owner 房间时返回 Conflict |
| JoinRoom 幂等      | 已在房间时刷新 `last_seen_at_ms`，不重复广播                                                   |
| LeaveRoom 幂等     | 不在房间时返回当前快照                                                                         |
| VerifyRoomPresence | 腾讯云 IM 回调或 gateway 代理进群前检查房间存在、用户在房间、未被 ban                          |
| Presence 过期      | 支持断线宽限期，超过 TTL 后自动清理                                                            |
| Recovery           | 节点重启后能通过 snapshot + command log 恢复房间                                               |

建议新增配置：

```yaml
room_presence_reconnect_grace: "60s"
room_presence_stale_timeout: "300s"
room_presence_sweep_interval: "30s"
```

### Tencent Cloud IM Integration

| Requirement      | Detail                                                                                              |
| ---------------- | --------------------------------------------------------------------------------------------------- |
| UserSig          | `gateway-service` 只给已鉴权用户签发腾讯云 IM UserSig                                               |
| EnsureRoomGroup  | `room-service.CreateRoom` 必须确保腾讯云 IM 群组存在                                                |
| PublishRoomEvent | 房间状态变更后同步推腾讯云 IM 群系统消息，失败不回滚房间状态                                        |
| Guard Callback   | 线上需要腾讯云 IM 进群/发言回调或等价 gateway 代理调用 `VerifyRoomPresence`、`CheckSpeakPermission` |
| History          | 房间最近消息优先使用腾讯云 IM 群历史；本仓库不再保存自研 IM 时间线                                  |

## Reconnect Design

断线分三种。

| Case               | Expected Behavior                                                                      |
| ------------------ | -------------------------------------------------------------------------------------- |
| IM 短断            | room-service presence 保留，客户端重新登录腾讯云 IM 后重新进群                         |
| App 退后台后恢复   | 客户端先调用 `RoomHeartbeat` 刷新已有 presence；如果返回 `NOT_FOUND` 再重新 `JoinRoom` |
| 超过 stale timeout | room-service 自动清理 presence，客户端需要重新进房                                     |

客户端重连标准流程：

```text
1. 如果 access token 过期，先 RefreshToken
2. 调 POST /api/v1/rooms/heartbeat，刷新房间业务 presence
3. 调 POST /api/v1/im/usersig，拿腾讯云 IM UserSig
4. 使用腾讯云 IM SDK 登录
5. 加入 JoinRoom 返回的 `im.group_id` 对应腾讯云 IM 群
6. 拉取腾讯云 IM 群历史或等待房间系统消息补齐
7. 如果需要音频且旧 RTC token 已过期，调用 `/api/v1/rtc/token` 补签
8. 用 RoomHeartbeat 返回的 RoomSnapshot 覆盖本地房间状态

如果第 2 步返回 `NOT_FOUND`，说明业务 presence 已不存在，客户端必须走完整 `JoinRoom -> IM/RTC 重进` 流程。
```

服务端原则：

- 断线不能立刻等同于离房。
- 主动 `LeaveRoom` 才是明确离房。
- 超时清理是兜底，不能依赖客户端一定发离房。
- 被踢或封禁后，即使客户端仍在腾讯云 IM 群内，也必须通过回调守卫、系统事件和客户端同步阻断继续发言或展示在房间内。

## Data Requirements

`RoomUserState` 至少需要支持：

| Field             | Meaning                                 |
| ----------------- | --------------------------------------- |
| `user_id`         | 房间内业务用户                          |
| `role`            | owner, host, admin, audience            |
| `joined_at_ms`    | 首次进入当前房间时间                    |
| `last_seen_at_ms` | 最近一次 JoinRoom 或 RoomHeartbeat 时间 |

建议后续追加：

| Field                   | Meaning                     |
| ----------------------- | --------------------------- |
| `connection_state`      | online, reconnecting, stale |
| `reconnect_deadline_ms` | 断线宽限截止时间            |

首版可以先不把 `connection_state` 暴露给客户端，但内部清理逻辑要能基于 `last_seen_at_ms` 判断 stale。

## Event Requirements

房间状态成功变更后，`room-service` 必须写 outbox。

| Action         | Outbox Event     | IM System Event                          |
| -------------- | ---------------- | ---------------------------------------- |
| 创建房间       | `RoomCreated`    | 可不广播                                 |
| 进房成功       | `RoomUserJoined` | `room_user_joined`                       |
| 离房成功       | `RoomUserLeft`   | `room_user_left`                         |
| stale 自动离房 | `RoomUserLeft`   | `room_user_left`，reason=`stale_timeout` |
| 被踢           | `RoomUserKicked` | `room_user_kicked`                       |

同步 `PublishRoomEvent` 失败不能回滚房间状态，仍由 room outbox 补投到腾讯云 IM 和后续活动系统。

## Acceptance Tests

必须覆盖这些测试。

| Test                       | Expected Result                                                   |
| -------------------------- | ----------------------------------------------------------------- |
| 创建房间成功               | 返回 version=1，owner 在 online_users 内                          |
| 创建房间请求带客户端 ID    | gateway 忽略 `room_id`，使用客户端 `command_id` 并生成内部房间 ID |
| 普通用户 JoinRoom          | presence 增加，广播进房事件                                       |
| 重复 JoinRoom              | 不重复广播，不重复增加人数，刷新 last_seen                        |
| LeaveRoom                  | presence 移除，进群/发言守卫拒绝后续实时消息权限                  |
| 重复 LeaveRoom             | 幂等返回，不报错                                                  |
| JoinRoom 后进群            | `VerifyRoomPresence` 通过，腾讯云 IM 群收到房间消息               |
| 未 JoinRoom 直接进群或发言 | `VerifyRoomPresence` 或 `CheckSpeakPermission` 返回 `not_in_room` |
| Kick 后进群或发言          | 返回 `user_banned`                                                |
| IM 断开后 60 秒内重连      | RoomHeartbeat 刷新，腾讯云 IM 重新登录和进群成功，历史可拉        |
| 超过 stale timeout         | presence 自动清理，需要重新 JoinRoom                              |
| room-service 重启恢复      | snapshot + command log 恢复后 JoinRoom/subscribe 继续可用         |

## Implementation Order

建议按这个顺序做：

1. 修正创建房间身份来源：gateway 不接收 body 的 owner/host，room-service 从 `RequestMeta.actor_user_id` 收敛 owner/default host。
2. 补齐 JoinRoom 幂等刷新 `last_seen_at_ms`。
3. 增加显式 RoomHeartbeat，只刷新已有 presence，不隐式进房。
4. 补齐 LeaveRoom 后对订阅守卫的拒绝行为测试。
5. 接入腾讯云 IM UserSig 签发、CreateRoom 建群和房间系统消息发布。
6. 增加 stale presence 清理 worker。
7. 增加重连集成测试：JoinRoom -> UserSig -> IM login/join group -> disconnect -> heartbeat/reconnect -> history。
8. 增加 room-service 重启恢复测试。
