hyapp-server/docs/flutter对接/语音房红包Flutter对接.md
2026-06-01 19:42:53 +08:00

19 KiB
Raw Permalink Blame History

语音房红包 Flutter 对接

本文按当前 gateway、wallet-service 和 activity-service 实现描述 Flutter 需要对接的房内红包 HTTP 接口和腾讯 IM 自定义消息。红包金额、份额、可抢时间、过期退款、领取幂等和房间在线态校验全部以后端为准Flutter 只负责展示、提交用户动作和处理实时刷新。

1. 基本约定

  • 所有 HTTP 路径都有 /api/v1 前缀。
  • 所有接口都需要 Authorization: Bearer <access_token>,并且用户必须已完成资料。
  • 成功响应统一是 gateway envelope
{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {}
}
  • 失败响应也使用同一个 envelopeFlutter 只按 HTTP 状态和 code 分支,message 只做兜底展示或日志。
  • 所有时间字段都是 Unix epoch milliseconds。Flutter 不用本地时区参与业务判断,只做展示格式化。
  • 红包主键统一使用 packetNo。IM 里兼容带 packetId,值和 packetNo 相同。
  • Go 返回的部分 ID 是 numberFlutter 模型里建议统一转成 String 保存,避免大整数精度和 UI 拼接问题。

2. 接入流程

进入语音房后:

  1. 走现有进房流程,加入房间 IM 群。
  2. 调用 GET /api/v1/app/voice-room/red-packet/active?roomId=<roomId> 初始化侧边栏红包列表。
  3. 监听腾讯 IM 房间群 TIMCustomElem,解析 ROOM_RED_PACKET
  4. 如果需要接收延时红包同语区飘窗,调用 GET /api/v1/app/voice-room/red-packet/im-group 获取当前语区群并加入。
  5. 用户点击 IM 红包弹窗或侧边栏红包时,直接用 payload 里的 packetNo 调领取或详情接口。

收到 CREATED IM 后payload 已带发送者头像昵称、金额、份数和剩余份数。Flutter 可以直接弹窗或显示飘窗,不需要为了基础展示再立刻拉 active/detail。断线重连、进房初始化、详情页打开和二级领取记录页仍然走 HTTP 查询。

3. 当前语区 IM 群

GET /api/v1/app/voice-room/red-packet/im-group

返回:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {
    "regionId": 310,
    "regionCode": "AR",
    "regionName": "Arabic",
    "groupId": "hy_lalu_bc_r_310",
    "groupName": "hy_lalu_bc_r_310"
  }
}

Flutter 用途:

字段 用途
groupId 腾讯 IM 区域广播群 ID加入后接收延时红包同语区飘窗。
regionId 当前用户所在语区 ID。
regionCode UI 或埋点使用。

如果返回 voice_room_region_missing,说明用户资料没有有效语区,隐藏区域红包飘窗入口。

4. 红包配置

GET /api/v1/app/voice-room/red-packet/config

当前该接口沿用 wallet 配置 DTO字段是 snake_case

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {
    "config": {
      "app_code": "lalu",
      "enabled": true,
      "count_tiers": [5, 10],
      "amount_tiers": [15000, 30000],
      "delayed_open_seconds": 300,
      "expire_seconds": 86400,
      "daily_send_limit": 10,
      "rule_url": "https://example.com/red-packet-rule",
      "created_at_ms": 1780000000000,
      "updated_at_ms": 1780000000000
    },
    "server_time_ms": 1780000000000
  }
}

Flutter 用途:

字段 用途
enabled false 时隐藏发送入口,已收到的历史红包仍可按接口结果展示。
count_tiers 发送红包份数选项。
amount_tiers 发送红包总金币金额选项。
delayed_open_seconds 延时红包等待秒数Flutter 不提交该值。
expire_seconds 过期秒数;当前为 86400。
daily_send_limit 每人每日发送上限提示。
rule_url 规则说明 H5。

5. 发送红包

