# 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. 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 关系 | `/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` | 是否已经没有下一等级 | | `formed_at_ms` | 关系形成时间 | | `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 | ## 9. 数据表 表放在 `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_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')`。 ## 10. 相关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` | 区域群 | 关系建立后发送区域播报 | C2C 使用腾讯云 IM `From_Account` 表达会话方向:申请为 A,拒绝为 B;同意会发 `B -> A` 和 `A -> B` 两条。区域 IM 群 ID 使用 `hy__bc_r_`,本地测试可通过 `group_id_prefix` 加前缀。 ## 11. 错误码 | 场景 | code | 说明 | | --- | --- | --- | | 申请不存在 | `NOT_FOUND` | application_id 不存在或不属于当前用户 | | 申请已过期 | `CONFLICT` | 不能再同意;内部 gRPC 语义是 FailedPrecondition | | 已有关系 | `CONFLICT` | A/B 任一方已经有 active 关系 | | 无权限处理 | `PERMISSION_DENIED` | 不是申请目标用户 | | 幂等冲突 | `CONFLICT` | command_id 或事件消费 payload 不一致 | ## 12. 验证项 - 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` 增加。 - 同意、拒绝、关系成立的 IM 都写入 `notice_delivery_events`,失败可重试。