hyapp-server/docs/Fami_VIP_Flutter对接方案.md

54 KiB
Raw Blame History

可配置 VIP 权限体系 Flutter 对接方案Fami / Lalu

本文定义 Flutter 与当前已落地 VIP 后端的对接契约。客户端不要重复实现 VIP 状态机,也不要按 App、VIP 等级或最低解锁等级推导权限。界面和文案统一使用“VIP”不再使用“SVIP”。

0. 当前稳定契约

  • GET /vip/meGET /vip/packages、购买、佩戴体验卡、卸下体验卡的 state 完全同构,固定包含 paid_vipequipped_trial_cardeffective_vipeffective_sourceeffective_benefitsevaluated_at_ms、完整 program_config、完整 user_settings。写成功后整体替换 store禁止局部合并。
  • user_settings 固定包含 5 项开关:进房通知、上线全服通知、隐藏个人数据、匿名访问主页、榜单隐身;均按 App 隔离、无记录默认开启。
  • 权益富展示读取结构化 benefits[].presentation。它覆盖说明文案、多项静态/动态预览、用户头像合成方式和数值奖励Flutter 不解析 metadata_json 生成 UI。
  • VIP 购买后的余额读取带 versioncoin_balancecoin_balance_after 仅兼容旧客户端。历史幂等回放若 version=0,必须重新请求钱包余额。
  • 业务分支只判断稳定 code,不得解析 message。精确 JSON、presentation Schema 和错误码矩阵以 新版 VIP 权限策略 Flutter 客户端对接文档 为唯一协议定义;本文后续较短 JSON 只解释场景,不定义裁剪响应。

1. 通用约定

  • 地址前缀:/api/v1,实际 host 按环境配置。
  • 所有接口需携带 Authorization: Bearer <access_token>app_codeuser_id 从登录态解析Flutter 不在 body 中传。
  • 所有成功响应使用统一外壳:
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {}
}
  • request_id 由后端生成,只用于追踪,不是幂等键。购买幂等键是 command_id
  • 所有 *_at_ms / duration_ms 都是 Unix epoch milliseconds。业务切日使用 UTCFlutter 只在展示层转本地时间。
  • 客户端展示当前身份只读 state.effective_vip,权益资格只读 state.effective_benefits。不要根据等级、unlock_level 或 App 名自行继承权益;进房/上线通知还要结合 state.user_settings

2. 客户端权限控制方案

2.1 结论

客户端只把“VIP 等级”用于套餐名称、价格、有效期和等级对比展示;所有功能权限一律使用稳定的 benefit_code 判断:

后台等级配置 -> wallet 计算当前 effective_benefits -> Flutter 权益快照 -> 页面预判
                                                     -> owner service 最终鉴权

权限分三层,三层职责不能混用:

层级 负责内容 是否是最终权限依据
VIP 配置与 wallet 快照 按当前 App、付费 VIP/体验卡、开关和配置版本计算 effective_benefits 是,提供资格事实
Flutter 权益中心 控制入口显隐、锁态、升级引导、装扮渲染,减少无效请求 否,只是体验层预判
room/user/wallet/notice 等 owner service 在写操作发生时重新校验权益、业务状态和平台权限 是,接口结果永远优先

禁止以下实现:

  • vipLevel >= 8 就允许进房通知,或 vipLevel >= 9 就认为不可踢。
  • 根据 unlock_level 在客户端累计低等级权益。
  • if (appCode == 'fami') 写一套权限分支、lalu 再写另一套。
  • 用本地缓存阻止房主发起踢人/禁言,或用缓存绕过服务端拒绝。
  • 收到 VIP IM 后直接拼装权限,不再查询 /vip/me

成熟会员/订阅产品通常采用“服务端维护 entitlement当前资格客户端缓存当前资格并做 UI 预判敏感能力由服务端再次鉴权”的模式。Google Play 官方建议在安全后端管理购买状态并在授予权益前验证购买Apple 的 StoreKit 也提供 current entitlements并给出服务端判定服务资格的模型。这里沿用同一边界但资格来源是本项目的 wallet VIP 状态,而不是让 Flutter 直接解释订单:

2.2 Flutter 统一权益中心

建议建立一个按登录 App 隔离的 VipEntitlementStore。下面是数据结构示意,不要求照搬类名:

class VipEntitlementSnapshot {
  final VipProgramConfig program;
  final VipIdentity effectiveVip;
  final String effectiveSource; // paid / trial / none
  final Map<String, VipBenefit> benefitsByCode;
  final VipUserSettings settings;
  final int evaluatedAtMs;

  bool has(String code) {
    return program.status == 'active' &&
        effectiveVip.active &&
        benefitsByCode[code]?.status == 'active';
  }

  bool allowsRoomEntryNotice() {
    return has('room_entry_notice') && settings.roomEntryNoticeEnabled;
  }

  bool allowsOnlineGlobalNotice() {
    return has('online_global_notice') &&
        settings.onlineGlobalNoticeEnabled;
  }

  bool allowsHideProfileData() {
    return has('hide_profile_data') && settings.hideProfileDataEnabled;
  }

  bool allowsAnonymousProfileVisit() {
    return has('anonymous_profile_visit') &&
        settings.anonymousProfileVisitEnabled;
  }

  bool allowsLeaderboardInvisible() {
    return has('leaderboard_invisible') &&
        settings.leaderboardInvisibleEnabled;
  }
}

benefitsByCode 只能由 /vip/me.data.state.effective_benefits 建立。客户端的已知权益注册表只描述“这个 code 由哪个模块渲染/调用”,不能配置“最低 VIP 等级”:

final benefitHandlers = <String, VipBenefitHandler>{
  'custom_room_background': roomBackgroundHandler,
  'daily_coin_rebate': coinRebateHandler,
  'room_entry_notice': roomEntryNoticeHandler,
  'online_global_notice': onlineNoticeHandler,
};

遇到服务端新下发、旧客户端不认识的 benefit_code 时:保留在快照中、上报一次兼容性埋点,但不自动开放未知功能、不崩溃。这样后台可先配置新权益,旧版本客户端会安全忽略。

2.3 页面与写操作的统一决策

场景 Flutter 预处理 最终处理
VIP 中心等级对比 使用 packages[].benefits 展示每个套餐当前完整权益 仅展示,不计算用户权限
功能入口 store.has(code) 决定正常态/锁态和升级引导 点击后的写接口再次鉴权
装扮权益 同时检查权益和服务端资源字段;素材为空时使用无图兜底 保存/佩戴仍调用资源 owner 接口
进房通知 本人的设置页读 store房间内展示读 Join/房间 IM 快照 room-service 在 Join 时校验
防踢、防禁言 不根据目标用户本地 VIP 快照禁用管理按钮 正常调用房管接口,分别处理 VIP_ANTI_KICKVIP_ANTI_MUTE
金币返现 daily_coin_rebate 可用于显示入口,金额/状态不在本地推导 current/statuses/claim 接口决定资格和入账
上线全服通知 权益和开关满足时发起 /vip/online-notice activity/notice 链路异步投递