POST /api/v1/app/voice-room/red-packet/send
Content-Type: application/json

请求:

{
  "roomId": "9001",
  "packetMode": "DELAYED",
  "totalAmount": 15000,
  "totalCount": 5,
  "requestId": "client-uuid-or-action-id"
}

字段:

字段 必填 说明
roomId 当前语音房 ID。
packetMode IMMEDIATEDELAYED。后端也兼容 normal/delayed
totalAmount 红包总金币,必须命中 amount_tiers
totalCount 红包份数,必须命中 count_tiers
requestId 客户端动作幂等 ID。同一用户同一 requestId 重试返回同一红包。

返回:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {
    "packet": {
      "packetNo": "VRRP2026052712340001",
      "roomId": "9001",
      "regionId": 310,
      "senderUserId": 10001,
      "senderAccount": "10001",
      "senderNickname": "Tom",
      "senderAvatar": "https://example.com/avatar1.png",
      "packetMode": "DELAYED",
      "totalAmount": 15000,
      "totalCount": 5,
      "remainingAmount": 15000,
      "remainingCount": 5,
      "status": "WAITING_OPEN",
      "claimStartTime": 1780000000000,
      "expireTime": 1780086400000,
      "createdAt": 1779999700000,
      "claimedByMe": false,
      "myClaimAmount": 0,
      "refundAmount": 0,
      "refundStatus": "",
      "refundedAt": 0,
      "claims": []
    },
    "balanceAfter": 985000,
    "serverTime": 1779999700000
  }
}

发送成功后服务端会异步发送 IM

  • 当前房间群:ROOM_RED_PACKET / CREATED / scope=ROOM
  • 延时红包额外发送当前语区广播群:ROOM_RED_PACKET / CREATED / scope=REGION
  • 延时红包到点后当前房间群:ROOM_RED_PACKET / OPEN_DUE / scope=ROOM

Flutter 发送成功后可以立即把返回的 packet 放入本地红包列表;后续 IM 可能也会收到同一个红包,需要按 packetNo + event + scope 做去重或覆盖更新。

6. 房间有效红包列表

GET /api/v1/app/voice-room/red-packet/active?roomId=9001

返回:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {
    "packets": [
      {
        "packetNo": "VRRP2026052712340001",
        "roomId": "9001",
        "regionId": 310,
        "senderUserId": 10001,
        "senderAccount": "10001",
        "senderNickname": "Tom",
        "senderAvatar": "https://example.com/avatar1.png",
        "packetMode": "DELAYED",
        "totalAmount": 15000,
        "totalCount": 5,
        "remainingAmount": 15000,
        "remainingCount": 5,
        "status": "WAITING_OPEN",
        "claimStartTime": 1780000000000,
        "expireTime": 1780086400000,
        "createdAt": 1779999700000,
        "claimedByMe": false,
        "myClaimAmount": 0
      }
    ],
    "serverTime": 1779999700000
  }
}

Flutter 用途:

  • 进房或重连后初始化侧边栏列表。
  • 侧边栏未领取数量可以用 packets.where((p) => !p.claimedByMe).length
  • 该接口只返回仍处于 ACTIVEWAITING_OPEN 的红包。抢完、已退款或过期后的最终状态通过详情或 IM STATUS_CHANGED 刷新。

7. 抢红包

POST /api/v1/app/voice-room/red-packet/claim
Content-Type: application/json

请求:

{
  "packetNo": "VRRP2026052712340001"
}

返回:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {
    "claim": {
      "claimId": "claim_xxx",
      "packetNo": "VRRP2026052712340001",
      "userId": 10002,
      "account": "10002",
      "nickname": "Jerry",
      "avatar": "https://example.com/avatar2.png",
      "amount": 3000,
      "status": "claimed",
      "createdAt": 1780000010000
    },
    "packet": {
      "packetNo": "VRRP2026052712340001",
      "roomId": "9001",
      "regionId": 310,
      "senderUserId": 10001,
      "senderAccount": "10001",
      "senderNickname": "Tom",
      "senderAvatar": "https://example.com/avatar1.png",
      "remainingAmount": 12000,
      "remainingCount": 4,
      "status": "ACTIVE",
      "claimedByMe": true,
      "myClaimAmount": 3000
    },
    "balanceAfter": 1003000,
    "serverTime": 1780000010000
  }
}

