hyapp-server/docs/flutter对接/房内猜拳Flutter对接.md
2026-07-16 18:51:58 +08:00

334 lines
9.8 KiB
Markdown
Raw Permalink 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.

# 房内猜拳 Flutter 对接
本文只记录房内猜拳 App 端 HTTP 接口和腾讯 IM 自定义消息字段。独立猜拳本阶段不接入。
## 1. 基本约定
- 所有 HTTP 地址都有 `/api/v1` 前缀。
- 所有接口都需要 `Authorization: Bearer <access_token>`
- 成功响应统一是 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 | 下注礼物 IDFlutter 优先用这个字段。 |
| `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 | 用户 IDFlutter 优先用这个字段。 |
| `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 | 礼物 IDFlutter 优先用这个字段提交。 |
| `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`
当挑战到达 `timeout_at_ms` 后,应战接口返回 HTTP `409`
```json
{
"code": "ROOM_RPS_CHALLENGE_EXPIRED",
"message": "猜拳已过期",
"request_id": "req_xxx"
}
```
Flutter 必须按 `code` 识别该状态,关闭应战弹窗并提示“猜拳已过期”;不要依赖 `message` 判断业务类型。
## 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
}
}
```