19 KiB
语音房红包 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": {}
}
- 失败响应也使用同一个 envelope;Flutter 只按 HTTP 状态和
code分支,message只做兜底展示或日志。 - 所有时间字段都是 Unix epoch milliseconds。Flutter 不用本地时区参与业务判断,只做展示格式化。
- 红包主键统一使用
packetNo。IM 里兼容带packetId,值和packetNo相同。 - Go 返回的部分 ID 是 number,Flutter 模型里建议统一转成
String保存,避免大整数精度和 UI 拼接问题。
2. 接入流程
进入语音房后:
- 走现有进房流程,加入房间 IM 群。
- 调用
GET /api/v1/app/voice-room/red-packet/active?roomId=<roomId>初始化侧边栏红包列表。 - 监听腾讯 IM 房间群
TIMCustomElem,解析ROOM_RED_PACKET。 - 如果需要接收延时红包同语区飘窗,调用
GET /api/v1/app/voice-room/red-packet/im-group获取当前语区群并加入。 - 用户点击 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 |
是 | IMMEDIATE 或 DELAYED。后端也兼容 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。 - 该接口只返回仍处于
ACTIVE或WAITING_OPEN的红包。抢完、已退款或过期后的最终状态通过详情或 IMSTATUS_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_finished或voice_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 | IMMEDIATE 或 DELAYED。 |
senderUserId |
string | 发送者长 ID。 |
actualAccount |
string | 发送者展示账号。 |
userNickname |
string | 发送者昵称。 |
userAvatar |
string | 发送者头像 URL。 |
totalAmount |
int64 | 红包总金币。 |
totalCount |
int | 红包总份数。 |
remainAmount |
int64 | 剩余总金币。CREATED 时通常等于 totalAmount。 |
remainCount |
int | 剩余可领份数。CREATED 时通常等于 totalCount。 |
status |
string | ACTIVE、WAITING_OPEN、FINISHED、REFUNDED。 |
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 == 0或status == 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/senderAvatar;IMCREATED/OPEN_DUE使用actualAccount/userNickname/userAvatar,落本地模型时归一到发送者字段。 - 对金额和时间,兼容 number 和 string,统一转 int。
CREATEDIM 里的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=ROOM,payload 带 sender 头像昵称、金额、总份数和剩余份数。 - 发送延时红包后,当前房间收到
CREATED scope=ROOM,当前语区群收到CREATED scope=REGION。 - 延时红包未到
claimStartTime调领取返回voice_room_red_packet_not_started。 - 到点后房间收到
OPEN_DUE,Flutter 可以强弹并允许点击领取。 - 用户不在红包所属房间时领取返回
voice_room_red_packet_room_presence_required。 - 同一用户重复领取同一
packetNo返回同一领取结果。 - 抢完后房间收到
STATUS_CHANGED,侧边栏数量减少,抢按钮隐藏。 - 领取记录页使用
claim-records?page=&pageSize=,每条记录有account/nickname/avatar。 - 重连或重新进房后调用
active恢复侧边栏,不依赖历史 IM 补全状态。