处理规则:

  • 领取成功响应里的 packet 结构和详情接口一致,示例只保留弹窗刷新需要的关键字段。
  • Flutter 只提交 packetNo。当前服务端按 packetNo + 当前用户 保证同一用户重复领取返回同一结果。
  • 如果返回 voice_room_red_packet_room_presence_required,提示用户需要在房间内操作,并刷新房间在线态。
  • 如果返回 voice_room_red_packet_not_started,展示倒计时,不要重试抢。
  • 如果返回 voice_room_red_packet_finishedvoice_room_red_packet_expired,刷新侧边栏和详情。
  • 如果返回 claim_processing,展示处理中,可让用户稍后重试同一个红包或打开详情。

8. 红包详情

GET /api/v1/app/voice-room/red-packet/detail?packetNo=VRRP2026052712340001

返回:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {
    "packet": {
      "packetNo": "VRRP2026052712340001",
      "roomId": "9001",
      "regionId": 310,
      "senderUserId": 10001,
      "senderAccount": "10001",
      "senderNickname": "Tom",
      "senderAvatar": "https://example.com/avatar1.png",
      "packetMode": "DELAYED",
      "totalAmount": 15000,
      "totalCount": 5,
      "remainingAmount": 12000,
      "remainingCount": 4,
      "status": "ACTIVE",
      "claimStartTime": 1780000000000,
      "expireTime": 1780086400000,
      "createdAt": 1779999700000,
      "claimedByMe": true,
      "myClaimAmount": 3000,
      "refundAmount": 0,
      "refundStatus": "",
      "refundedAt": 0,
      "claims": []
    },
    "serverTime": 1780000010000
  }
}

详情和领取成功响应里的 packet 会补发送者 senderAccount/senderNickname/senderAvatar。详情接口里的 claims 不作为二级领取记录页的资料来源。需要领取用户头像昵称账号时使用 claim-records,因为该接口会按当前页批量补用户资料。

9. 领取记录

GET /api/v1/app/voice-room/red-packet/claim-records?packetNo=VRRP2026052712340001&page=1&pageSize=20

当前实现使用 page/pageSize,不要按旧文档使用 cursor/limit

返回:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {
    "items": [
      {
        "claimId": "claim_xxx",
        "packetNo": "VRRP2026052712340001",
        "userId": 10002,
        "account": "10002",
        "nickname": "Jerry",
        "avatar": "https://example.com/avatar2.png",
        "amount": 3000,
        "status": "claimed",
        "createdAt": 1780000010000
      }
    ],
    "total": 1,
    "page": 1,
    "pageSize": 20,
    "serverTime": 1780000010000
  }
}

Flutter 用途:

  • 二级领取记录页使用 items 渲染头像、昵称、账号、领取金额和领取时间。
  • account/nickname/avatar 已由 gateway 从 user-service 补齐。
  • 分页加载下一页时递增 page

10. 我发出的红包记录

GET /api/v1/app/voice-room/red-packet/sent-records?page=1&pageSize=20

返回:

{
  "code": "OK",
  "message": "ok",
  "request_id": "req_xxx",
  "data": {
    "items": [
      {
        "packetNo": "VRRP2026052712340001",
        "roomId": "9001",
        "packetMode": "DELAYED",
        "totalAmount": 15000,
        "totalCount": 5,
        "remainingAmount": 12000,
        "remainingCount": 4,
        "status": "ACTIVE",
        "claimStartTime": 1780000000000,
        "expireTime": 1780086400000,
        "createdAt": 1779999700000,
        "refundAmount": 0,
        "refundStatus": "",
        "refundedAt": 0
      }
    ],
    "total": 1,
    "page": 1,
    "pageSize": 20,
    "serverTime": 1780000010000
  }
}

