# 房内猜拳 Flutter 对接 本文只记录房内猜拳 App 端 HTTP 接口和腾讯 IM 自定义消息字段。独立猜拳本阶段不接入。 ## 1. 基本约定 - 所有 HTTP 地址都有 `/api/v1` 前缀。 - 所有接口都需要 `Authorization: Bearer `。 - 成功响应统一是 gateway envelope: ```json { "code": "OK", "message": "ok", "request_id": "req_xxx", "data": {} } ``` - 所有时间字段都是 Unix epoch milliseconds。 - Flutter 模型里用户 ID、挑战 ID、礼物 ID 建议统一按 `String` 保存。 - HTTP 返回的发起人和应战人资料在 `challenge.initiator.user`、`challenge.challenger.user`。 - IM 里的发起人和应战人也必须带 `nickname` 和 `avatar`;客户端不要为了基础展示再查用户资料。 ## 2. 通用挑战单字段 `challenge` 字段在列表、详情、发起、应战接口中结构一致。 | 字段 | 类型 | 说明 | | --- | --- | --- | | `challenge_id` | string | 挑战单 ID。 | | `room_id` | string | 房间 ID。 | | `region_id` | number | 用户语区 ID。 | | `status` | string | `pending`、`matched`、`revealing`、`finished`、`timeout`、`settlement_failed`、`cancelled`。 | | `stake_gift_id` | string | 下注礼物 ID,Flutter 优先用这个字段。 | | `stake_gift_id_number` | number | 下注礼物 ID 数字值。 | | `stake_coin` | number | 本局礼物对应金币价值。 | | `initiator` | object | 发起人。 | | `challenger` | object | 应战人;`pending` 时可能为空用户。 | | `winner_user_id` | string | 胜者用户 ID;平局或未结算为空或 `0`。 | | `winner_user_id_number` | number | 胜者用户 ID 数字值。 | | `settlement_status` | string | 结算状态,例如 `pending`、`settled`、`failed`。 | | `failure_reason` | string | 结算失败原因。 | | `timeout_at_ms` | number | 无人应战自动过期时间。 | | `reveal_at_ms` | number | 揭晓时间。 | | `created_at_ms` | number | 创建时间。 | | `matched_at_ms` | number | 应战成功时间。 | | `settled_at_ms` | number | 结算时间。 | | `updated_at_ms` | number | 更新时间。 | `initiator` 和 `challenger` 字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `user_id` | string | 用户 ID,Flutter 优先用这个字段。 | | `user_id_number` | number | 用户 ID 数字值。 | | `gesture` | string | `rock`、`paper`、`scissors`。 | | `result` | string | `win`、`lose`、`draw`;未结算时为空。 | | `balance_after` | number | 结算后的金币余额。 | | `joined_at_ms` | number | 参与时间。 | | `user.user_id` | string | 展示用户 ID。 | | `user.display_user_id` | string | 短号或展示号。 | | `user.nickname` | string | 昵称;发起人和应战人都要有。 | | `user.avatar` | string | 头像;发起人和应战人都要有。 | ## 3. 查询配置 地址: ```http GET /api/v1/games/room-rps/config ``` 参数:无。 返回值: | 字段 | 类型 | 说明 | | --- | --- | --- | | `config.status` | string | `active` 或 `disabled`。 | | `config.challenge_timeout_ms` | number | 发起后无人应战的超时时间,默认 10 分钟。 | | `config.reveal_countdown_ms` | number | 双方出拳后的揭晓倒计时,默认 3 秒。 | | `config.stake_gifts` | array | 礼物下注档位,固定 4 档。 | | `server_time_ms` | number | 服务端时间。 | `stake_gifts` 字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `gift_id` | string | 礼物 ID,Flutter 优先用这个字段提交。 | | `gift_id_number` | number | 礼物 ID 数字值。 | | `gift_name` | string | 礼物名称。 | | `gift_icon_url` | string | 礼物图标。 | | `gift_price_coin` | number | 礼物价格金币。 | | `enabled` | bool | 是否启用。 | | `sort_order` | number | 排序。 | 相关 IM:无。 ## 4. 查询房间挑战单 地址: ```http GET /api/v1/games/room-rps/challenges ``` 参数: | 字段 | 位置 | 必填 | 说明 | | --- | --- | --- | --- | | `room_id` | query | 是 | 房间 ID。 | | `status` | query | 否 | 挑战状态。 | | `page_size` | query | 否 | 页大小,默认 20。 | | `cursor` | query | 否 | 下一页游标。 | 返回值: | 字段 | 类型 | 说明 | | --- | --- | --- | | `challenges` | array | 挑战单列表,结构见通用挑战单字段。 | | `next_cursor` | string | 下一页游标。 | | `page_size` | number | 本次页大小。 | | `server_time_ms` | number | 服务端时间。 | 相关 IM:无。客户端漏掉 IM 后,用这个接口补拉当前房间挑战单。 ## 5. 发起挑战 地址: ```http POST /api/v1/games/room-rps/challenges/create Content-Type: application/json ``` 参数: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `room_id` | string | 是 | 房间 ID。 | | `gift_id` | number | 是 | 后台配置的礼物档位 ID。 | | `gesture` | string | 是 | `rock`、`paper`、`scissors`。 | 请求示例: ```json { "room_id": "room_1001", "gift_id": 10001, "gesture": "rock" } ``` 返回值: | 字段 | 类型 | 说明 | | --- | --- | --- | | `challenge` | object | 挑战单,结构见通用挑战单字段。 | | `server_time_ms` | number | 服务端时间。 | 相关 IM:`room_rps_challenge_created`。 ## 6. 应战 地址: ```http POST /api/v1/games/room-rps/challenges/{challenge_id}/accept Content-Type: application/json ``` 参数: | 字段 | 位置 | 必填 | 说明 | | --- | --- | --- | --- | | `challenge_id` | path | 是 | 挑战单 ID。 | | `gesture` | body | 是 | `rock`、`paper`、`scissors`。 | 请求示例: ```json { "gesture": "paper" } ``` 返回值: | 字段 | 类型 | 说明 | | --- | --- | --- | | `challenge` | object | 应战后的挑战单,结构见通用挑战单字段。 | | `server_time_ms` | number | 服务端时间。 | 相关 IM: - `room_rps_challenge_accepted` - `room_rps_reveal_countdown` - `room_rps_finished` ## 7. 查询挑战详情 地址: ```http GET /api/v1/games/room-rps/challenges/{challenge_id} ``` 参数: | 字段 | 位置 | 必填 | 说明 | | --- | --- | --- | --- | | `challenge_id` | path | 是 | 挑战单 ID。 | 返回值: | 字段 | 类型 | 说明 | | --- | --- | --- | | `challenge` | object | 挑战单,结构见通用挑战单字段。 | | `server_time_ms` | number | 服务端时间。 | 相关 IM:无。客户端收到 IM 后,可以用这个接口刷新挑战事实。 ## 8. 腾讯 IM 字段 房间群消息使用腾讯 IM `TIMCustomElem`,`data` 是 JSON 字符串。Flutter 只需要解析下面字段。 通用字段: | 字段 | 类型 | 说明 | | --- | --- | --- | | `event_id` | string | IM 事件 ID,客户端用它去重。 | | `event_type` | string | 事件类型。 | | `room_id` | string | 房间 ID。 | | `challenge_id` | string | 挑战单 ID。 | | `server_time_ms` | number | 服务端时间。 | | `challenge` | object | 挑战单快照,结构见通用挑战单字段。 | `challenge.initiator.user` 字段: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `user_id` | string | 是 | 发起人用户 ID。 | | `display_user_id` | string | 否 | 发起人展示号。 | | `nickname` | string | 是 | 发起人昵称。 | | `avatar` | string | 是 | 发起人头像。 | `challenge.challenger.user` 字段: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `user_id` | string | 应战后是 | 应战人用户 ID。 | | `display_user_id` | string | 否 | 应战人展示号。 | | `nickname` | string | 应战后是 | 应战人昵称。 | | `avatar` | string | 应战后是 | 应战人头像。 | IM 事件: | `event_type` | 触发时机 | Flutter 处理 | | --- | --- | --- | | `room_rps_challenge_created` | 用户发起挑战后 | 展示 `[PK]` 入口、发起人头像昵称、礼物和倒计时。此时 `challenger` 可以为空。 | | `room_rps_challenge_accepted` | 有用户应战后 | 锁定挑战入口,展示双方头像昵称。 | | `room_rps_reveal_countdown` | 双方出拳后 | 播放 3 秒揭晓动画,展示双方头像昵称。 | | `room_rps_finished` | 结算完成或平局退款后 | 展示胜负、双方头像昵称、余额或失败提示。 | IM 示例: ```json { "event_id": "room_rps_finished:rps_10001", "event_type": "room_rps_finished", "room_id": "room_1001", "challenge_id": "rps_10001", "server_time_ms": 1780000003000, "challenge": { "challenge_id": "rps_10001", "room_id": "room_1001", "status": "finished", "stake_gift_id": "10001", "stake_gift_id_number": 10001, "stake_coin": 100, "initiator": { "user_id": "10001", "user_id_number": 10001, "gesture": "rock", "result": "lose", "balance_after": 900, "joined_at_ms": 1780000000000, "user": { "user_id": "10001", "display_user_id": "880001", "nickname": "Alice", "avatar": "https://cdn.example/alice.png" } }, "challenger": { "user_id": "10002", "user_id_number": 10002, "gesture": "paper", "result": "win", "balance_after": 1200, "joined_at_ms": 1780000001000, "user": { "user_id": "10002", "display_user_id": "880002", "nickname": "Bob", "avatar": "https://cdn.example/bob.png" } }, "winner_user_id": "10002", "winner_user_id_number": 10002, "settlement_status": "settled", "timeout_at_ms": 1780000600000, "reveal_at_ms": 1780000003000, "created_at_ms": 1780000000000, "matched_at_ms": 1780000001000, "settled_at_ms": 1780000003000, "updated_at_ms": 1780000003000 } } ```