# 房间背景图 Flutter 对接

本文描述 Flutter App 对接房间背景图的 HTTP 接口和腾讯云 IM 房间消息。房间背景图是房间维度素材：上传文件只拿 URL，保存接口把 URL 归档到房间素材列表，设置接口才会改变当前房间背景并广播房间 IM 消息。

## 基础地址

本地开发地址：

```text
http://127.0.0.1:13000
```

线上环境使用当前 App 配置里的 gateway base URL。下面所有路径都拼在 gateway base URL 后。

## 请求头

| Header | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `Authorization` | 是 | `Bearer <access_token>` | 登录 access token；服务端从 token 读取当前用户 ID。 |
| `X-App-Code` | 否 | `lalu` | App 编码；登录态接口最终以 token 内的 `app_code` 为准。 |
| `Content-Type` | POST JSON 必填 | `application/json` | 保存和设置背景使用 JSON。 |
| `Content-Type` | 上传必填 | `multipart/form-data` | 上传文件使用 multipart。 |

统一响应 envelope：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_abc",
  "data": {}
}
```

客户端分支判断只看 `code`，`message` 只用于兜底展示，`request_id` 要写入日志。

## 调用顺序

```text
1. 房主选择本地图片
2. POST /api/v1/files/upload 上传图片，拿 data.url
3. POST /api/v1/rooms/backgrounds/save 保存到房间背景素材列表
4. GET /api/v1/rooms/{room_id}/backgrounds 刷新背景列表
5. POST /api/v1/rooms/backgrounds/set 设置当前背景
6. 当前房间内所有用户通过 IM 收到 room_background_changed
7. 房间页用 data.room.background_url 或 IM attributes.room_background_url 刷新背景
```

## 上传图片

背景图先走通用文件上传。该接口不改变房间状态，只返回对象存储 URL。

```http
POST /api/v1/files/upload
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: multipart/form-data

file=<binary>
```

请求字段：

| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `file` | 是 | multipart file | 单文件最大 20MB。背景建议传图片文件；服务端通用入口不绑定房间语义。 |

成功响应：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_upload_bg",
  "data": {
    "url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
    "object_key": "app/files/10001/1778000000000_xxx.png",
    "content_type": "image/png",
    "size_bytes": 345678
  }
}
```

客户端只需要把 `data.url` 传给保存背景接口。

## 保存背景图

保存背景图把 URL 写入当前房间的背景素材列表。保存成功不会改变当前房间背景，也不会发房间 IM 消息。

```http
POST /api/v1/rooms/backgrounds/save
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json

{
  "room_id": "lalu_abc123",
  "image_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png"
}
```

请求字段：

| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `room_id` | 是 | string | 房间 ID。 |
| `image_url` | 是 | string | 上传接口返回的 `data.url`；服务端 trim 后最长 1024 字符。 |

成功响应：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_bg_save",
  "data": {
    "background": {
      "background_id": 101,
      "room_id": "lalu_abc123",
      "image_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
      "created_by_user_id": "10001",
      "created_at_ms": 1778000000123,
      "updated_at_ms": 1778000000123
    },
    "server_time_ms": 1778000000123
  }
}
```

字段说明：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `background.background_id` | int64 | 设置当前背景时使用。Flutter 如果统一按字符串保存 ID，也可以转成字符串保存；提交时服务端支持数字或数字字符串。 |
| `background.image_url` | string | 背景图展示 URL。 |
| `background.created_by_user_id` | string | 保存素材的用户 ID。 |
| `server_time_ms` | int64 | 服务端时间，Unix epoch milliseconds。 |

## 获取背景图列表

房主打开房间背景编辑面板时调用。

```http
GET /api/v1/rooms/{room_id}/backgrounds
Authorization: Bearer <access_token>
X-App-Code: lalu
```

路径参数：

| 参数 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `room_id` | 是 | string | 房间 ID。 |

成功响应：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_bg_list",
  "data": {
    "backgrounds": [
      {
        "background_id": 101,
        "room_id": "lalu_abc123",
        "image_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
        "created_by_user_id": "10001",
        "created_at_ms": 1778000000123,
        "updated_at_ms": 1778000000123
      }
    ],
    "selected_background_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
    "server_time_ms": 1778000000456
  }
}
```

字段说明：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `backgrounds` | array | 房间保存过的背景图，按最近保存优先。 |
| `selected_background_url` | string | 当前生效背景；没有设置过时为空或缺省。 |
| `server_time_ms` | int64 | 服务端时间。 |

## 设置当前背景

设置当前背景会改变 Room Cell 快照，返回最新房间数据，并向房间 IM 群广播 `room_background_changed`。

```http
POST /api/v1/rooms/backgrounds/set
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json

{
  "room_id": "lalu_abc123",
  "command_id": "cmd_background_lalu_abc123_10001_1778000000000",
  "background_id": "101"
}
```

请求字段：

| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `room_id` | 是 | string | 房间 ID。 |
| `command_id` | 是 | string | 房间命令幂等键；同一次点击超时重试必须复用。 |
| `background_id` | 是 | int64/string | 背景素材 ID，必须来自当前房间的背景列表。 |