对于锁定入口,“升级到哪一级”也不能使用本地最低等级表。应按 sort_order 遍历当前 /vip/packages,找到第一个 status=activecan_purchase=truebenefits 包含目标 code 的套餐;找不到时只显示“暂未开放”,不要给出错误等级承诺。

3. 每一级 VIP 如何展示和控制

3.1 Fami P1 初始权益矩阵

下表是初始化配置中的“本级新增权益”,用于产品和客户端理解页面分组,不是客户端授权表。后台保存的是每一级完整权益集合,运营调整后必须以 packages[level].benefits 为准。

等级 初始化新增权益
VIP1 vip_badge_identity VIP标识、vip_medal VIP勋章、avatar_frame VIP头像框、vip_title VIP称号
VIP2 chat_bubble VIP聊天气泡、vip_gift VIP礼物、entry_effect VIP进场通知、visitor_history 查看访客
VIP3 voice_wave VIP声波纹、profile_card VIP资料卡皮肤、custom_room_background 自定义房间背景、room_image_message 房间发图
VIP4 animated_avatar 动态头像、animated_room_cover 动态房间封面、mic_skin 麦克风皮肤、room_border 房间边框、vehicle VIP座驾
VIP5 colored_room_name VIP彩色房间名称、colored_nickname VIP彩色昵称、colored_id VIP彩色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 上线全服通知

展示等级详情时直接循环该套餐的 benefits,可按 benefit_typeexecution_scope 分组;不要把上表写进 Flutter。高等级是否包含低等级权益由后台下发的完整集合决定。

3.2 Lalu 与其他 App

  • /vip/packages/vip/me 返回当前登录态对应 App 的配置Flutter 不传 app_code
  • Lalu 当前是 legacy_timedFami 当前是 tiered_privilege_v1;客户端只展示 program_config,不按该值重写购买状态机。
  • 等级数量使用 program_config.level_count,可购买等级使用 packages 实际数组。不能假设所有 App 都有 VIP1VIP9。
  • 套餐是否可买使用 can_purchase;购买后的等级、期限、权益和当前展示身份使用购买响应中的 state

4. 权益快照生命周期与缓存

4.1 刷新时机

事件 客户端动作
登录成功、切换账号或切换 App 清空旧 App store调用 /vip/me
App 冷启动、回前台 /vip/me 校准过期、后台停用和体验卡回落
进入 VIP 中心 并发调用 /vip/packages/vip/me
购买、佩戴体验卡、卸下体验卡成功 立即用响应 state 原子替换 store
收到 vip_notice event_id 去重,然后调用 /vip/me;若版本变更再刷新 packages
state.program_config.config_version 与套餐版本不一致 用户权限先以 state 为准,重新请求 packages不能跨版本合并权益数组
进入房间 本人首屏读 Join 响应;其他人的进房效果读当前房间 IM不查询对方 /vip/me

内存快照是当前会话的主缓存。磁盘缓存只可用于会员徽章等首帧占位,必须立刻后台校准;防踢、防禁言、返现、发送图片等敏感功能不能在离线状态仅凭磁盘快照授权。

客户端不要用手机定时器直接删除权益。可以用 expires_at_ms 做倒计时和提前显示过期态,但恢复联网或执行写操作时,最终结果仍以服务端为准。

4.2 config_version 规则

  • /vip/me.state.program_config.config_version:当前用户权益快照所依据的版本。
  • /vip/packages.program_config.config_version 和各 package 的 config_version:套餐列表所依据的版本。
  • 收到更高版本的购买响应或 vip_notice.config_version 时,刷新 /vip/me/vip/packages
  • 版本不一致时,绝不能把旧 packages 的权益补进新 state也不能把新 packages 当作用户已拥有权益。

5. 当前接口与消息总览

5.1 HTTP

