hyapp-server/docs/CP关系服务端开发文档.md
2026-07-04 01:53:10 +08:00

299 lines
14 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.

# 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 发给 Apayload 带 `c2c_notice_role=accepted_to_requester``message_text=我同意了你` |
| `cp_application_accepted` | C2C | B 同意后A 发给 Bpayload 带 `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` | 区域群 | 关系建立后发送区域播报 |
| 无 | 无 | 解除关系成功不发 IMApp 根据解除接口返回刷新 |
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`,失败可重试。