成功响应：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_bg_set",
  "data": {
    "result": {
      "applied": true,
      "room_version": 18,
      "server_time_ms": 1778000000789
    },
    "room": {
      "room_id": "lalu_abc123",
      "im_group_id": "lalu_abc123",
      "room_short_id": "100001",
      "title": "Live Room",
      "cover_url": "https://cdn.example.com/room.png",
      "background_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
      "description": "welcome",
      "owner_user_id": "10001",
      "mode": "voice",
      "status": "active",
      "chat_enabled": true,
      "locked": false,
      "heat": 1000,
      "online_count": 12,
      "seat_count": 10,
      "occupied_seat_count": 2,
      "visible_region_id": 1001,
      "version": 18
    },
    "background": {
      "background_id": 101,
      "room_id": "lalu_abc123",
      "image_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
      "created_by_user_id": "10001",
      "created_at_ms": 1778000000123,
      "updated_at_ms": 1778000000123
    },
    "server_time_ms": 1778000000789
  }
}
```

字段说明：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `result.applied` | bool | `true` 表示本次命令产生状态变更；同一 `command_id` 重试可能为 `false`。 |
| `result.room_version` | int64 | 命令完成后的房间版本。 |
| `room.background_url` | string | 当前生效背景图。客户端设置成功后直接用它刷新 UI。 |
| `background` | object | 被设置的背景素材。 |

## 房间详情返回

进房或详情页重建时不需要额外查背景列表。当前生效背景在房间详情里返回：

```http
GET /api/v1/rooms/{room_id}/detail
Authorization: Bearer <access_token>
X-App-Code: lalu
```

关键字段：

```json
{
  "data": {
    "room": {
      "room_id": "lalu_abc123",
      "background_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
      "version": 18
    }
  }
}
```

客户端进入房间页时用 `data.room.background_url` 初始化背景。收到 IM 后如果 `room_version` 更大，再覆盖本地背景。

## 腾讯 IM 消息

设置背景成功后，room-service 会向房间 IM 群发送 TIMCustomElem。Flutter 需要监听群自定义消息并解析 JSON。

消息负载是 `tencentim.RoomEvent` JSON：

```json
{
  "event_id": "evt_xxx",
  "room_id": "lalu_abc123",
  "event_type": "room_background_changed",
  "actor_user_id": 10001,
  "room_version": 18,
  "attributes": {
    "background_id": "101",
    "room_background_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png"
  }
}
```

腾讯 IM 自定义消息字段：

| 字段 | 值 | 说明 |
| --- | --- | --- |
| `TIMCustomElem.data` | 上面的 JSON 字符串 | 业务负载。 |
| `TIMCustomElem.desc` | `room_background_changed` | 事件类型。 |
| `TIMCustomElem.extension` | `room_system_message` | 房间系统消息标记。 |

客户端处理规则：

| 条件 | 处理 |
| --- | --- |
| `event_type != room_background_changed` | 交给其他房间消息处理器。 |
| `room_id` 不是当前房间 | 忽略。 |
| `event_id` 已处理过 | 忽略，避免重放或 SDK 重复投递。 |
| `room_version` 小于本地房间版本 | 忽略旧消息。 |
| `room_version` 大于等于本地房间版本 | 设置房间背景为 `attributes.room_background_url`，并更新本地 `room_version`。 |

Flutter 伪代码：

```dart
void onRoomCustomMessage(String data) {
  final event = jsonDecode(data) as Map<String, dynamic>;
  if (event['event_type'] != 'room_background_changed') return;
  if (event['room_id'] != currentRoomId) return;

  final eventId = event['event_id'] as String? ?? '';
  if (handledEventIds.contains(eventId)) return;

  final version = (event['room_version'] as num?)?.toInt() ?? 0;
  if (version < localRoomVersion) return;

  final attrs = (event['attributes'] as Map?)?.cast<String, dynamic>() ?? {};
  final backgroundUrl = attrs['room_background_url'] as String? ?? '';
  handledEventIds.add(eventId);
  localRoomVersion = version;
  setRoomBackground(backgroundUrl);
}
```

## 错误处理

| code | HTTP | 常见场景 | Flutter 处理 |
| --- | --- | --- | --- |
| `INVALID_ARGUMENT` | 400 | `room_id` 格式不对、`image_url` 为空、`background_id` 为空、上传文件过大或格式不合法。 | 停止当前动作，提示重新选择图片或刷新页面后重试。 |
| `UNAUTHORIZED` | 401 | access token 缺失或过期。 | 回登录或刷新 token。 |
| `PROFILE_REQUIRED` | 403 | 用户未完成资料。 | 引导补全资料。 |
| `PERMISSION_DENIED` | 403 | 当前用户不是房主。 | 关闭编辑入口或提示无权限；不要重试。 |
| `NOT_FOUND` | 404 | 房间不存在，或设置的 `background_id` 不属于该房间。 | 刷新我的房间和背景列表。 |
| `CONFLICT` | 409 | 房间不是 active，或同一 `command_id` 携带了不同 payload。 | 刷新房间状态；如果是超时重试，确保复用同一个请求体。 |
| `UPSTREAM_ERROR` | 502 | gateway、room-service、对象存储或 IM 投递依赖临时不可用。 | 保留用户选择，提示稍后重试。 |

失败响应示例：

```json
{
  "code": "PERMISSION_DENIED",
  "message": "permission denied",
  "request_id": "req_bg_set_denied"
}
```

## UI 状态建议

| 场景 | 建议 |
| --- | --- |
| 房主打开背景设置面板 | 调 `GET /rooms/{room_id}/backgrounds`，用 `selected_background_url` 标记当前背景。 |
| 用户选择新图片 | 先本地预览，再上传，上传成功后调用保存接口。 |
| 保存成功 | 把返回的 `background` 插入列表顶部，但不切换房间背景。 |
| 点击应用背景 | 生成新的 `command_id` 调设置接口；按钮进入 loading，防止重复点击。 |
| 设置成功 | 用响应里的 `room.background_url` 立即刷新当前设备 UI。 |
| 其他用户在线 | 通过 `room_background_changed` IM 消息刷新背景。 |
| 断线重进 | 通过 `/rooms/{room_id}/detail` 的 `room.background_url` 恢复当前背景，不依赖历史 IM。 |
