hyapp-server/docs/flutter对接/CP关系Flutter对接.md
2026-06-08 19:39:22 +08:00

523 lines
14 KiB
Markdown
Raw 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.

# CP关系Flutter对接
本文只写 App 需要对接的地址、参数、返回值和相关 IM。CP 关系包含 `cp``brother``sister` 三种类型CP 关系每个用户最多 1 个,兄弟和姐妹数量由后台 CP 配置控制;不需要佩戴接口。
## 1. 礼物面板CP Tab
地址:
```http
GET /api/v1/rooms/{room_id}/gift-panel
Authorization: Bearer <access_token>
X-App-Code: lalu
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID |
返回值:
`tabs` 里会有 `gift_type_code=cp` 的 Tab。CP 礼物会带:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `gift_id` | string | 送礼时使用 |
| `gift_type_code` | string | CP 礼物为 `cp` |
| `cp_relation_type` | string | `cp``brother``sister` |
| `name` | string | 礼物名 |
| `coin_price` | int64 | 展示价格,扣费以后端为准 |
| `resource.asset_url` | string | 礼物 icon |
| `resource.animation_url` | string | 礼物动效,可为空 |
Flutter 在 CP 礼物 Tab 下按 `cp_relation_type` 分成 CP、兄弟、姐妹三个二级 Tab。
## 2. 发送CP礼物
地址:
```http
POST /api/v1/rooms/gift/send
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `room_id` | string | 是 | 当前房间 ID |
| `command_id` | string | 是 | 本次送礼幂等键,重试必须复用 |
| `target_type` | string | 否 | 当前传 `user` |
| `target_user_ids` | int64[] | 是 | CP 礼物只传 1 个收礼人 |
| `gift_id` | string | 是 | CP 礼物 ID |
| `gift_count` | int32 | 是 | 必须大于 0 |
示例:
```json
{
"room_id": "room_1001",
"command_id": "cp_gift_1779259000000_10001_10002",
"target_type": "user",
"target_user_ids": [10002],
"gift_id": "cp_ring",
"gift_count": 1
}
```
返回值:
沿用普通送礼响应。关系申请通过 IM 到达,送礼响应不保证返回申请 ID。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `result.applied` | bool | 本次命令是否落地 |
| `billing_receipt_id` | string | 扣费回执 |
| `room_heat` | int64 | 最新房间热度 |
| `gift_rank` | array | 房间礼物榜 |
| `room` | object | 房间快照 |
| `lucky_gift` | object | 幸运礼物结果,普通 CP 礼物为空 |
## 3. 查询我的申请
地址:
```http
GET /api/v1/cp/applications?direction=incoming&status=pending&page=1&page_size=20
Authorization: Bearer <access_token>
X-App-Code: lalu
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `direction` | string | 否 | `incoming` 我收到的;`outgoing` 我发出的 |
| `status` | string | 否 | `pending``accepted``rejected``expired``blocked` |
| `page` | int32 | 否 | 默认 1 |
| `page_size` | int32 | 否 | 默认 20 |
返回值:
```json
{
"applications": [
{
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"status": "pending",
"requester": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"target": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"room_id": "room_1001",
"room_region_id": 86,
"gift": {
"gift_id": "cp_ring",
"gift_name": "CP Ring",
"gift_icon_url": "https://cdn.example/cp-ring.png",
"gift_animation_url": "",
"gift_count": 1,
"gift_value": 520,
"billing_receipt_id": "gift_tx_1"
},
"created_at_ms": 1779259000000,
"updated_at_ms": 1779259000000,
"expires_at_ms": 1779345400000,
"decided_at_ms": 0
}
],
"total": 1,
"server_time_ms": 1779259001000
}
```
## 4. 同意申请
地址:
```http
POST /api/v1/cp/applications/{application_id}/accept
Authorization: Bearer <access_token>
X-App-Code: lalu
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `application_id` | string | 是 | 申请 ID |
返回值:
```json
{
"application": {
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"status": "accepted",
"decided_at_ms": 1779259100000
},
"relationship": {
"relationship_id": "cp_rel_10001_10002",
"relation_type": "cp",
"status": "active",
"me": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"partner": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"intimacy_value": 0,
"level": 1,
"formed_at_ms": 1779259100000,
"updated_at_ms": 1779259100000
}
}
```
错误处理:
| code | 处理 |
| --- | --- |
| `CONFLICT` | A/B 任一方已经有 active 关系,或申请已过期;刷新申请和关系列表 |
| `NOT_FOUND` | 申请不存在,刷新申请列表 |
| `PERMISSION_DENIED` | 当前用户不是申请目标用户 |
## 5. 拒绝申请
地址:
```http
POST /api/v1/cp/applications/{application_id}/reject
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `application_id` | string | 是 | 申请 ID |
| `reason` | string | 否 | 拒绝原因 |
返回值:
```json
{
"application": {
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"status": "rejected",
"decided_at_ms": 1779259100000
}
}
```
## 6. 查询我的关系
地址:
```http
GET /api/v1/cp/relationships?relation_type=cp&page=1&page_size=20
Authorization: Bearer <access_token>
X-App-Code: lalu
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `relation_type` | string | 否 | `cp``brother``sister`;不传返回全部 |
| `page` | int32 | 否 | 默认 1 |
| `page_size` | int32 | 否 | 默认 20 |
返回值:
```json
{
"relationships": [
{
"relationship_id": "cp_rel_10001_10002",
"relation_type": "cp",
"status": "active",
"me": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"partner": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"intimacy_value": 1888,
"level": 2,
"current_level_threshold": 1000,
"next_level_threshold": 3000,
"needed_for_next_level": 1112,
"level_progress_percent": 44,
"max_level": false,
"breakup_cost_coins": 88,
"formed_at_ms": 1779259100000,
"ended_at_ms": 0,
"updated_at_ms": 1779259200000
}
],
"total": 1
}
```
说明:
- CP 关系每个用户最多 1 个;兄弟和姐妹可拥有数量以后台配置为准。
- A/B 两个用户之间同一时间只能存在一种 active 关系,不能同时是 CP、兄弟或姐妹。
- 不需要佩戴接口,资料卡直接展示当前 active 关系。
- 资料卡弹窗只使用后端返回的 `intimacy_value``level``current_level_threshold``next_level_threshold``needed_for_next_level``level_progress_percent``max_level`
- `max_level=true``next_level_threshold=0` 时表示没有下一等级,隐藏“距离下一级”和进度条。
- Flutter 不要用本地常量、礼物价格或后台配置接口自行推算进度条和“距离下一级”。
- `breakup_cost_coins` 是解除当前关系需要支付的金币数,弹窗和二次确认直接使用该值。
## 7. 解除关系
地址:
```http
POST /api/v1/cp/relationships/{relationship_id}/break
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
{
"command_id": "break_cp_rel_10001_10002_1779259300000"
}
```
参数:
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `relationship_id` | string | 是 | URL path关系 ID |
| `command_id` | string | 是 | 解除关系幂等键,同一次重试必须复用 |
返回值:
```json
{
"relationship": {
"relationship_id": "cp_rel_10001_10002",
"relation_type": "cp",
"status": "ended",
"breakup_cost_coins": 88,
"formed_at_ms": 1779259100000,
"ended_at_ms": 1779259300000,
"updated_at_ms": 1779259300000
},
"breakup_cost_coins": 88,
"wallet_transaction_id": "wallet_tx_break_1",
"coin_balance_after": 912
}
```
说明:
- 解除前先展示 `/api/v1/cp/relationships` 返回的 `breakup_cost_coins`
- 金币不足时接口返回 `INSUFFICIENT_BALANCE`,关系仍保持 active。
- 解除成功不发 C2C、房间 IM 或区域 IMFlutter 使用接口返回刷新资料卡和关系列表。
## 8. 相关IM
所有 IM 都是 Tencent `TIMCustomElem``MsgContent.Desc` 等于下表事件名,`MsgContent.Data` 是 JSON 字符串。客户端按 `event_id` 去重。
### 8.1 收到关系申请
房间 IM 和 C2C IM 都会收到:
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `cp_application_created` |
| `Data.event_type` | `cp_application_created` |
Data
```json
{
"event_id": "cp_application:created:10002:1",
"event_type": "cp_application_created",
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"status": "pending",
"requester": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"target": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"room_id": "room_1001",
"room_region_id": 86,
"gift": {
"gift_id": "cp_ring",
"gift_name": "CP Ring",
"gift_icon_url": "https://cdn.example/cp-ring.png",
"gift_animation_url": "",
"gift_count": 1,
"gift_value": 520,
"billing_receipt_id": "gift_tx_1"
},
"expires_at_ms": 1779345400000,
"decided_at_ms": 0,
"source_created_at_ms": 1779259000000,
"recipient_user_id": "10002"
}
```
处理:
- 房间 IM 只有 `recipient_user_id` 等于当前用户时显示同意/拒绝按钮。
- C2C IM 在私聊里显示同意/拒绝按钮。
- `expires_at_ms <= server_time_ms` 时按钮置灰并刷新申请列表。
### 8.2 申请被同意
C2C 会收到双方互发的两条消息,房间 IM 仍然只给申请发起人 A 展示:
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `cp_application_accepted` |
| `Data.event_type` | `cp_application_accepted` |
Data 字段和申请消息一致,差异是:
| 字段 | 值 |
| --- | --- |
| `status` | `accepted` |
| `decided_at_ms` | 处理时间 |
| `recipient_user_id` | 房间 IM 中为 A 的 user_id |
C2C 差异:
| 方向 | `c2c_notice_role` | `message_text` |
| --- | --- | --- |
| B -> A | `accepted_to_requester` | `我同意了你` |
| A -> B | `accepted_to_acceptor` | `对方已同意你的申请` |
处理:
- A 收到后刷新 `/api/v1/cp/relationships`
- B 操作成功后也刷新关系列表。
- 同一对用户其他 pending 申请需要置灰。
### 8.3 申请被拒绝
C2C 和房间 IM 都会收到:
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `cp_application_rejected` |
| `Data.event_type` | `cp_application_rejected` |
Data 字段和申请消息一致,差异是:
| 字段 | 值 |
| --- | --- |
| `status` | `rejected` |
| `reason` | 拒绝原因,可为空 |
| `decided_at_ms` | 处理时间 |
| `recipient_user_id` | 房间 IM 中为 A 的 user_id |
### 8.4 区域关系成立播报
用户组成任意关系后,会发送区域 IM
| 字段 | 值 |
| --- | --- |
| `MsgContent.Desc` | `cp_relationship_created` |
| `Data.event_type` | `cp_relationship_created` |
Data
```json
{
"event_id": "cp_relationship:created:10002:1",
"event_type": "cp_relationship_created",
"relationship_id": "cp_rel_10001_10002",
"application_id": "cp_app_10001_10002_cp",
"relation_type": "cp",
"requester": {
"user_id": "10001",
"display_user_id": "123456",
"username": "Alice",
"avatar": "https://cdn.example/a.png"
},
"target": {
"user_id": "10002",
"display_user_id": "654321",
"username": "Bob",
"avatar": "https://cdn.example/b.png"
},
"gift": {
"gift_id": "cp_ring",
"gift_name": "CP Ring",
"gift_icon_url": "https://cdn.example/cp-ring.png",
"gift_animation_url": "",
"gift_count": 1,
"gift_value": 520,
"billing_receipt_id": "gift_tx_1"
},
"room_id": "room_1001",
"room_region_id": 86,
"group_id": "hy_lalu_bc_r_86",
"source_created_at_ms": 1779259100000
}
```
处理:
- 只在区域播报位展示,不当作私聊消息。
- 点击可进入 `room_id` 对应房间;房间不存在或进房失败时只关闭播报。
## 8. 客户端错误提示
| code | 建议提示 |
| --- | --- |
| `CONFLICT` | `你或对方已经有关系`;如果申请已过期则提示 `申请已过期` |
| `NOT_FOUND` | `申请不存在或已失效` |
| `PERMISSION_DENIED` | `只能处理发给你的申请` |
## 9. 本地状态规则
- 不要靠送礼成功直接创建关系。
- 不要靠 C2C 或房间按钮本地改关系,必须调用同意或拒绝接口。
- 同一个 `event_id` 只处理一次。
- 同一个 `application_id` 在房间和 C2C 里可能都出现,按钮状态要共享。
- 申请过期时间以 `expires_at_ms` 为准。
- 亲密值和等级以后端关系接口返回为准,不在 Flutter 里按礼物价格推算。
- 进度条和距离下一级只使用关系接口返回的阈值字段;字段缺失时不展示,不要伪造 `next_level_threshold``needed_for_next_level` 或进度百分比。