299 lines
14 KiB
Markdown
299 lines
14 KiB
Markdown
# CP关系服务端开发文档
|
||
|
||
本文定义当前 CP、兄弟、姐妹关系的服务端边界。关系事实归 `user-service`,送礼扣费归 `wallet-service`,房间送礼事实归 `room-service`,CP 相关 IM 由 `notice-service` 消费 `user_outbox` 后补偿投递。
|
||
|
||
## 1. 关系规则
|
||
|
||
| 类型 | 编码 | 说明 |
|
||
| --- | --- | --- |
|
||
| CP | `cp` | CP 关系 |
|
||
| 兄弟 | `brother` | 兄弟关系 |
|
||
| 姐妹 | `sister` | 姐妹关系 |
|
||
|
||
规则:
|
||
|
||
- CP 关系每个用户最多 1 个,不提供可调数量。
|
||
- 兄弟和姐妹的每人 active 数量由后台 CP 配置控制。
|
||
- A 和 B 同时只能存在一种 active 关系。
|
||
- A 可以给 B 发送 `cp`、`brother`、`sister` 多种 CP 礼物,形成多条 pending 申请。
|
||
- B 接受其中一个申请后,同一对用户其他 pending 申请会被置为 `blocked`,不能再接受。
|
||
- 申请默认 24 小时过期。
|
||
- 不提供佩戴 CP 功能;资料卡如果要展示关系,直接展示用户当前 active 关系。
|
||
|
||
## 2. 服务归属
|
||
|
||
| 服务 | 负责内容 |
|
||
| --- | --- |
|
||
| `wallet-service` | 礼物配置、扣费、热度值、礼物 icon 和动画快照 |
|
||
| `room-service` | 送礼命令、房间热度、房间礼物 IM、`RoomGiftSent` outbox |
|
||
| `user-service` | CP 申请、关系、亲密值、等级、等级奖励幂等、`user_outbox` |
|
||
| `gateway-service` | App HTTP 接口、鉴权、request_id 透传 |
|
||
| `notice-service` | 申请、同意、拒绝、关系成立的 C2C/房间/区域 IM |
|
||
| `cron-service` | 定时触发 user-service 过期申请处理 |
|
||
|
||
边界:
|
||
|
||
- `room-service` 不保存 CP 关系状态。
|
||
- `user-service` 不直连 `wallet-service`、`room-service` 或 `notice-service` 数据库。
|
||
- CP 申请和亲密值变化只能消费 `RoomGiftSent` MQ 事实,不能扫 `room_outbox`。
|
||
- 下游服务只能消费 owner service 发布到 RocketMQ 的 outbox。
|
||
|
||
## 3. CP礼物配置
|
||
|
||
礼物大类使用 `gift_type_code=cp`。CP 子类型使用发送时快照字段 `cp_relation_type`,值为 `cp`、`brother`、`sister`。
|
||
|
||
| 配置 | 说明 |
|
||
| --- | --- |
|
||
| 礼物类型 | `gift_type_code=cp`,礼物面板展示为 CP 礼物 Tab |
|
||
| CP 子类型 | `cp_relation_type=cp/brother/sister`,用于 CP Tab 二级分类 |
|
||
| 礼物 icon | 来自绑定资源的 `asset_url`,发送事件保存快照 |
|
||
| 礼物价格和热度 | 继续由 `wallet-service` 结算,客户端不能传价格或热度 |
|
||
|
||
`cp_relation_type` 写入礼物 `presentation_json`:
|
||
|
||
```json
|
||
{
|
||
"cp_relation_type": "cp",
|
||
"cp_relation_label": "CP礼物"
|
||
}
|
||
```
|
||
|
||
## 4. RoomGiftSent字段
|
||
|
||
`api/proto/events/room/v1/events.proto` 的 `RoomGiftSent` 必须带以下送礼快照:
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
| --- | --- | --- |
|
||
| `gift_type_code` | string | CP 礼物为 `cp` |
|
||
| `cp_relation_type` | string | `cp`、`brother`、`sister`;非 CP 礼物为空 |
|
||
| `gift_name` | string | 礼物名称 |
|
||
| `gift_icon_url` | string | 礼物 icon |
|
||
| `gift_animation_url` | string | 礼物动画,可为空 |
|
||
|
||
## 5. 送礼触发申请
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant F as Flutter
|
||
participant G as gateway-service
|
||
participant R as room-service
|
||
participant W as wallet-service
|
||
participant U as user-service
|
||
participant N as notice-service
|
||
participant IM as Tencent IM
|
||
|
||
F->>G: POST /api/v1/rooms/gift/send
|
||
G->>R: SendGift
|
||
R->>W: DebitGift
|
||
W-->>R: heat_value + gift_type_code + cp_relation_type + gift snapshot
|
||
R->>R: update heat/rank/command log
|
||
R-->>F: SendGiftResponse
|
||
R-->>U: RoomGiftSent MQ
|
||
U->>U: create or refresh CP application
|
||
U-->>N: UserCPApplicationCreated MQ
|
||
N->>IM: A -> B C2C cp_application_created
|
||
N->>IM: room cp_application_created, recipient_user_id=B
|
||
```
|
||
|
||
处理规则:
|
||
|
||
- 只有 `gift_type_code=cp` 且 `cp_relation_type` 有效时创建申请。
|
||
- 同一个 `RoomGiftSent.event_id` 只能消费一次。
|
||
- A 给 B 发送同一类型 CP 礼物时,如果已有 pending 申请,刷新礼物快照和 `expires_at_ms`。
|
||
- A 给 B 发送不同类型 CP 礼物时,可以各有一条 pending 申请。
|
||
- 如果任一方已经有 active 关系,申请可以存在,但接受时会被拒绝;已有关系双方互送礼物会增加亲密值。
|
||
|
||
## 6. 同意和拒绝
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant F as Flutter B
|
||
participant G as gateway-service
|
||
participant U as user-service
|
||
participant N as notice-service
|
||
participant IM as Tencent IM
|
||
|
||
F->>G: POST /api/v1/cp/applications/{id}/accept
|
||
G->>U: AcceptCPApplication
|
||
U->>U: lock pair + lock both users + create relationship
|
||
U-->>N: UserCPApplicationAccepted MQ
|
||
U-->>N: UserCPRelationshipCreated MQ
|
||
N->>IM: B -> A C2C cp_application_accepted
|
||
N->>IM: A -> B C2C cp_application_accepted
|
||
N->>IM: room cp_application_accepted, recipient_user_id=A
|
||
N->>IM: region cp_relationship_created
|
||
```
|
||
|
||
处理规则:
|
||
|
||
- 接受和拒绝都只能由申请目标用户 B 操作。
|
||
- pending 申请过期后不能接受,gateway envelope 返回 `CONFLICT`;内部 gRPC 语义是 FailedPrecondition。
|
||
- 接受时同时校验 A 和 B 是否已经有任意 active 关系;任一方已有关系就返回 `CONFLICT`。
|
||
- 同意成功后写 active 关系,关闭同一对用户其他 pending 申请。
|
||
- 同意会发两条 C2C:`B -> A` 表示“我同意了你”,`A -> B` 表示“对方已同意你的申请”。
|
||
- 同意和拒绝都会向 A 发送房间 IM,用 `recipient_user_id=A` 限定展示对象。
|
||
- 拒绝仍只向 A 发送一条 C2C 和一条房间 IM。
|
||
- 同意成功后额外发送区域 IM,区域取申请来源房间的 `visible_region_id`。
|
||
|
||
## 7. 亲密值和升级
|
||
|
||
任意礼物发送成功后,如果发送方和收礼方已经有 active 关系,则按本次礼物的 `heat_value` 增加亲密值。
|
||
|
||
规则:
|
||
|
||
- 普通礼物按 100% 热度值增加亲密值。
|
||
- 幸运礼物、超级幸运礼物直接使用钱包回执里的最终 `heat_value`,不重复计算比例。
|
||
- 多目标送礼时按每个目标的账务回执分别增加对应关系亲密值。
|
||
- 等级范围 1 到 5;等级亲密值阈值和奖励资源组由后台 CP 配置维护。
|
||
- 每个等级可绑定奖励资源组,升级时调用 `wallet-service` 给双方发放资源。
|
||
- 奖励发放幂等键为 `cp_level_reward:{relationship_id}:{level}:{user_id}`。
|
||
- App 关系查询返回当前等级阈值、下一等级阈值、距离下一级和进度百分比。
|
||
- 资料卡弹窗只能使用 `/api/v1/cp/relationships` 返回的阈值字段展示进度;客户端不能读取后台配置或硬编码等级表。
|
||
|
||
## 8. 解除关系
|
||
|
||
用户可以主动解除自己的 active 关系。解除费用由后台 CP 配置按关系类型维护,单位是金币。
|
||
|
||
规则:
|
||
|
||
- App 先通过 `/api/v1/cp/relationships` 读取 `breakup_cost_coins` 展示费用。
|
||
- 解除接口必须传 `command_id`,同一次解除重试复用同一个 `command_id`。
|
||
- gateway 先在 user-service 创建解除占位,再调用 wallet-service 扣金币,最后确认关系为 `ended`。
|
||
- 如果 wallet-service 扣费失败,user-service 会取消解除占位,关系仍保持 active。
|
||
- 解除成功后不会发送 C2C、房间 IM 或区域 IM;客户端使用接口返回和关系列表刷新 UI。
|
||
|
||
## 9. App HTTP接口
|
||
|
||
所有接口走 `gateway-service`,响应使用 `{code,message,request_id,data}`。
|
||
|
||
| 方法 | 地址 | 说明 |
|
||
| --- | --- | --- |
|
||
| `GET` | `/api/v1/cp/applications` | 查询我的申请 |
|
||
| `POST` | `/api/v1/cp/applications/{application_id}/accept` | 同意申请 |
|
||
| `POST` | `/api/v1/cp/applications/{application_id}/reject` | 拒绝申请 |
|
||
| `GET` | `/api/v1/cp/relationships` | 查询我的 active 关系 |
|
||
| `POST` | `/api/v1/cp/relationships/{relationship_id}/break` | 解除关系并扣金币 |
|
||
|
||
`/api/v1/cp/relationships` 的 relationship item 包含:
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `relationship_id` | 关系 ID |
|
||
| `relation_type` | `cp`、`brother`、`sister` |
|
||
| `status` | 当前为 `active` |
|
||
| `me` | 当前用户快照 |
|
||
| `partner` | 对方用户快照 |
|
||
| `intimacy_value` | 当前亲密值 |
|
||
| `level` | 当前等级 |
|
||
| `current_level_threshold` | 当前等级起始亲密值 |
|
||
| `next_level_threshold` | 下一等级所需亲密值;满级时为 0 |
|
||
| `needed_for_next_level` | 距离下一级还需要多少亲密值;满级时为 0 |
|
||
| `level_progress_percent` | 当前等级内进度百分比,0 到 100 |
|
||
| `max_level` | 是否已经没有下一等级 |
|
||
| `breakup_cost_coins` | 当前关系解除需要支付的金币数 |
|
||
| `formed_at_ms` | 关系形成时间 |
|
||
| `ended_at_ms` | 解除时间;active 关系为 0 |
|
||
| `updated_at_ms` | 最近更新时间 |
|
||
|
||
这些字段由 user-service 从 `user_cp_level_rules` 计算,gateway 只做透传。客户端资料卡不能根据本地表推算这些值。
|
||
|
||
申请列表参数:
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `direction` | string | 否 | `incoming` 或 `outgoing`,默认 `incoming` |
|
||
| `status` | string | 否 | `pending`、`accepted`、`rejected`、`expired`、`blocked` |
|
||
| `page` | int32 | 否 | 默认 1 |
|
||
| `page_size` | int32 | 否 | 默认 20 |
|
||
|
||
关系列表参数:
|
||
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `relation_type` | string | 否 | `cp`、`brother`、`sister`;不传返回全部 |
|
||
| `page` | int32 | 否 | 默认 1 |
|
||
| `page_size` | int32 | 否 | 默认 20 |
|
||
|
||
拒绝请求体:
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `reason` | string | 否 | 拒绝原因,只进入 IM/outbox payload |
|
||
|
||
解除请求体:
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `command_id` | string | 是 | 解除关系幂等键,同一次重试必须复用 |
|
||
|
||
解除返回值:
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `relationship` | 已解除的关系,`status=ended` |
|
||
| `breakup_cost_coins` | 本次实际扣除金币数 |
|
||
| `wallet_transaction_id` | 钱包扣费交易 ID;费用为 0 时为空 |
|
||
| `coin_balance_after` | 扣费后金币余额;费用为 0 时为 0 |
|
||
|
||
## 10. 数据表
|
||
|
||
表放在 `user-service` 库,所有表带 `app_code`,时间使用 UTC epoch milliseconds。
|
||
|
||
| 表 | 说明 |
|
||
| --- | --- |
|
||
| `user_cp_relation_configs` | CP/兄弟/姐妹数量、申请过期和解除费用配置;CP 数量服务端固定为 1,兄弟/姐妹读取后台配置 |
|
||
| `user_cp_level_rules` | 等级阈值和奖励资源组 |
|
||
| `user_cp_gift_event_consumption` | `RoomGiftSent` 消费幂等 |
|
||
| `user_cp_applications` | CP 关系申请 |
|
||
| `user_cp_relationships` | 双人关系事实 |
|
||
| `user_cp_relationship_breakups` | 解除关系 pending/confirmed/canceled 占位和幂等事实 |
|
||
| `user_cp_level_reward_grants` | 等级奖励发放幂等 |
|
||
| `user_outbox` | CP 事件事实 |
|
||
|
||
关键唯一约束:
|
||
|
||
- active 关系按用户对唯一:`(app_code, user_a_id, user_b_id, status='active')`。
|
||
- CP active 关系按单用户唯一:任一用户已有 CP 关系就不能再接受新的 CP 关系。
|
||
- 兄弟/姐妹 active 关系按后台配置做单用户数量上限校验。
|
||
- pending 申请按用户对和类型唯一:`(app_code, requester_user_id, target_user_id, relation_type, status='pending')`。
|
||
- 解除关系按 `command_id` 幂等;同一关系同一时间只能有一条 pending 解除占位。
|
||
|
||
## 11. 相关IM
|
||
|
||
| IM | 通道 | 发送时机 |
|
||
| --- | --- | --- |
|
||
| `cp_application_created` | C2C | A 送 CP 礼物触发申请后,A 发给 B |
|
||
| `cp_application_created` | 房间群 | A 送 CP 礼物触发申请后,发到来源房间,payload 带 `recipient_user_id=B` |
|
||
| `cp_application_accepted` | C2C | B 同意后,B 发给 A,payload 带 `c2c_notice_role=accepted_to_requester`、`message_text=我同意了你` |
|
||
| `cp_application_accepted` | C2C | B 同意后,A 发给 B,payload 带 `c2c_notice_role=accepted_to_acceptor`、`message_text=对方已同意你的申请` |
|
||
| `cp_application_accepted` | 房间群 | B 同意后,发到来源房间,payload 带 `recipient_user_id=A` |
|
||
| `cp_application_rejected` | C2C | B 拒绝后,B 发给 A |
|
||
| `cp_application_rejected` | 房间群 | B 拒绝后,发到来源房间,payload 带 `recipient_user_id=A` |
|
||
| `cp_relationship_created` | 区域群 | 关系建立后发送区域播报 |
|
||
| 无 | 无 | 解除关系成功不发 IM,App 根据解除接口返回刷新 |
|
||
|
||
C2C 使用腾讯云 IM `From_Account` 表达会话方向:申请为 A,拒绝为 B;同意会发 `B -> A` 和 `A -> B` 两条。区域 IM 群 ID 使用 `hy_<app_code>_bc_r_v2_<region_id>`(v1 旧群 `hy_<app_code>_bc_r_<region_id>` 仅保留过渡期双发),本地测试可通过 `group_id_prefix` 加前缀。
|
||
|
||
## 12. 错误码
|
||
|
||
| 场景 | code | 说明 |
|
||
| --- | --- | --- |
|
||
| 申请不存在 | `NOT_FOUND` | application_id 不存在或不属于当前用户 |
|
||
| 申请已过期 | `CONFLICT` | 不能再同意;内部 gRPC 语义是 FailedPrecondition |
|
||
| 已有关系 | `CONFLICT` | A/B 任一方已经有 active 关系 |
|
||
| 无权限处理 | `PERMISSION_DENIED` | 不是申请目标用户 |
|
||
| 余额不足 | `INSUFFICIENT_BALANCE` | 解除关系扣金币失败 |
|
||
| 关系不可解除 | `CONFLICT` | 关系不是 active,或已有 pending 解除 |
|
||
| 幂等冲突 | `CONFLICT` | command_id 或事件消费 payload 不一致 |
|
||
|
||
## 13. 验证项
|
||
|
||
- CP 礼物发送成功后,`RoomGiftSent` 带 `gift_type_code=cp`、`cp_relation_type`、礼物 icon 快照。
|
||
- A 给 B 连发 `cp`、`brother`、`sister` 三种礼物,会生成三条 pending 申请。
|
||
- B 接受其中一条后,其他两条不能再接受。
|
||
- A/B 建立关系后,A 或 B 都不能再和第三人建立 active 关系。
|
||
- pending 申请超过默认 24 小时后不能接受。
|
||
- active 关系双方互送任意礼物,亲密值按 `heat_value` 增加。
|
||
- active 关系解除会扣除后台配置的金币费用,扣费失败不改变关系状态。
|
||
- 同意、拒绝、关系成立的 IM 都写入 `notice_delivery_events`,失败可重试。
|