# 用户举报 Flutter 对接

本文描述 Flutter App 对接用户举报和房间举报的 HTTP 契约。通用文件上传接口用于上传举报图片并拿到 URL，提交举报接口只接收 JSON。

## 基础地址

本地开发地址：

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

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

## 请求头

| Header | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `Authorization` | 是 | `Bearer <access_token>` | 登录 access token；举报发起人只从 token 读取，Flutter 不传举报发起人 ID。 |
| `X-App-Code` | 否 | `lalu` | App 编码；登录态接口最终以 token 内的 `app_code` 为准。 |
| `Content-Type` | 提交举报必填 | `application/json` | 提交举报使用 JSON。 |
| `Content-Type` | 上传图片必填 | `multipart/form-data` | 上传举报图片使用 multipart。 |

统一响应 envelope：

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

Flutter 只在 `code == "OK"` 时读取 `data`。失败时记录 `request_id`，用于后端排查。

## 调用顺序

```text
1. 用户在举报弹窗选择举报类型。
2. 如有举报图，逐张调用 POST /api/v1/files/upload，收集每次返回的 data.url。
3. 调用 POST /api/v1/reports 提交举报。
4. 提交成功后关闭弹窗，并用 data.report_id 写本地日志。
```

## 上传举报图片

上传接口只负责把文件写入对象存储并返回 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。多图逐张上传，Flutter 把每张图返回的 `data.url` 放进提交举报接口的 `image_urls`。 |

成功响应：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_upload_report_image",
  "data": {
    "url": "https://cdn.example.com/app/files/10001/20260529/file_xxx.jpg",
    "object_key": "app/files/10001/20260529/file_xxx.jpg",
    "content_type": "image/jpeg",
    "size_bytes": 345678
  }
}
```

Flutter 提交举报时只需要使用 `data.url`。

## 提交举报

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

{
  "user_id": "10002",
  "report_type": "pornography",
  "reason": "用户在房间内发布违规内容",
  "image_urls": [
    "https://cdn.example.com/app/files/10001/20260529/file_xxx.jpg"
  ]
}
```

举报房间示例：

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

{
  "room_id": "lalu_abc123",
  "report_type": "fraud",
  "reason": "房间引导线下交易",
  "image_urls": []
}
```

请求字段：

| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `user_id` | 条件必填 | string | 被举报用户 ID。和 `room_id` 二选一，只能传一个。 |
| `room_id` | 条件必填 | string | 被举报房间 ID。和 `user_id` 二选一，只能传一个。 |
| `report_type` | 是 | string | 举报类型，取值见下方枚举。 |
| `reason` | 否 | string | 举报理由；Flutter 可允许用户不填。 |
| `image_urls` | 否 | string[] | 举报图 URL 数组；无图可传 `[]` 或不传。 |

目标校验规则：

| 场景 | 结果 |
| --- | --- |
| 只传 `user_id` | 举报用户。 |
| 只传 `room_id` | 举报房间。 |
| `user_id` 和 `room_id` 都不传 | `400 INVALID_ARGUMENT`。 |
| `user_id` 和 `room_id` 同时传 | `400 INVALID_ARGUMENT`，Flutter 需要拆成一次明确的用户举报或房间举报。 |

举报发起人不在请求体里传。gateway 必须从 `Authorization` 对应的 access token 读取当前登录用户，避免客户端伪造举报发起人。

## 举报类型

| UI 文案 | `report_type` |
| --- | --- |
| Politics | `politics` |
| Pornography | `pornography` |
| Graphic Violence | `graphic_violence` |
| Verbal Abuse | `verbal_abuse` |
| Fraud | `fraud` |
| Illegal Content | `illegal_content` |
| Minors | `minors` |
| In-Person Transactions | `in_person_transactions` |
| Other | `other` |

## 返回体模拟数据

举报用户成功：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_report_user",
  "data": {
    "report_id": "rpt_10001_1778000000000",
    "target_type": "user",
    "user_id": "10002",
    "room_id": "",
    "report_type": "pornography",
    "status": "submitted",
    "created_at_ms": 1778000000000
  }
}
```

举报房间成功：

```json
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_report_room",
  "data": {
    "report_id": "rpt_10001_1778000001000",
    "target_type": "room",
    "user_id": "",
    "room_id": "lalu_abc123",
    "report_type": "fraud",
    "status": "submitted",
    "created_at_ms": 1778000001000
  }
}
```

字段说明：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `report_id` | string | 举报单 ID；Flutter 可用于本地日志和客服排查。 |
| `target_type` | string | `user` 或 `room`，由服务端按本次传入目标生成。 |
| `user_id` | string | 被举报用户 ID；房间举报返回空字符串。 |
| `room_id` | string | 被举报房间 ID；用户举报返回空字符串。 |
| `report_type` | string | 本次提交的举报类型。 |
| `status` | string | 首次提交固定返回 `submitted`。 |
| `created_at_ms` | int64 | 举报创建时间，Unix epoch milliseconds。 |

## 错误处理

失败返回仍使用统一 envelope：

```json
{
  "code": "INVALID_ARGUMENT",
  "message": "invalid argument",
  "request_id": "req_abc"
}
```

| HTTP 状态码 | `code` | Flutter 处理 |
| --- | --- | --- |
| `400` | `INVALID_JSON` | JSON 格式错误，检查本地序列化逻辑。 |
| `400` | `INVALID_ARGUMENT` | `user_id/room_id` 二选一不满足、`report_type` 非法、图片 URL 格式非法或字段超长。 |
| `401` | `UNAUTHORIZED` | token 缺失、无效、过期或会话失效，重新登录。 |
| `403` | `PROFILE_REQUIRED` | 用户资料未完成，跳转资料补全流程。 |
| `404` | `NOT_FOUND` | 被举报用户不存在，刷新页面状态。 |
| `502` | `UPSTREAM_ERROR` | 内部服务暂不可用，可提示稍后重试。 |

## Flutter 数据模型建议

```dart
class SubmitReportRequest {
  SubmitReportRequest({
    this.userId,
    this.roomId,
    required this.reportType,
    this.reason,
    this.imageUrls = const [],
  });

  final String? userId;
  final String? roomId;
  final String reportType;
  final String? reason;
  final List<String> imageUrls;

  Map<String, dynamic> toJson() => {
        if (userId != null && userId!.isNotEmpty) 'user_id': userId,
        if (roomId != null && roomId!.isNotEmpty) 'room_id': roomId,
        'report_type': reportType,
        if (reason != null && reason!.trim().isNotEmpty) 'reason': reason!.trim(),
        if (imageUrls.isNotEmpty) 'image_urls': imageUrls,
      };
}
```

提交前本地校验：

```dart
void validateReportTarget({String? userId, String? roomId}) {
  final hasUser = userId != null && userId.trim().isNotEmpty;
  final hasRoom = roomId != null && roomId.trim().isNotEmpty;
  if (hasUser == hasRoom) {
    throw ArgumentError('user_id and room_id must be exactly one');
  }
}
```

## 实现边界

- Flutter 不传举报发起人 ID，服务端只认 token 中的当前用户。
- `user_id` 表示被举报用户，不是举报发起人。
- `room_id` 表示被举报房间，不要求同时传房间内某个用户。
- `user_id` 和 `room_id` 必须二选一，不能同时提交一个混合举报。
- 举报图片先上传再提交 URL；提交举报接口不接收 multipart 文件。
- `message` 不作为业务分支依据，Flutter 只按 `code` 和 HTTP 状态处理。
