hyapp-server/docs/VIP系统架构.md

183 lines
12 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.

# 可配置 VIP 系统
## 目标与边界
VIP 是 `wallet-service``app_code` 隔离的会员与权益域。每个 App 通过一份 `vip_program_configs` 选择等级数量、购买有效期策略、后台发放模式和体验卡开关gateway、room、admin 和客户端只能消费这份显式配置,不得通过 `app_code` 写第二套状态机。
- `wallet-service`:会员、体验卡、用户功能开关、权益矩阵、购买扣费、每日返现、资源发放、历史和 outbox 的 owner。
- `gateway-service`App HTTP 鉴权、协议转换和钱包 gRPC 编排。
- `room-service`:只执行房间权限并通过腾讯云 IM 投递当前房间事件,不能拥有会员状态。
- `server/admin`:配置 program、等级和每级完整权益发放真实 VIP 或体验卡。
- Flutter展示后端状态、操作背包佩戴并处理客户端生命周期效果不参与购买规则计算。
全链路业务时间使用 UTC epoch milliseconds。每日结算或刷新按 UTC 0 点切日;用户本地时间只用于展示。
## 当前 App 策略
| 配置 | Lalu | Fami |
| --- | --- | --- |
| `program_type` | `legacy_timed` | `tiered_privilege_v1` |
| `level_count` | 10 | 9 |
| 同级购买 | 从当前截止时间续期 | 从当前截止时间续期 |
| 高级升级 | 保留剩余时间后追加新时长 | 立即替换为高级,从购买时刻重新计时,旧等级剩余时间丢弃 |
| 低级购买 | 拒绝 | 拒绝 |
| 后台发放 | `direct_membership`,直接写真实 VIP | `trial_card`,发到背包后由用户佩戴 |
| 体验卡 | 关闭 | 开启 |
| 权益集合 | 每级显式保存 | 每级显式保存完整集合,不按 `unlock_level` 自动继承 |
Fami 的 VIP1VIP9 初始全部为 `disabled`。30 天只是不产生零时长脏配置的占位值;价格和资源组保持 0运营完成配置并启用前不可购买或发卡。
## 数据模型
### App 级 program
`vip_program_configs` 是行为规则的唯一来源:
- `program_type`
- `level_count`
- `same_level_expiry_policy`
- `upgrade_expiry_policy`
- `downgrade_purchase_policy`
- `benefit_inheritance_policy`,当前固定 `target_only`
- `grant_mode`
- `trial_card_enabled`
- `status`
- `config_version`
后台修改 program、等级或权益时递增 `config_version`。会员、订单和体验卡保留创建时的 program/version 快照,当前权限判断使用最新有效 program 和等级显式权益集合。
### 等级和权益
- `vip_levels`:等级名称、状态、金币价格、时长、可选资源组和排序。
- `vip_level_benefits`:以 `(app_code, level, benefit_code)` 保存该等级最终完整权益集合。
`unlock_level` 只记录 P1 初始化来源,运行时和后台保存都不能据此给高等级自动补权益。纯功能权益的 `resource_id=0`;装扮权益可以绑定同 App 的实际资源。
### 付费会员
`user_vip_memberships` 保存用户付费会员事实。有效条件是 `level > 0 && expires_at_ms > now_ms`;不依赖异步过期任务才能失效。
购买事务同时完成:
1. 锁定 program、目标等级和当前会员
2. 根据 program 计算有效期,拒绝降级;
3. 锁定 COIN 账户并扣费;
4. 发放目标资源组;
5. 更新会员,写订单、历史和 wallet outbox
6. 一次事务提交。
`command_id` 是购买幂等键,`request_id` 仅用于链路追踪。
`vip_command_locks` 在任何会员、账户或资源业务锁之前串行化 `(app_code, command_id)`。购买、直接会员赠送和体验卡发放共享该守卫空间;同命令并发重试会等待原事务提交后读取稳定回执,三种业务也不能交叉复用同一命令号。
### 体验卡
体验卡使用三层事实:
- `resources(resource_type=vip_trial_card)`:卡面与等级元数据;
- `user_resource_entitlements`:背包实例和绝对有效期;
- `user_resource_equipment`:当前单选佩戴指针;
- `user_vip_trial_cards`:等级、来源、时长和 program/version 快照。
发卡时即写入 `effective_at_ms``expires_at_ms`,但不自动佩戴。切换卡片只替换 `user_resource_equipment` 指针,不修改、删除、暂停或延长任一卡片。
例如 A=14 天、B=20 天、C=30 天:佩戴 A 后立刻切 BB 生效A 仍保留原绝对到期时间;之后只要 A 未过期即可重新佩戴。体验卡等级不受当前付费 VIP 或上一张体验卡等级限制。
### 用户功能开关
`user_vip_settings``(app_code,user_id)` 保存两个用户偏好:
- `room_entry_notice_enabled`:进房 VIP 高亮通知;
- `online_global_notice_enabled`:上线 VIP 全服飘屏。
无记录时两项默认为 `true`,首次修改才写行。偏好不授予权益:无 VIP 或对应等级没有权益时,即使开关为开也不会放行。用户可在无 VIP、VIP 过期或切换体验卡时保留偏好Fami 与 Lalu 互不影响。
### 每日金币返现
`daily_coin_rebate``metadata_json.coin_amount` 是该等级每日可领金币正整数。只有启用的付费 VIP 参与,体验卡即使历史配置误开 `trial_enabled` 也会被 wallet 二次排除。
每个 App 每个 UTC 日期只有一个 `vip_daily_coin_rebate_runs`
1. 首个成功批次固化当时的 `config_version` 和等级金额矩阵;后续分页不重读新配置。
2. wallet 根据会员历史还原 UTC 0 点时的付费 VIP0 点后才购买或升级的状态从次日起参与。
3. 生成 `vip_daily_coin_rebates` 资格,领取窗口固定为 `[UTC 当日0点,UTC 次日0点)`
4. 同事务写 `VipDailyCoinRebateAvailable` outboxactivity-service 幂等投影为“VIP金币返现”系统消息。
5. 用户手动领取时返现行锁、COIN 余额、交易、分录和 `WalletBalanceChanged` outbox 在一个事务内提交。
“日切资格”严格按 UTC 0 点;“金额配置”是当日首个成功 run 的快照,不伪造系统尚未保存的 0 点历史配置。正常情况下 cron 每分钟触发;如零点后 wallet/cron 长时间不可用,恢复前的后台金额修改会进入当日快照。运营若要保证修改次日生效,应在当日 run 已创建后修改。
## 最终生效状态
所有调用方统一消费 `VipState`
- `paid_vip`:真实购买会员;
- `equipped_trial_card`:当前有效且已佩戴的体验卡;
- `effective_vip`:最终展示和权限使用的 VIP
- `effective_source``paid``trial``none`
- `effective_benefits`:最终身份拥有的权益资格;可关闭的通知类权益还要与 `user_settings` 做 AND
- `program_config`:当前 App 规则;
- `user_settings`:当前 App 下的进房/上线通知偏好。
program 启用时的优先级固定为:`trial_card_enabled=true` 且体验卡有效并已佩戴时覆盖展示;否则使用有效付费会员;二者都无效则为 `none`。关闭体验卡开关只停止它参与最终权限合并,背包和佩戴原始事实仍保留;关闭整个 program 时付费与卡片原始事实仍可查询,但最终身份为 `none`、不下发权益。
钱包只读取 `effective_vip.level` 对应的显式权益行。来源为 `trial` 时,再移除 `trial_enabled=false` 的权益;当前 `daily_coin_rebate` 不对体验卡开放。owner service 必须按 `benefit_code` 判断,不能改回 `level >= N`;进房和上线通知的执行方必须使用 wallet 已合并用户开关的结果。
## P1 权益编码
| 等级 | 本级新增权益 |
| --- | --- |
| VIP1 | `vip_badge_identity``vip_medal``avatar_frame``vip_title` |
| VIP2 | `chat_bubble``vip_gift``entry_effect``visitor_history` |
| VIP3 | `voice_wave``profile_card``custom_room_background``room_image_message` |
| VIP4 | `animated_avatar``animated_room_cover``mic_skin``room_border``vehicle` |
| VIP5 | `colored_room_name``colored_nickname``colored_id``gift_tray_skin` |
| VIP6 | `hide_profile_data``custom_avatar_frame``leaderboard_invisible``daily_coin_rebate` |
| VIP7 | `custom_profile_card``anonymous_profile_visit``custom_gift` |
| VIP8 | `room_entry_notice``custom_pretty_id``custom_vehicle` |
| VIP9 | `anti_kick``anti_mute``online_global_notice` |
初始化时高等级行已展开为完整集合,但这是 seed 结果,不是运行时继承规则。后台可以显式移除任一等级的某项权益。
## 房间权限和 IM
`room-service` 在需要房间权限时读取 wallet 的 `GetMyVip.state`
- `custom_room_background`P1 program 的房主保存或切换自定义背景前强校验Lalu 保持原逻辑。
- `anti_kick`:普通房主/管理员踢人时拒绝操作。
- `anti_mute`:普通房主/管理员新增禁言时拒绝操作。
- 平台封禁、风控、超级管理员使用独立系统治理入口,不经过上述普通房管守卫。
- `room_entry_notice`:权益有效且用户开关开启时,随 `JoinRoomResponse``RoomUserJoined` 和腾讯云 IM 当前房间系统消息携带;只在用户进入的当前房间展示,不做全服广播。
`online_global_notice``POST /vip/online-notice` 触发gateway 先调用 wallet `CheckVipBenefit`,同时校验权益资格和用户开关;然后从 user-service 取服务端用户资料,最后写 activity-service 全局播报 outbox。腾讯云 IM 使用 `TIMCustomElem``Ext=im_broadcast``Desc=vip_online_notice``broadcast_type=vip_online_notice``scope=global`。Flutter 在进程内按 UTC 日控制触发:同一进程每天最多触发一次,不持久化该标记;杀进程重启后允许再次触发。服务端不做用户日级去重,但相同 `command_id` 的 HTTP 重试映射到同一 `event_id`。Flutter 不能直接向全服群发消息。
## App HTTP API
统一前缀 `/api/v1`,响应使用 `{code,message,request_id,data}`
- `GET /vip/packages`program、可购买等级、每级权益和完整 state。
- `GET /vip/me`:付费、体验卡、最终 VIP、权益资格和用户功能开关。
- `POST /vip/purchase`:请求 `{command_id,level}`;价格、时长和资源由服务端读取。
- `GET|PATCH /vip/settings`:读取或局部修改进房/上线通知偏好;空 PATCH 拒绝。
- `POST /vip/online-notice`:请求 `{command_id}`,服务端校验权益、开关和资料后写全服播报 outbox。
- `GET /vip/coin-rebates/current`:返回当前 UTC 日返现资格;无资格或 cron 尚未生成时 `found=false`
- `POST /vip/coin-rebates/statuses`:按最多 100 个 `rebate_ids` 恢复系统消息真实状态,或用受限分页查历史。
- `POST /vip/coin-rebates/{rebate_id}/claim`:请求 `{command_id}`,金额、归属、过期和入账全由 wallet 裁决。
- `GET /users/me/resources?resource_type=vip_trial_card&active_only=true`:查询背包体验卡资源实例。
- `POST /vip/trial-cards/{entitlement_id}/equip`:佩戴指定背包实例。
- `DELETE /vip/trial-cards/equipped`:卸下当前体验卡。
- 历史通用背包装备/卸下接口命中 `vip_trial_card` 时由 wallet service 委托上述专用状态机禁止仓储层直接改装备槽Flutter VIP 流程仍使用专用接口以一次取得完整 `VipState`
- `POST /manager-center/vip-grants`gateway 按当前 program 自动选择直接会员或 30 天体验卡发放Fami 发卡不自动佩戴。
后台接口:
- `GET|PUT /v1/admin/activity/vip-program`
- `GET|PUT /v1/admin/activity/vip-levels`
- `POST /v1/admin/activity/vip-trial-card-grants`
- `POST /v1/admin/activity/vip-grants`,仅用于 `direct_membership` program
## 权益落地边界
当前服务端已强制执行购买有效期、体验卡状态合并、用户通知开关、每日金币返现资格/消息/手动领取入账、自定义房间背景、防踢、防禁言、房内进场通知和上线全服通知。装扮类权益由已绑定的钱包资源与 Flutter 展示链路执行。
以下权益仍只有稳定编码和资格下发,不能仅凭本架构宣称业务已完整闭环:访客匿名/隐藏、榜单隐身、房间发图、动态媒体、彩色样式和礼物托盘等。各 owner 实现时必须调用 `CheckVipBenefit` 或消费 `effective_benefits`,并补自己的存储、审计、风控和边界测试。