12 KiB
可配置 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 的 VIP1–VIP9 初始全部为 disabled。30 天只是不产生零时长脏配置的占位值;价格和资源组保持 0,运营完成配置并启用前不可购买或发卡。
数据模型
App 级 program
vip_program_configs 是行为规则的唯一来源:
program_typelevel_countsame_level_expiry_policyupgrade_expiry_policydowngrade_purchase_policybenefit_inheritance_policy,当前固定target_onlygrant_modetrial_card_enabledstatusconfig_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;不依赖异步过期任务才能失效。
购买事务同时完成:
- 锁定 program、目标等级和当前会员;
- 根据 program 计算有效期,拒绝降级;
- 锁定 COIN 账户并扣费;
- 发放目标资源组;
- 更新会员,写订单、历史和 wallet outbox;
- 一次事务提交。
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 后立刻切 B,B 生效,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:
- 首个成功批次固化当时的
config_version和等级金额矩阵;后续分页不重读新配置。 - wallet 根据会员历史还原 UTC 0 点时的付费 VIP;0 点后才购买或升级的状态从次日起参与。
- 生成
vip_daily_coin_rebates资格,领取窗口固定为[UTC 当日0点,UTC 次日0点)。 - 同事务写
VipDailyCoinRebateAvailableoutbox;activity-service 幂等投影为“VIP金币返现”系统消息。 - 用户手动领取时,返现行锁、COIN 余额、交易、分录和
WalletBalanceChangedoutbox 在一个事务内提交。
“日切资格”严格按 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-programGET|PUT /v1/admin/activity/vip-levelsPOST /v1/admin/activity/vip-trial-card-grantsPOST /v1/admin/activity/vip-grants,仅用于direct_membershipprogram
权益落地边界
当前服务端已强制执行购买有效期、体验卡状态合并、用户通知开关、每日金币返现资格/消息/手动领取入账、自定义房间背景、防踢、防禁言、房内进场通知和上线全服通知。装扮类权益由已绑定的钱包资源与 Flutter 展示链路执行。
以下权益仍只有稳定编码和资格下发,不能仅凭本架构宣称业务已完整闭环:访客匿名/隐藏、榜单隐身、房间发图、动态媒体、彩色样式和礼物托盘等。各 owner 实现时必须调用 CheckVipBenefit 或消费 effective_benefits,并补自己的存储、审计、风控和边界测试。