11. IM 消息

腾讯 IM 自定义消息的 TIMCustomElem.Data 是 JSON 字符串。Flutter 先解析外层:

{
  "type": "ROOM_RED_PACKET",
  "data": {}
}

只有 type == "ROOM_RED_PACKET" 才进入红包处理。

11.1 CREATED

当前房间弹窗和同语区飘窗都使用 CREATED,通过 scope 区分。修正后的 CREATED payload 已带足够的首屏展示字段。

{
  "type": "ROOM_RED_PACKET",
  "data": {
    "event": "CREATED",
    "scope": "ROOM",
    "packetNo": "VRRP2026052712340001",
    "packetId": "VRRP2026052712340001",
    "roomId": "9001",
    "regionId": 310,
    "regionCode": "AR",
    "packetMode": "DELAYED",
    "senderUserId": "10001",
    "actualAccount": "10001",
    "userNickname": "Tom",
    "userAvatar": "https://example.com/avatar.png",
    "totalAmount": 15000,
    "totalCount": 5,
    "remainAmount": 15000,
    "remainCount": 5,
    "status": "WAITING_OPEN",
    "delaySeconds": 300,
    "refundedAmount": 0,
    "claimStartTime": 1780000000000,
    "expireTime": 1780086400000,
    "serverTime": 1779999700000
  }
}

Flutter 行为:

scope 行为
ROOM 如果当前正在该房间,直接弹抢红包窗口,并更新侧边栏。
REGION 作为同语区飘窗展示;点击后进入 roomId 对应房间并打开红包弹窗或详情。

字段说明:

字段 类型 说明
packetNo / packetId string 红包 ID接口参数使用 packetNo
packetMode string IMMEDIATEDELAYED
senderUserId string 发送者长 ID。
actualAccount string 发送者展示账号。
userNickname string 发送者昵称。
userAvatar string 发送者头像 URL。
totalAmount int64 红包总金币。
totalCount int 红包总份数。
remainAmount int64 剩余总金币。CREATED 时通常等于 totalAmount
remainCount int 剩余可领份数。CREATED 时通常等于 totalCount
status string ACTIVEWAITING_OPENFINISHEDREFUNDED
delaySeconds int64 DELAYED 红包的等待秒数,普通红包为 0。
claimStartTime int64 可领取开始时间。
expireTime int64 过期时间。
serverTime int64 该红包事件生成时间。

11.2 OPEN_DUE

延时红包到可领取时间后,服务端向房间群发 OPEN_DUE。当前 payload 使用同一结构Flutter 只需要关心 event/scope/packetNo/status/claimStartTime/expireTime/remainCount

{
  "type": "ROOM_RED_PACKET",
  "data": {
    "event": "OPEN_DUE",
    "scope": "ROOM",
    "packetNo": "VRRP2026052712340001",
    "packetId": "VRRP2026052712340001",
    "roomId": "9001",
    "packetMode": "DELAYED",
    "senderUserId": "10001",
    "actualAccount": "10001",
    "userNickname": "Tom",
    "userAvatar": "https://example.com/avatar.png",
    "totalAmount": 15000,
    "totalCount": 5,
    "remainAmount": 15000,
    "remainCount": 5,
    "status": "WAITING_OPEN",
    "claimStartTime": 1780000000000,
    "expireTime": 1780086400000,
    "serverTime": 1779999700000
  }
}

Flutter 收到后:

  • 如果用户仍在 roomId 房间,强制弹出该红包或把已展示红包切换为可抢。
  • 不要在客户端发放金额;仍然调用领取接口。
  • 如果本地已经显示该红包,按 packetNo 覆盖更新。

11.3 STATUS_CHANGED

红包抢完、退款完成或状态变化时,服务端向房间群发 STATUS_CHANGED