目的 方法与地址 客户端状态影响
套餐与每级完整权益 GET /api/v1/vip/packages 更新套餐目录,不直接授权
当前 VIP 与生效权益 GET /api/v1/vip/me 原子替换 VIP store
购买/续期/升级 POST /api/v1/vip/purchase 成功后使用响应 state
体验卡背包 GET /api/v1/users/me/resources?resource_type=vip_trial_card 只更新卡列表
佩戴体验卡 POST /api/v1/vip/trial-cards/{entitlement_id}/equip 成功后使用响应 state
卸下体验卡 DELETE /api/v1/vip/trial-cards/equipped 成功后使用响应 state
VIP 功能开关 GET/PATCH /api/v1/vip/settings 更新 user_settings,不改变权益集
金币返现 /api/v1/vip/coin-rebates/* 使用服务端记录和余额回执
上线全服通知 POST /api/v1/vip/online-notice 只创建异步播报事件

5.2 腾讯云 IM 与 inbox

事件 通道与标识 客户端处理
购买/直接激活 VIP C2C TIMCustomElemExt=vip_noticeDesc=VipActivated event_id 去重,刷新 /vip/me,不能直接授权
后台发 Fami 体验卡 当前无专用 C2C 进入背包时刷新资源列表,不假装自动佩戴
用户进入房间 当前房间群 TIMCustomElemExt=room_system_messageDesc=room_user_joined 使用事件内 VIP 快照渲染本次进房效果
VIP 上线全服通知 全局播报群 TIMCustomElemExt=im_broadcastDesc=vip_online_notice event_id 去重并渲染事件快照
VIP 金币返现 后端 system inboxaction_type=vip_coin_rebate_claim 解析 action_param 后调用 statuses 恢复真实状态

当前没有“任意 VIP 权限变化”的通用 IM。客户端必须依靠上述刷新生命周期校准不能把 IM 是否到达当作权益是否生效的依据。

6. VIP 套餐

接口地址

GET /api/v1/vip/packages

参数

无 query/body 参数。

返回值

以下 JSON 仅保留对接所需的代表字段;后端会按本节字段表返回完整对象。

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_01",
  "data": {
    "current_vip": {
      "level": 3,
      "name": "VIP3",
      "active": true,
      "started_at_ms": 1780000000000,
      "expires_at_ms": 1782592000000,
      "program_type": "tiered_privilege_v1",
      "config_version": 7
    },
    "packages": [
      {
        "level": 4,
        "name": "VIP4",
        "status": "active",
        "price_coin": 4000,
        "duration_ms": 2592000000,
        "reward_resource_group_id": 0,
        "reward_items": [],
        "can_purchase": true,
        "sort_order": 40,
        "recharge_gate_required": false,
        "required_recharge_coin_amount": 0,
        "user_recharge_coin_amount": 0,
        "purchase_locked_reason": "",
        "benefits": [
          {
            "benefit_code": "custom_room_background",
            "name": "自定义房间背景",
            "benefit_type": "function",
            "unlock_level": 3,
            "status": "active",
            "trial_enabled": true,
            "resource_id": 0,
            "resource_type": "",
            "execution_scope": "room",
            "auto_equip": false,
            "sort_order": 110,
            "metadata_json": "",
            "presentation": {
              "description": "进入房间时展示动态背景和用户头像",
              "preview_items": [
                {
                  "preview_id": "room_background_gold",
                  "title": "鎏金房间背景",
                  "media_type": "animation",
                  "asset_url": "https://cdn.example/vip/room-bg.png",
                  "preview_url": "https://cdn.example/vip/room-bg-preview.png",
                  "animation_url": "https://cdn.example/vip/room-bg.svga",
                  "composition_type": "user_avatar_center",
                  "sort_order": 10
                }
              ],
              "numeric_reward": null
            },
            "created_at_ms": 0,
            "updated_at_ms": 0
          }
        ],
        "config_version": 7
      }
    ],
    "program_config": {
      "app_code": "fami",
      "program_type": "tiered_privilege_v1",
      "level_count": 9,
      "same_level_expiry_policy": "extend_remaining",
      "upgrade_expiry_policy": "replace_from_now",
      "downgrade_purchase_policy": "reject",
      "benefit_inheritance_policy": "target_only",
      "grant_mode": "trial_card",
      "trial_card_enabled": true,
      "status": "active",
      "config_version": 7
    },
    "state": {
      "paid_vip": {
        "level": 3,
        "name": "VIP3",
        "active": true,
        "expires_at_ms": 1782592000000
      },
      "equipped_trial_card": null,
      "effective_vip": {
        "level": 3,
        "name": "VIP3",
        "active": true,
        "expires_at_ms": 1782592000000
      },
      "effective_source": "paid",
      "effective_benefits": [
        {
          "benefit_code": "custom_room_background",
          "status": "active"
        }
      ],
      "evaluated_at_ms": 1780000000000,
      "program_config": {
        "app_code": "fami",
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "user_settings": {
        "app_code": "fami",
        "user_id": 10001,
        "room_entry_notice_enabled": true,
        "online_global_notice_enabled": true,
        "hide_profile_data_enabled": true,
        "anonymous_profile_visit_enabled": true,
        "leaderboard_invisible_enabled": true,
        "updated_at_ms": 0
      }
    }
  }
}

packages 只返回后台已启用的等级。Flutter 必须动态渲染,不得硬编码 9 个套餐。Fami 初始 VIP1VIP9 均为 disabled,运营配置价格并启用前,packages 为空是正常结果。

相关 IM

无。

场景处理

  • 购买按钮以 can_purchase 为准;false 时可用 purchase_locked_reason 决定禁用态。
  • benefits 已是该等级的完整集合。unlock_level 仅是配置来源信息,不参与 Flutter 权益计算。
  • current_vip 是兼容字段,新代码优先使用 state.effective_vip

7. 当前 VIP 状态

接口地址

GET /api/v1/vip/me

参数

无 query/body 参数。

返回值

以下为结构示例,stateprogram_config 实际返回完整字段。

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_02",
  "data": {
    "level": 8,
    "name": "VIP8",
    "active": true,
    "started_at_ms": 1780100000000,
    "expires_at_ms": 1782692000000,
    "program_type": "tiered_privilege_v1",
    "config_version": 7,
    "program_config": {
      "app_code": "fami",
      "program_type": "tiered_privilege_v1",
      "level_count": 9,
      "same_level_expiry_policy": "extend_remaining",
      "upgrade_expiry_policy": "replace_from_now",
      "downgrade_purchase_policy": "reject",
      "benefit_inheritance_policy": "target_only",
      "grant_mode": "trial_card",
      "trial_card_enabled": true,
      "status": "active",
      "config_version": 7
    },
    "state": {
      "paid_vip": {
        "level": 3,
        "name": "VIP3",
        "active": true,
        "started_at_ms": 1780000000000,
        "expires_at_ms": 1782592000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "equipped_trial_card": {
        "trial_card_id": "vip_trial_card_xxx",
        "entitlement_id": "ent_xxx",
        "resource_id": 108,
        "user_id": 10001,
        "level": 8,
        "name": "VIP8",
        "status": "active",
        "equipped": true,
        "duration_ms": 2592000000,
        "effective_at_ms": 1780100000000,
        "expires_at_ms": 1782692000000,
        "remaining_duration_ms": 2591000000,
        "grant_source": "admin_grant",
        "source_grant_id": "grant_xxx",
        "created_at_ms": 1780100000000,
        "updated_at_ms": 1780100000000
      },
      "effective_vip": {
        "level": 8,
        "name": "VIP8",
        "active": true,
        "started_at_ms": 1780100000000,
        "expires_at_ms": 1782692000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "effective_source": "trial",
      "effective_benefits": [
        {
          "benefit_code": "room_entry_notice",
          "name": "进房高亮通知",
          "benefit_type": "function",
          "unlock_level": 8,
          "status": "active",
          "trial_enabled": true,
          "resource_id": 0,
          "resource_type": "",
          "execution_scope": "room",
          "auto_equip": false,
          "sort_order": 290,
          "metadata_json": "",
          "presentation": null
        }
      ],
      "evaluated_at_ms": 1780101000000,
      "program_config": {
        "app_code": "fami",
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "user_settings": {
        "app_code": "fami",
        "user_id": 10001,
        "room_entry_notice_enabled": true,
        "online_global_notice_enabled": true,
        "hide_profile_data_enabled": true,
        "anonymous_profile_visit_enabled": true,
        "leaderboard_invisible_enabled": true,
        "updated_at_ms": 0
      }
    }
  }
}

data.level/name/active/... 是兼容旧版的最终生效 VIP 扁平投影,新 Flutter 代码应读 data.state

相关 IM

本接口自身不发 IM。收到购买/发放的 vip_notice 后,应调用本接口刷新最终状态。

场景处理

  • effective_source 取值为 paid / trial / none
  • paid_vip 是付费会员事实;equipped_trial_card 是当前佩戴卡;effective_vip 是页面和房间展示的唯一真值。
  • 后台关闭 trial_card_enabled 时,equipped_trial_card 原始背包事实仍返回,但 effective_source 会立即回落为 paidFlutter 不得继续沿用旧体验卡权限缓存。
  • 当体验卡被佩戴时,不限制卡等级高低,它会覆盖付费 VIP 成为 effective_vip;卸下或过期后自然回落到仍有效的 paid_vip
  • 体验卡状态下,daily_coin_rebate 不会进入 effective_benefits。Flutter 不要只看卡等级猜测权益。
  • 无 VIP 时,effective_source="none"effective_vip.active=falseeffective_benefits=[]
  • user_settings.updated_at_ms=0 表示用户尚未修改5 个开关使用默认开启值;设置在无 VIP 时也保留,但不会单独授权。

8. 购买、续期和升级

接口地址

POST /api/v1/vip/purchase

参数

{
  "command_id": "vip_purchase_10001_4_1780100000000",
  "level": 4
}
字段 说明
command_id 必填,同一次购买的网络重试必须复用同一值;新的购买动作必须生成新值。
level 必填,目标 VIP 等级,必须来自 packages

返回值

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_03",
  "data": {
    "order_id": "vip_order_xxx",
    "transaction_id": "tx_xxx",
    "vip": {
      "level": 4,
      "name": "VIP4",
      "active": true,
      "started_at_ms": 1780100000000,
      "expires_at_ms": 1782692000000,
      "program_type": "tiered_privilege_v1",
      "config_version": 7
    },
    "coin_spent": 4000,
    "coin_balance_after": 6000,
    "coin_balance": {
      "asset_type": "COIN",
      "available_amount": 6000,
      "frozen_amount": 0,
      "version": 18
    },
    "reward_items": [],
    "program_config": {
      "app_code": "fami",
      "program_type": "tiered_privilege_v1",
      "upgrade_expiry_policy": "replace_from_now",
      "config_version": 7
    },
    "state": {
      "effective_source": "paid",
      "paid_vip": {
        "level": 4,
        "name": "VIP4",
        "active": true,
        "expires_at_ms": 1782692000000
      },
      "equipped_trial_card": null,
      "effective_vip": {
        "level": 4,
        "name": "VIP4",
        "active": true,
        "expires_at_ms": 1782692000000
      },
      "effective_benefits": [
        {
          "benefit_code": "custom_room_background",
          "status": "active"
        }
      ],
      "evaluated_at_ms": 1780100000000,
      "program_config": {
        "app_code": "fami",
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "user_settings": {
        "app_code": "fami",
        "user_id": 10001,
        "room_entry_notice_enabled": true,
        "online_global_notice_enabled": true,
        "hide_profile_data_enabled": true,
        "anonymous_profile_visit_enabled": true,
        "leaderboard_invisible_enabled": true,
        "updated_at_ms": 0
      }
    }
  }
}

相关 IM

购买或后台直接激活成功后notice-service 向该用户发送 C2C TIMCustomElem

{
  "Ext": "vip_notice",
  "Desc": "VipActivated",
  "Data": "{\"event_type\":\"VipActivated\",\"event_id\":\"evt_xxx\",\"app_code\":\"fami\",\"transaction_id\":\"tx_xxx\",\"command_id\":\"vip_purchase_10001_4_1780100000000\",\"user_id\":\"10001\",\"asset_type\":\"VIP\",\"available_delta\":0,\"frozen_delta\":0,\"source\":\"vip_purchase\",\"action\":\"upgrade\",\"previous_level\":3,\"previous_expires_at_ms\":1781396000000,\"level\":4,\"vip_name\":\"VIP4\",\"started_at_ms\":1780100000000,\"expires_at_ms\":1782692000000,\"program_type\":\"tiered_privilege_v1\",\"config_version\":7,\"expiry_policy\":\"replace_from_now\",\"reward_resource_group_id\":0,\"resource_grant_id\":\"\",\"reward_items\":[],\"created_at_ms\":1780100000000,\"source_created_at_ms\":1780100000000}"
}

Flutter 收到后用 event_id 做会话内去重,再调 GET /vip/me。不要用 IM payload 自行拼出最终权益。

场景处理

  • Fami 无有效会员时:新截止时间 = 购买时间 + duration_ms
  • Fami 同级续期:新截止时间 = 旧 expires_at_ms + duration_ms,保留剩余时间。
  • Fami 高级升级:新截止时间 = 购买时间 + duration_ms,旧低等级剩余时间丢弃。例如 VIP3 剩 15 天购买 VIP4 30 天,结果是 VIP4 30 天。
  • 购买更低等级由后端拒绝,错误 VIP_DOWNGRADE_NOT_ALLOWED
  • 余额不足返回 VIP_INSUFFICIENT_COIN;套餐不存在、停用或不可购买返回 VIP_PACKAGE_NOT_PURCHASABLE;体系停用返回 VIP_PROGRAM_INACTIVE
  • 如果购买时仍佩戴体验卡,购买结果会写入 paid_vip,但当前展示仍可能是 effective_source="trial"。购买成功页也必须以返回的 state.effective_vip 为准。
  • 购买成功后用 coin_balance.version 防止旧响应覆盖新余额。version=0 只可能来自升级前的历史幂等订单,必须刷新钱包余额。

9. 体验卡背包列表

接口地址

GET /api/v1/users/me/resources?resource_type=vip_trial_card

参数

参数 位置
resource_type query 固定 vip_trial_card

返回值

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_04",
  "data": {
    "items": [
      {
        "entitlement_id": "ent_card_a",
        "resource_id": 101,
        "resource": {
          "resource_id": 101,
          "resource_code": "vip_trial_card_3",
          "resource_type": "vip_trial_card",
          "name": "VIP3体验卡",
          "status": "active",
          "grantable": true,
          "grant_strategy": "new_entitlement",
          "usage_scopes": [],
          "asset_url": "",
          "preview_url": "",
          "animation_url": "",
          "metadata_json": "{\"vip_level\":3}",
          "sort_order": 30
        },
        "status": "active",
        "quantity": 1,
        "remaining_quantity": 1,
        "effective_at_ms": 1780100000000,
        "expires_at_ms": 1781309600000,
        "source_grant_id": "grant_a",
        "created_at_ms": 1780100000000,
        "updated_at_ms": 1780100000000,
        "equipped": false
      }
    ],
    "total": 1
  }
}

metadata_json 是 JSON 字符串,如需读取 vip_level 必须再解析一次;但当前生效等级仍以 /vip/me 为准。

相关 IM

无。后台发卡目前不会自动佩戴,也没有已定义的体验卡 C2C 消息。

场景处理

  • 体验卡从发放时的 effective_at_ms 开始绝对计时,不是首次佩戴时才开始。
  • 切换、卸下都不修改任何卡的 expires_at_ms,也不删除旧卡。
  • 列表仅返回当前有效的背包实例。卡过期后会自然从 active 列表消失。
  • 预置卡素材初始为空Flutter 需提供无图兜底,不得拼接本地固定素材 URL。

10. 佩戴体验卡

接口地址

POST /api/v1/vip/trial-cards/{entitlement_id}/equip

参数

  • path entitlement_id:必须使用背包列表的实例 ID不能使用 resource_id
  • 无 body。

返回值

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_05",
  "data": {
    "trial_card": {
      "trial_card_id": "vip_trial_card_b",
      "entitlement_id": "ent_card_b",
      "resource_id": 102,
      "user_id": 10001,
      "level": 4,
      "name": "VIP4",
      "status": "active",
      "equipped": true,
      "duration_ms": 1728000000,
      "effective_at_ms": 1780200000000,
      "expires_at_ms": 1781928000000,
      "remaining_duration_ms": 1700000000,
      "grant_source": "admin_grant",
      "source_grant_id": "grant_b",
      "created_at_ms": 1780200000000,
      "updated_at_ms": 1780200000000
    },
    "program_config": {
      "app_code": "fami",
      "program_type": "tiered_privilege_v1",
      "level_count": 9,
      "same_level_expiry_policy": "extend_remaining",
      "upgrade_expiry_policy": "replace_from_now",
      "downgrade_purchase_policy": "reject",
      "benefit_inheritance_policy": "target_only",
      "grant_mode": "trial_card",
      "trial_card_enabled": true,
      "status": "active",
      "config_version": 7
    },
    "state": {
      "paid_vip": null,
      "equipped_trial_card": {
        "trial_card_id": "vip_trial_card_b",
        "entitlement_id": "ent_card_b",
        "resource_id": 102,
        "user_id": 10001,
        "level": 4,
        "name": "VIP4",
        "status": "active",
        "equipped": true,
        "duration_ms": 1728000000,
        "effective_at_ms": 1780200000000,
        "expires_at_ms": 1781928000000,
        "remaining_duration_ms": 1700000000,
        "grant_source": "admin_grant",
        "source_grant_id": "grant_b",
        "created_at_ms": 1780200000000,
        "updated_at_ms": 1780200000000
      },
      "effective_vip": {
        "user_id": 10001,
        "level": 4,
        "name": "VIP4",
        "active": true,
        "started_at_ms": 1780200000000,
        "expires_at_ms": 1781928000000,
        "updated_at_ms": 1780200000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "effective_source": "trial",
      "effective_benefits": [],
      "evaluated_at_ms": 1780228000000,
      "program_config": {
        "app_code": "fami",
        "program_type": "tiered_privilege_v1",
        "level_count": 9,
        "same_level_expiry_policy": "extend_remaining",
        "upgrade_expiry_policy": "replace_from_now",
        "downgrade_purchase_policy": "reject",
        "benefit_inheritance_policy": "target_only",
        "grant_mode": "trial_card",
        "trial_card_enabled": true,
        "status": "active",
        "config_version": 7
      },
      "user_settings": {
        "app_code": "fami",
        "user_id": 10001,
        "room_entry_notice_enabled": true,
        "online_global_notice_enabled": true,
        "hide_profile_data_enabled": true,
        "anonymous_profile_visit_enabled": true,
        "leaderboard_invisible_enabled": true,
        "updated_at_ms": 0
      }
    },
    "server_time_ms": 1780228000000
  }
}

相关 IM

无。

场景处理

  • 任意等级可自由切换,不比较新旧卡高低。
  • 切换 A 到 B 后B 成为 equipped=trueA 仍在背包中A/B 的绝对截止时间均不变。
  • 必须直接用返回的 state 更新全局 VIP不要等待 IM。
  • 倒计时使用 expires_at_ms - server_time_ms,不要在每次切换时重置为 duration_ms
  • Flutter 的 VIP 流程固定调用本接口,不调用通用 /users/me/resources/{resource_id}/equip。后端虽兼容通用背包装备并会内部转交同一 VIP 状态机,但通用响应没有完整 state;历史通用背包组件若触发成功,必须立即补拉 GET /vip/me

11. 卸下体验卡

接口地址

DELETE /api/v1/vip/trial-cards/equipped

参数

无 path/query/body 参数。

返回值

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_06",
  "data": {
    "unequipped": true,
    "program_config": {
      "app_code": "fami",
      "program_type": "tiered_privilege_v1",
      "level_count": 9,
      "same_level_expiry_policy": "extend_remaining",
      "upgrade_expiry_policy": "replace_from_now",
      "downgrade_purchase_policy": "reject",
      "benefit_inheritance_policy": "target_only",
      "grant_mode": "trial_card",
      "trial_card_enabled": true,
      "status": "active",
      "config_version": 7
    },
    "state": {
      "paid_vip": {
        "user_id": 10001,
        "level": 3,
        "name": "VIP3",
        "active": true,
        "started_at_ms": 1780000000000,
        "expires_at_ms": 1782592000000,
        "updated_at_ms": 1780000000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "equipped_trial_card": null,
      "effective_vip": {
        "user_id": 10001,
        "level": 3,
        "name": "VIP3",
        "active": true,
        "started_at_ms": 1780000000000,
        "expires_at_ms": 1782592000000,
        "updated_at_ms": 1780000000000,
        "program_type": "tiered_privilege_v1",
        "config_version": 7
      },
      "effective_source": "paid",
      "effective_benefits": [],
      "evaluated_at_ms": 1780229000000,
      "program_config": {
        "app_code": "fami",
        "program_type": "tiered_privilege_v1",
        "level_count": 9,
        "same_level_expiry_policy": "extend_remaining",
        "upgrade_expiry_policy": "replace_from_now",
        "downgrade_purchase_policy": "reject",
        "benefit_inheritance_policy": "target_only",
        "grant_mode": "trial_card",
        "trial_card_enabled": true,
        "status": "active",
        "config_version": 7
      },
      "user_settings": {
        "app_code": "fami",
        "user_id": 10001,
        "room_entry_notice_enabled": true,
        "online_global_notice_enabled": true,
        "hide_profile_data_enabled": true,
        "anonymous_profile_visit_enabled": true,
        "leaderboard_invisible_enabled": true,
        "updated_at_ms": 0
      }
    },
    "server_time_ms": 1780229000000
  }
}

相关 IM

无。

场景处理

  • Flutter 的 VIP 流程固定调用本接口,不调用通用 /users/me/resources/vip_trial_card/unequip。后端对历史通用背包调用同样会转交专用状态机,但通用响应不包含回落后的完整 state

  • unequipped=true 表示本次确实解除了佩戴;重复调用可返回 false,仍按成功处理。

  • 卸下只删除佩戴关系,不删卡、不暂停卡、不修改卡的截止时间。

  • 卸下后优先回落到有效 paid_vip;没有付费 VIP 则 effective_source="none"

12. 经理中心发放 VIP

经理中心页面如果复用 Flutter仍调用原地址不需要按 Fami/Lalu 写分支。gateway 会读取当前 App 的 program_config.grant_modeLalu 调直接会员发放Fami 调体验卡专用发放。

接口地址

POST /api/v1/manager-center/vip-grants

参数

{
  "command_id": "mgr_vip_10001_1780300000000",
  "target_user_id": "10001",
  "level": 5,
  "reason": "manager_center"
}
  • command_id 必填且最长 128 字节;重试必须复用同一个值和同一业务参数。
  • 经理中心现有权限仍只允许 VIP1VIP5目标用户国家与经理权限校验保持不变。

返回值

Laludirect_membership)直接激活会员:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_08a",
  "data": {
    "grant_mode": "direct_membership",
    "transaction_id": "tx_xxx",
    "level": 5,
    "active": true,
    "expires_at_ms": 1782892000000,
    "server_time_ms": 1780300000000
  }
}

Famitrial_card)发一张 30 天体验卡到背包,不自动佩戴:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_08b",
  "data": {
    "grant_mode": "trial_card",
    "transaction_id": "",
    "trial_card_id": "vip_card_xxx",
    "entitlement_id": "ent_xxx",
    "source_grant_id": "grant_xxx",
    "level": 5,
    "active": false,
    "card_status": "active",
    "equipped": false,
    "expires_at_ms": 1782892000000,
    "server_time_ms": 1780300000000
  }
}

相关 IM

无。Fami 发卡成功后,被发放用户下次进入背包或收到后续资源刷新信号时重新拉取资源列表;当前接口不会替用户佩戴。

场景处理

  • 只根据响应 grant_mode 展示“VIP 已生效”或“体验卡已发到背包”,不能仅按 level 判断。
  • trial_card 返回的 active=false 表示没有直接激活会员;卡片本身状态看 card_status
  • 配置恰好在 gateway 查询后被后台切换时wallet 会拒绝旧模式写入Flutter 展示接口错误并允许使用同一 command_id 重试。

13. 进房通知 IM

接口地址

进房仍使用现有 POST /api/v1/rooms/join。VIP 展示不新增 Flutter HTTP 请求。

参数

沿用现有进房参数:

{
  "room_id": "room_1001",
  "command_id": "join_10001_room_1001_1780300000000",
  "role": "audience",
  "password": "",
  "response_mode": "full"
}

返回值

response_mode=fullresponse_mode=lite 均会在进房成功响应的 data 中返回当前用户的进房 VIP 快照:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_join_xxx",
  "data": {
    "result": {
      "applied": true,
      "room_version": 18,
      "server_time_ms": 1780300000000
    },
    "room": {},
    "viewer": {},
    "seats": [],
    "profiles": [],
    "im": {
      "group_id": "room_1001",
      "need_join_group": true
    },
    "rtc": {},
    "effective_vip": {
      "program_type": "tiered_privilege_v1",
      "level": 8,
      "name": "VIP8",
      "room_entry_notice_enabled": true
    },
    "server_time_ms": 1780300000000
  }
}

effective_vip 是当前进房用户在本次 Join 时的轻量快照,可用于本人首屏。完整会员状态和权益集仍以 /vip/me.state 为准。

相关 IM

进房成功后只向当前房间的腾讯 IM 群发送 TIMCustomElem,不发全服群:

{
  "Ext": "room_system_message",
  "Desc": "room_user_joined",
  "Data": "{\"event_id\":\"evt_join_xxx\",\"room_id\":\"room_1001\",\"event_type\":\"room_user_joined\",\"actor_user_id\":10001,\"target_user_id\":10001,\"room_version\":18,\"role\":\"audience\",\"nickname\":\"Nina\",\"avatar\":\"https://cdn.example/avatar.png\",\"display_user_id\":\"100042\",\"pretty_display_user_id\":\"888042\",\"vip_program_type\":\"tiered_privilege_v1\",\"effective_vip_level\":\"8\",\"effective_vip_name\":\"VIP8\",\"room_entry_notice_enabled\":\"true\",\"attributes\":{\"role\":\"audience\",\"nickname\":\"Nina\",\"avatar\":\"https://cdn.example/avatar.png\",\"display_user_id\":\"100042\",\"pretty_display_user_id\":\"888042\",\"vip_program_type\":\"tiered_privilege_v1\",\"effective_vip_level\":\"8\",\"effective_vip_name\":\"VIP8\",\"room_entry_notice_enabled\":\"true\"}}"
}

effective_vip_levelroom_entry_notice_enabled 在 Data 中是字符串Flutter 需做兼容解析。

场景处理

  • 当前用户进房成功后,可直接用 HTTP data.effective_vip 初始化本人进房效果;其他成员的进房展示仍以房间 IM 为准。
  • 仅当 event_type == "room_user_joined"room_entry_notice_enabled == "true" 时显示 VIP 进房高亮效果。
  • 消息所在 IM GroupID 就是当前 room_id;不要转发到 App 全局播报群。
  • event_id 去重。重连/重复 Join 仍可收到进房展示事件,不要以本地 presence 猜测是否展示。
  • 进房 VIP 查询失败时后端会降级为普通进房,不阻断进房主链路。

14. VIP 功能开关

接口地址

  • GET /api/v1/vip/settings
  • PATCH /api/v1/vip/settings

GET /api/v1/vip/medata.state.user_settings 与 GET 接口是同一份事实。VIP 页面已经拉取 /vip/me 时,不必再为展示开关多发一次请求。

参数

GET 无参数。PATCH 是局部更新5 个字段至少传一个;显式 false 不能被当成未传:

{
  "room_entry_notice_enabled": false,
  "online_global_notice_enabled": true,
  "hide_profile_data_enabled": false,
  "anonymous_profile_visit_enabled": true,
  "leaderboard_invisible_enabled": true
}

Flutter 不传 app_codeuser_id,后端只从登录态解析。空对象 {} 返回 INVALID_ARGUMENT

返回值

GET

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_settings_get",
  "data": {
    "settings": {
      "app_code": "fami",
      "user_id": 10001,
      "room_entry_notice_enabled": true,
      "online_global_notice_enabled": false,
      "hide_profile_data_enabled": true,
      "anonymous_profile_visit_enabled": true,
      "leaderboard_invisible_enabled": true,
      "updated_at_ms": 1780300000000
    },
    "evaluated_at_ms": 1780300001000
  }
}

PATCH

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_settings_patch",
  "data": {
    "settings": {
      "app_code": "fami",
      "user_id": 10001,
      "room_entry_notice_enabled": false,
      "online_global_notice_enabled": false,
      "hide_profile_data_enabled": false,
      "anonymous_profile_visit_enabled": true,
      "leaderboard_invisible_enabled": true,
      "updated_at_ms": 1780300010000
    },
    "server_time_ms": 1780300010000
  }
}

用户从未修改过时5 项均默认为 trueupdated_at_ms=0。PATCH 只改本次提交的字段,不会覆盖其他项。

相关 IM

修改开关本身不发 IM。进房开关会在下一次 Join 时影响当前房间 room_user_joined;上线开关会在触发 /vip/online-notice 时由服务端再校验。

场景处理

  • 开关是用户偏好,不是权益;无 VIP 时也可保存,但不会产生进房或上线播报。
  • 进房展示必须同时满足 effective_benefitsroom_entry_noticeroom_entry_notice_enabled=true
  • 上线展示必须同时满足 online_global_notice 权益和开关;最终以触发接口的服务端判定为准。
  • 隐藏个人数据、匿名访问主页、榜单隐身分别要求对应权益和同名开关同时成立;客户端不得只看设置值授权。
  • 设置按 App 隔离Fami 的修改不影响 Lalu。

15. 每日 VIP 金币返现

接口地址

  • GET /api/v1/vip/coin-rebates/current:查当前 UTC 日资格。
  • POST /api/v1/vip/coin-rebates/statuses:批量恢复系统消息中的真实状态,或分页查历史。
  • POST /api/v1/vip/coin-rebates/{rebate_id}/claim:手动领取。
  • GET /api/v1/messages?section=system&page_size=20&page_token=查“VIP金币返现”系统消息。

参数

current 无 query/body。statuses 有两种用法:

{
  "rebate_ids": ["vip_rebate_xxx", "vip_rebate_yyy"],
  "page": 1,
  "page_size": 20
}

rebate_ids 最多 100 个,后端只返回当前登录用户拥有的记录。传空数组或不传时使用 page/page_size 查历史,page_size 最大 100。

claim

{
  "command_id": "vip_rebate_claim_10001_20260710"
}

rebate_id 只来自 current/statuses/系统消息。Flutter 不传金额、VIP 等级、app_codeuser_id

返回值

current 已生成资格:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_rebate_current",
  "data": {
    "found": true,
    "rebate": {
      "rebate_id": "vip_rebate_xxx",
      "user_id": "10001",
      "task_day": "2026-07-10",
      "vip_level": 6,
      "vip_name": "VIP6",
      "coin_amount": 100,
      "status": "claimable",
      "available_at_ms": 1783641600000,
      "expires_at_ms": 1783728000000,
      "config_version": 8,
      "claimed_at_ms": 0,
      "wallet_transaction_id": "",
      "created_at_ms": 1783641605000,
      "updated_at_ms": 1783641605000
    },
    "server_time_ms": 1783641610000
  }
}

无资格或 cron 尚未生成时也是成功响应:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_rebate_none",
  "data": {
    "found": false,
    "rebate": null,
    "server_time_ms": 1783641610000
  }
}

statuses

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_rebate_statuses",
  "data": {
    "rebates": [
      {
        "rebate_id": "vip_rebate_xxx",
        "user_id": "10001",
        "task_day": "2026-07-10",
        "vip_level": 6,
        "vip_name": "VIP6",
        "coin_amount": 100,
        "status": "claimable",
        "available_at_ms": 1783641600000,
        "expires_at_ms": 1783728000000,
        "config_version": 8,
        "claimed_at_ms": 0,
        "wallet_transaction_id": "",
        "created_at_ms": 1783641605000,
        "updated_at_ms": 1783641605000
      }
    ],
    "total": 1,
    "server_time_ms": 1783641610000
  }
}

claim

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_rebate_claim",
  "data": {
    "rebate": {
      "rebate_id": "vip_rebate_xxx",
      "user_id": "10001",
      "task_day": "2026-07-10",
      "vip_level": 6,
      "vip_name": "VIP6",
      "coin_amount": 100,
      "status": "claimed",
      "available_at_ms": 1783641600000,
      "expires_at_ms": 1783728000000,
      "config_version": 8,
      "claimed_at_ms": 1783641700000,
      "wallet_transaction_id": "wallet_tx_xxx",
      "created_at_ms": 1783641605000,
      "updated_at_ms": 1783641700000
    },
    "transaction_id": "wallet_tx_xxx",
    "coin_balance": {
      "asset_type": "COIN",
      "available_amount": 20100,
      "frozen_amount": 0,
      "version": 18
    },
    "server_time_ms": 1783641700000
  }
}

status 只处理 claimable / claimed / expiredexpired 是 wallet 根据当前 UTC 时间的投影,不要仅凭手机时钟自行改状态。

相关 IM

返现可领取通知进入后端 system inbox不新增腾讯云 TIMCustomElemGET /messages?section=system 的条目示例:

{
  "message_id": "msg_vip_rebate_xxx",
  "title": "VIP金币返现",
  "summary": "尊敬的VIP用户平台今日返现100金币请及时领取。",
  "body": "尊敬的VIP用户平台今日返现100金币请及时领取。",
  "icon_url": "",
  "image_url": "",
  "action_type": "vip_coin_rebate_claim",
  "action_param": "{\"coin_amount\":100,\"expires_at_ms\":1783728000000,\"rebate_id\":\"vip_rebate_xxx\",\"task_day\":\"2026-07-10\"}",
  "read": false,
  "sent_at_ms": 1783641600000
}

Flutter 先把 action_param 再解析为 JSON。点击前可将页面上所有 rebate_id 合并调 statuses用后端状态刷新“去领取/已领取/已过期”。

场景处理

  • 资格按 UTC 0 点时的有效付费 VIP 还原0 点后才购买或升级的状态从次日参与,体验卡不参与。
  • 领取窗口是 [UTC 当日0点,UTC 次日0点)。到次日0点后服务端返回 VIP_COIN_REBATE_EXPIRED,不再入账。
  • 金额取当日首个成功 cron run 固化的后台配置;同一 run 不会因运营中途改价而分裂。
  • 领取按钮要防连点,网络重试复用同一 command_id。同一返现换新 command_id 重复提交也只返回原成功回执,不会重复加币。
  • 领取成功后用 data.coin_balance 替换 COIN 余额快照,不在 Flutter 本地做 balance += coin_amount
  • 记录不存在或不属于当前用户时返回 VIP_COIN_REBATE_NOT_FOUND;复用一个 command_id 改投另一笔返现时返回 IDEMPOTENCY_CONFLICT

16. 上线全服通知

接口地址

POST /api/v1/vip/online-notice

调用前先完成 GET /api/v1/im/usersig 的 IM 登录,并按 join_groups 加入 type=global_broadcast 的全局播报群。Flutter 只请求后端发送,不向全局群自行发消息。

参数

{
  "command_id": "vip_online_550e8400-e29b-41d4-a716-446655440000_2026-07-10"
}

command_id 最长 128用“进程启动唯一 ID + UTC 日期”生成。同一进程当日网络重试必须复用同一值。

返回值

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_online_notice",
  "data": {
    "event_id": "vip_online_notice:10001:sha256_digest",
    "group_id": "hy_fami_bc_g_v2",
    "status": "pending",
    "created": true,
    "evaluated_at_ms": 1783641610000
  }
}

status=pending 表示已持久化到 activity 播报 outbox腾讯云 IM 由 worker 异步投递。相同 command_id 重试返回同一 event_idcreated=false 是幂等命中,不是失败。

相关 IM

全局播报群下发 TIMCustomElem

字段
Ext im_broadcast
Desc vip_online_notice
Data 下方 JSON 字符串
CloudCustomData Data 相同的 JSON 字符串
{
  "event_id": "vip_online_notice:10001:sha256_digest",
  "broadcast_type": "vip_online_notice",
  "scope": "global",
  "app_code": "fami",
  "user_id": 10001,
  "sender_user_id": 10001,
  "nickname": "Nina",
  "avatar": "https://cdn.example/avatar.png",
  "avatar_frame_url": "https://cdn.example/frame.png",
  "avatar_frame_resource_id": 108,
  "avatar_frame_code": "frame_vip9",
  "avatar_frame_asset_url": "https://cdn.example/frame.png",
  "avatar_frame_preview_url": "https://cdn.example/frame-preview.png",
  "avatar_frame_animation_url": "https://cdn.example/frame-animation.svga",
  "display_user_id": "888042",
  "raw_display_user_id": "100042",
  "pretty_display_user_id": "888042",
  "vip_program_type": "tiered_privilege_v1",
  "vip_level": 9,
  "vip_name": "VIP9",
  "message": "欢迎进入Fami祝你有美好的一天",
  "action": {
    "type": "none"
  },
  "sent_at_ms": 1783641610500
}

display_user_id 优先美号,无美号时回落 raw_display_user_idavatar_frame_url 是兼容字段,固定等于 avatar_frame_asset_url;无当前佩戴头像框时 ID 为 0、URL/code 为空字符串。用户资料、VIP 等级和头像框都由服务端取当前事实Flutter 不能用请求伪造。用 event_id 做会话内去重。

场景处理

  • 在进程内保存 process_boot_id、当前 UTC 日和该日 command_id,不写 SharedPreferences/数据库/磁盘文件。
  • 同进程同 UTC 日首次进入 App/vip/me 中具有 online_global_notice 资格、online_global_notice_enabled=true 时触发。成功后当日不再产生新 command。
  • 超时/5xx 可用同一 command_id 重试;不能在同进程同日换 command 规避幂等。
  • 杀掉进程后内存清空,新进程产生新 process_boot_idcommand_id,同日可再播一次,这是已确认产品规则。
  • 权益过期、后台停用、体验卡不允许或用户关闭开关时,服务端返回 VIP_BENEFIT_REQUIREDFlutter 不做本地飘屏,可重拉 /vip/me
  • 不把“每日一次”放到服务端持久化去重,否则无法满足杀进程后同日可再展示。

17. 权益场景和完成边界

场景 Flutter 处理 当前后端状态
App 维度 VIP 体系 始终渲染 program_config 和接口返回的动态等级,不按 Fami/Lalu 写死业务分支 已强制。Fami 为 tiered_privilege_v1Lalu 为 legacy_timed
Fami 高级升级 直接展示购买响应的新截止时间 已强制,旧低级剩余时间丢弃
同级续期 不在本地计算,以响应为准 已强制,从旧截止时间累加
体验卡自由切换 entitlement_id 佩戴,每次直接替换全局 state 已强制,不限等级,不作废旧卡,不改绝对截止时间
体验卡权益 只渲染 effective_benefits 资格 已强制过滤 trial_enabled=false,金币返现不给体验卡
VIP 功能开关 state.user_settings,用 PATCH 局部修改 已按 App 持久化,无记录默认 5 项开启,开关不单独授权
进房高亮通知 本人首屏读 Join HTTP effective_vip,其他成员消费当前房间 room_user_joined 后端已同时校验权益与用户开关,并透传 Join HTTP 和当前房间 IM不发全服
防踢 房管操作返回 VIP_ANTI_KICK 时显示不可踢,不做本地预判 已在 room-service 强制;只阻止普通房主/管理员的 KickUser
防禁言 房管操作返回 VIP_ANTI_MUTE 时显示不可禁言,解禁始终可发起 已在 room-service 强制;只阻止普通房主/管理员新增禁言
平台封禁/风控/超级管理 Flutter 不作 VIP 免责提示 独立 SystemEvictUser 治理链路会绕过防踢VIP 不能绕过平台治理
自定义房间背景 按现有房间背景页面调用 tiered_privilege_v1 已由 room-service 强制校验 custom_room_background
金币返现 消费 system inbox批量恢复状态手动领取后使用响应余额 已完成 UTC 日资格、异步系统消息、24 小时窗口和 wallet 原子入账;体验卡不参与
上线全服通知 按进程内 UTC 日规则请求后端,消费全局播报群 vip_online_notice 已完成权益/开关校验、服务端资料与头像框、activity outbox 和腾讯云 IM服务端不做日级去重
其他 VIP 装扮/功能 可根据 effective_benefits 展示锁定/解锁态;有素材时使用后端 URL 权益矩阵已配置,但不等于各 owner 均已强制。Fami 初始 resource_id=0、素材为空,未绑定 owner/资源前不能宣称已实现

18. Flutter 建议状态流

  1. 进 VIP 页并发请求 /vip/packages/vip/me,套餐用前者,用户生效状态用后者。
  2. 购买成功、佩戴成功、卸下成功时,立即用响应中的 state 替换全局 VIP store。
  3. App 冷启动、登录成功、回前台以及收到 vip_notice 后调 /vip/me 校准;同时更新 user_settings
  4. 背包页用 vip_trial_card 列表展示每张独立卡;当前佩戴态以 /vip/me.state.equipped_trial_card.entitlement_id 为准。
  5. 所有房间、个人页和消息展示都使用 effective_vip,不要用 paid_vip 覆盖体验卡展示。
  6. system inbox 遇到 action_type=vip_coin_rebate_claim 时,批量用 statuses 恢复真实状态;领取成功后替换余额与返现状态。
  7. IM 登录并加入全局播报群后,按进程内 UTC 日规则触发 /vip/online-notice;收到 vip_online_notice 时用 event_id 去重。
  8. 修改 VIP 功能开关成功后,用 PATCH 响应替换本地 user_settings,不修改 effective_benefits