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

36 KiB
Raw Blame History

Fami VIP Flutter 对接方案

本文只定义 Flutter 与已落地后端的对接契约,不要在客户端重复实现 VIP 状态机。界面和文案统一使用“VIP”不再使用“SVIP”。

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. 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": "",
            "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,
        "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

3. 当前 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": ""
        }
      ],
      "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,
        "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 表示用户尚未修改,两个开关使用默认开启值;设置在无 VIP 时也保留,但不会单独授权。

4. 购买、续期和升级

接口地址

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,
    "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
      }
    }
  }
}

相关 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
  • 余额不足返回 INSUFFICIENT_BALANCE;等级未启用返回 VIP_LEVEL_DISABLED
  • 如果购买时仍佩戴体验卡,购买结果会写入 paid_vip,但当前展示仍可能是 effective_source="trial"。购买成功页也必须以返回的 state.effective_vip 为准。

5. 体验卡背包列表

接口地址

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。

6. 佩戴体验卡

接口地址

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",
      "trial_card_enabled": true
    },
    "state": {
      "effective_source": "trial",
      "effective_vip": {
        "level": 4,
        "name": "VIP4",
        "active": true,
        "expires_at_ms": 1781928000000
      },
      "effective_benefits": []
    },
    "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

7. 卸下体验卡

接口地址

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"
    },
    "state": {
      "effective_source": "paid",
      "paid_vip": {
        "level": 3,
        "name": "VIP3",
        "active": true
      },
      "equipped_trial_card": null,
      "effective_vip": {
        "level": 3,
        "name": "VIP3",
        "active": true
      },
      "effective_benefits": []
    },
    "server_time_ms": 1780229000000
  }
}

相关 IM

无。

场景处理

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

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

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

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

8. 经理中心发放 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 重试。

9. 进房通知 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 查询失败时后端会降级为普通进房,不阻断进房主链路。

10. 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 是局部更新,两个字段至少传一个;显式 false 不能被当成未传:

{
  "room_entry_notice_enabled": false
}
{
  "online_global_notice_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,
      "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,
      "updated_at_ms": 1780300010000
    },
    "server_time_ms": 1780300010000
  }
}

用户从未修改过时,两项默认为 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。

11. 每日 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

12. 上线全服通知

接口地址

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,同日可再播一次,这是已确认产品规则。
  • 权益过期、后台停用、体验卡不允许或用户关闭开关时,服务端返回 PERMISSION_DENIEDFlutter 不做本地飘屏,可重拉 /vip/me
  • 不把“每日一次”放到服务端持久化去重,否则无法满足杀进程后同日可再展示。

13. 权益场景和完成边界

场景 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 持久化,无记录默认两项开启,开关不单独授权
进房高亮通知 本人首屏读 Join HTTP effective_vip,其他成员消费当前房间 room_user_joined 后端已同时校验权益与用户开关,并透传 Join HTTP 和当前房间 IM不发全服
防踢 房管操作返回 PERMISSION_DENIED 时显示不可踢,不做本地预判 已在 room-service 强制;只阻止普通房主/管理员的 KickUser
防禁言 房管操作返回 PERMISSION_DENIED 时显示不可禁言,解禁始终可发起 已在 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/资源前不能宣称已实现

14. 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. 修改通知开关成功后,用 PATCH 响应替换本地 user_settings,不修改 effective_benefits