{
  "type": "ROOM_RED_PACKET",
  "data": {
    "event": "STATUS_CHANGED",
    "scope": "ROOM",
    "packetNo": "VRRP2026052712340001",
    "packetId": "VRRP2026052712340001",
    "roomId": "9001",
    "status": "FINISHED",
    "remainCount": 0,
    "refundedAmount": 0,
    "serverTime": 1780000020000
  }
}

Flutter 收到后:

  • 更新侧边栏数量。
  • 如果当前弹窗或详情页是同一个 packetNo,同步刷新状态。
  • remainCount == 0status == FINISHED/REFUNDED 时不再展示抢按钮。

12. Flutter 状态模型建议

建议本地以 packetNo 为 key 存一份红包 read model

class VoiceRoomRedPacket {
  final String packetNo;
  final String roomId;
  final String packetMode;
  final String status;
  final String senderUserId;
  final String senderAccount;
  final String senderNickname;
  final String senderAvatar;
  final int totalAmount;
  final int totalCount;
  final int remainingAmount;
  final int remainingCount;
  final int claimStartTimeMs;
  final int expireTimeMs;
  final bool claimedByMe;
  final int myClaimAmount;
}

解析建议:

  • senderUserId/userId 这类 ID兼容 number 和 string统一转 string。
  • HTTP 发送成功、active、详情、领取成功里的 packet 使用 senderAccount/senderNickname/senderAvatarIM CREATED/OPEN_DUE 使用 actualAccount/userNickname/userAvatar,落本地模型时归一到发送者字段。
  • 对金额和时间,兼容 number 和 string统一转 int。
  • CREATED IM 里的 remainAmount/remainCount 对应 HTTP 里的 remainingAmount/remainingCount
  • 如果 IM 和 HTTP 字段冲突,以 HTTP 最新查询结果为准。

13. 错误码处理

HTTP code code Flutter 处理
409 voice_room_red_packet_disabled 隐藏发送入口,提示暂不可用。
400 voice_room_red_packet_amount_invalid 刷新配置,提示金额档位已变化。
400 voice_room_red_packet_count_invalid 刷新配置,提示份数档位已变化。
409 voice_room_red_packet_daily_limit_exceeded 提示今日发送次数已达上限。
409 voice_room_red_packet_insufficient_balance 拉起充值或余额不足提示。
404 voice_room_red_packet_not_found 关闭弹窗并刷新 active 列表。
409 voice_room_red_packet_not_started 展示倒计时,不重试领取。
409 voice_room_red_packet_expired 提示已过期,刷新列表和详情。
409 voice_room_red_packet_finished 提示已抢完,刷新列表和详情。
409 voice_room_red_packet_room_presence_required 提示需在房间内操作,重新走进房或刷新在线态。
403 voice_room_region_not_match 用户语区和红包语区不一致,禁止领取。
409 voice_room_region_missing 用户资料缺少语区,提示完善资料或重登刷新。
409 claim_processing 展示处理中,可稍后重试或打开详情。

14. 联调检查清单

  • 发送普通红包后,当前房间收到 CREATED scope=ROOMpayload 带 sender 头像昵称、金额、总份数和剩余份数。
  • 发送延时红包后,当前房间收到 CREATED scope=ROOM,当前语区群收到 CREATED scope=REGION
  • 延时红包未到 claimStartTime 调领取返回 voice_room_red_packet_not_started
  • 到点后房间收到 OPEN_DUEFlutter 可以强弹并允许点击领取。
  • 用户不在红包所属房间时领取返回 voice_room_red_packet_room_presence_required
  • 同一用户重复领取同一 packetNo 返回同一领取结果。
  • 抢完后房间收到 STATUS_CHANGED,侧边栏数量减少,抢按钮隐藏。
  • 领取记录页使用 claim-records?page=&pageSize=,每条记录有 account/nickname/avatar
  • 重连或重新进房后调用 active 恢复侧边栏,不依赖历史 IM 补全状态。