# 房内红包需求接口拆分 ## 1. 结论 房内红包拆成两类通道: - HTTP 接口:承载有状态业务,包括配置、发送、领取、详情、记录、退款后台操作;房间在线态使用语音房通用接口。 - IM 消息:承载实时 UI 触达,包括房内弹窗、延时红包同语区飘窗、倒计时结束强弹、侧边栏数量刷新。 本需求以产品文档为准: - 红包类型:即时红包、延时红包。 - 发送成功立刻扣除金币。 - 发送者本人可以领取自己发出的红包。 - 红包有效领取时间为 24 小时,过期未领取金额退回发送者。 - 即时红包不触发同语区广播飘窗。 - 延时红包发送成功后触发同语区广播飘窗。 - 当前房间内无论即时还是延时,发送成功后都需要立刻弹出抢红包窗口。 现有历史文档中的 `2 小时退款` 不适用于本次新需求,应改为 24 小时。 ## 2. 统一响应约定 现有 Go 网关成功响应会同时返回 `body` 和 `data`,客户端读取任意一个都可以。 成功响应: ```json { "status": true, "errorCode": 0, "errorMsg": "success", "body": {}, "code": "success", "message": "success", "data": {}, "time": 1780000000000 } ``` 错误响应: ```json { "status": false, "errorCode": 409, "errorCodeName": "voice_room_red_packet_expired", "errorMsg": "red packet expired", "body": null, "code": "voice_room_red_packet_expired", "message": "red packet expired", "data": null, "time": 1780000000000 } ``` 接口文档下文只定义 `body/data` 中的业务字段。 ## 3. 哪些需要使用 IM | 场景 | IM key | 发送范围 | 使用 IM 的原因 | | --- | --- | --- | --- | | 红包创建后当前房间立刻弹窗 | `ROOM_RED_PACKET`,`event=CREATED`,`scope=ROOM` | 当前房间 IM 群 | 房内所有在线用户需要实时收到弹窗,不能依赖轮询 | | 延时红包同语区飘窗引流 | `ROOM_RED_PACKET`,`event=CREATED`,`scope=REGION` | 发送者所在同语区 IM 群 | 首页和其他房间用户没有主动请求,现有区域飘屏能力通过语区 IM 群触达 | | 延时红包倒计时结束强弹 | `ROOM_RED_PACKET`,`event=OPEN_DUE`,`scope=ROOM` | 当前房间 IM 群 | 用户可能提前关闭弹窗,到点必须再次强制弹出 | | 红包抢完、过期、退款后刷新侧边栏 | `ROOM_RED_PACKET`,`event=STATUS_CHANGED`,`scope=ROOM` | 当前房间 IM 群 | 房内侧边栏红包数量需要实时减少 | 不使用 IM 的动作: | 动作 | 原因 | | --- | --- | | 发送红包 | 需要鉴权、扣款、档位校验、日上限、幂等落库 | | 抢红包 | 需要鉴权、并发防超卖、领取幂等、钱包入账 | | 查询配置 | 普通读接口,礼物面板打开时请求即可 | | 查询详情和记录 | 需要分页、权限和稳定数据 | | 过期退款 | 服务端定时任务和后台补偿,不应由客户端 IM 触发 | ## 4. IM 消息格式 现有腾讯 IM 自定义消息通过 `TIMCustomElem.MsgContent.Data` 发送 JSON 字符串。业务层统一使用外层: ```json { "type": "ROOM_RED_PACKET", "data": {} } ``` 腾讯 IM 实际 payload: ```json { "MsgType": "TIMCustomElem", "MsgContent": { "Data": "{\"type\":\"ROOM_RED_PACKET\",\"data\":{...}}", "Desc": "customBody", "Sound": "dingdong.aiff" } } ``` ### 4.1 CREATED 当前房间弹窗和同语区飘窗都使用 `CREATED`,通过 `scope` 区分。 ```json { "type": "ROOM_RED_PACKET", "data": { "event": "CREATED", "scope": "ROOM", "packetNo": "VRRP2026052712340001", "packetId": "VRRP2026052712340001", "roomId": "9001", "regionCode": "AR", "packetMode": "DELAYED", "senderUserId": "10001", "actualAccount": "10001", "userNickname": "Tom", "userAvatar": "https://example.com/avatar.png", "totalAmount": 15000, "totalCount": 5, "remainCount": 5, "status": "ACTIVE", "delaySeconds": 300, "claimStartTime": 1780000000000, "expireTime": 1780086400000, "serverTime": 1779999700000 } } ``` 字段说明: | 字段 | 类型 | 说明 | | --- | --- | --- | | `event` | string | 固定为 `CREATED` | | `scope` | string | `ROOM` 表示房间弹窗,`REGION` 表示同语区飘窗 | | `packetNo` | string | 红包唯一编号,接口参数使用该字段 | | `packetId` | string | 兼容旧 Flutter 飘屏字段,值同 `packetNo` | | `roomId` | string | 红包所属房间,点击飘窗进入该房间 | | `regionCode` | string | 语区编码 | | `packetMode` | string | `IMMEDIATE` 或 `DELAYED` | | `senderUserId` | string | 发送者用户 ID | | `actualAccount` | string | 发送者账号 | | `userNickname` | string | 发送者昵称 | | `userAvatar` | string | 发送者头像 | | `totalAmount` | int64 | 红包总金币 | | `totalCount` | int | 红包份数 | | `remainCount` | int | 剩余可领份数 | | `status` | string | 红包状态 | | `delaySeconds` | int | 后台配置的延时秒数 | | `claimStartTime` | int64 | 可领取开始时间,毫秒时间戳 | | `expireTime` | int64 | 过期时间,毫秒时间戳,创建后 24 小时 | | `serverTime` | int64 | 服务端当前时间,毫秒时间戳 | 发送规则: - 即时红包:只发送 `scope=ROOM` 到当前房间 IM 群。 - 延时红包:发送 `scope=ROOM` 到当前房间 IM 群;同时发送 `scope=REGION` 到同语区 IM 群。 ### 4.2 OPEN_DUE 延时红包倒计时结束后,服务端向当前房间发送强弹消息。 ```json { "type": "ROOM_RED_PACKET", "data": { "event": "OPEN_DUE", "scope": "ROOM", "packetNo": "VRRP2026052712340001", "packetId": "VRRP2026052712340001", "roomId": "9001", "packetMode": "DELAYED", "claimStartTime": 1780000000000, "expireTime": 1780086400000, "serverTime": 1780000000000 } } ``` ### 4.3 STATUS_CHANGED 红包被抢完、过期、退款完成后,服务端向当前房间发送状态刷新消息。 ```json { "type": "ROOM_RED_PACKET", "data": { "event": "STATUS_CHANGED", "scope": "ROOM", "packetNo": "VRRP2026052712340001", "packetId": "VRRP2026052712340001", "roomId": "9001", "status": "EXPIRED", "remainCount": 0, "refundedAmount": 3000, "serverTime": 1780086400000 } } ``` 客户端收到后刷新侧边栏未领取数量;如果当前展示的是该红包弹窗或详情页,同步刷新状态。 ## 5. App HTTP 接口 ### 5.1 获取当前用户语区 IM 群 ```http GET /app/region-im-group/current ``` 兼容旧路径: ```http GET /app/voice-room/red-packet/im-group ``` 返回: ```json { "configured": true, "sysOrigin": "LIKEI", "regionCode": "AR", "groupId": "@TGS#xxxx", "groupName": "region-im-group-LIKEI-AR", "status": "ACTIVE" } ``` 客户端处理: - `configured=true` 且 `groupId` 非空时加入腾讯 IM 群。 - `joinGroup` 返回成功或已在群内都按成功处理。 - 区域变化时退出旧区域群并加入新区域群。 ### 5.2 通用房间心跳 ```http POST /api/v1/rooms/heartbeat ``` 请求: ```json { "room_id": "9001", "command_id": "cmd_heartbeat_9001_10001_1780000000000" } ``` 返回: ```json { "result": { "ok": true }, "user": { "user_id": "10001", "room_id": "9001", "last_seen_at_ms": 1780000000000 }, "room": {} } ``` 用途: - 这是语音房通用 presence 能力,不属于红包专用接口。 - 只刷新已经存在的业务 presence,不能替代进房 `JoinRoom`。 - 红包发送、红包领取、侧边栏有效红包查询都依赖该 presence 判断用户是否仍在当前房间。 - 用户不在房间、已离房、被踢或房间关闭时,心跳应失败,客户端应停止心跳并清理本地房间态。 客户端心跳间隔由房间模块统一配置,必须小于 room-service 的 presence stale 窗口。 ### 5.3 通用离开房间 ```http POST /api/v1/rooms/leave ``` 请求: ```json { "room_id": "9001", "command_id": "cmd_leave_9001_10001_1780000000000" } ``` 返回: ```json { "result": { "ok": true }, "room": {} } ``` 用途: - 这是语音房通用离房接口,不属于红包专用接口。 - 主动关闭房间页、切换账号、被客户端确认退出时调用。 - 离房后 room-service 移除业务 presence,红包领取接口再校验该用户时应返回不在房间。 - 如果用户在麦上,离房流程同时释放麦位。 - LeaveRoom 后客户端必须停止房间心跳、退出 RTC 房、退出或忽略 IM 房间群消息。 ### 5.4 红包配置 ```http GET /app/voice-room/red-packet/config ``` 返回: ```json { "enabled": true, "dailySendLimit": 10, "todaySendCount": 1, "delaySeconds": 300, "expireSeconds": 86400, "amountOptions": [15000, 30000, 70000, 100000], "countOptions": [5, 10, 20, 50], "ruleUrl": "https://example.com/red-packet-rule" } ``` 参数定义: | 字段 | 类型 | 说明 | | --- | --- | --- | | `enabled` | bool | 红包功能开关 | | `dailySendLimit` | int | 每日发送上限 | | `todaySendCount` | int | 当前用户当天已发送数量 | | `delaySeconds` | int | 延时红包等待秒数,后台全局唯一配置 | | `expireSeconds` | int | 固定 86400 秒 | | `amountOptions` | int64[] | 后台配置金额档位 | | `countOptions` | int[] | 后台配置红包份数档位 | | `ruleUrl` | string | 红包规则 H5 地址 | ### 5.5 发送红包 ```http POST /app/voice-room/red-packet/send ``` 请求: ```json { "requestId": "client-unique-id", "roomId": "9001", "packetMode": "DELAYED", "totalAmount": 15000, "totalCount": 5 } ``` 参数定义: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `requestId` | string | 是 | 客户端幂等 ID,重复请求返回同一红包 | | `roomId` | string | 是 | 当前房间 ID | | `packetMode` | string | 是 | `IMMEDIATE` 或 `DELAYED` | | `totalAmount` | int64 | 是 | 总金币,必须命中后台金额档位 | | `totalCount` | int | 是 | 红包份数,必须命中后台份数档位 | 返回: ```json { "packetNo": "VRRP2026052712340001", "roomId": "9001", "packetMode": "DELAYED", "totalAmount": 15000, "totalCount": 5, "remainAmount": 15000, "remainCount": 5, "claimStartTime": "2026-05-27T21:05:00+08:00", "claimStartTimeMs": 1780000000000, "expireTime": "2026-05-28T21:00:00+08:00", "expireTimeMs": 1780086400000, "status": "ACTIVE", "balanceAfter": 100000 } ``` 服务端逻辑: 1. 校验登录态、功能开关,并通过 room-service 通用 presence 校验发送者仍在 `roomId` 房间内。 2. 校验金额和份数档位。 3. 校验每日发送上限。 4. 校验余额并立刻扣金币。 5. 预拆随机份额,写红包主表和份额表。 6. 即时红包 `claimStartTime = createdAt`。 7. 延时红包 `claimStartTime = createdAt + delaySeconds`。 8. 所有红包 `expireTime = createdAt + 24h`。 9. 发送对应 IM 消息。 ### 5.6 房间有效红包列表 ```http GET /app/voice-room/red-packet/active?roomId=9001 ``` 用途: - 进入房间时初始化侧边栏红包数量。 - 用户关闭弹窗后,通过侧边栏图标重新唤起红包窗口。 - 收到 IM 状态变化后刷新列表。 返回: ```json { "roomId": "9001", "unclaimedCount": 3, "items": [ { "packetNo": "VRRP2026052712340001", "packetMode": "DELAYED", "senderUserId": "10001", "senderNickname": "Tom", "senderAvatar": "https://example.com/avatar.png", "totalAmount": 15000, "totalCount": 5, "remainCount": 5, "claimStartTime": "2026-05-27T21:05:00+08:00", "claimStartTimeMs": 1780000000000, "expireTime": "2026-05-28T21:00:00+08:00", "expireTimeMs": 1780086400000, "status": "ACTIVE", "currentUserClaimed": false, "currentUserAmount": 0 } ] } ``` `unclaimedCount` 统计当前用户未领取且未过期、未抢完的红包数量。 ### 5.7 抢红包 ```http POST /app/voice-room/red-packet/claim ``` 请求: ```json { "packetNo": "VRRP2026052712340001" } ``` 返回: ```json { "packetNo": "VRRP2026052712340001", "claimed": true, "amount": 3000, "remainCount": 4, "totalCount": 5, "claimTime": "2026-05-27T21:05:10+08:00", "claimTimeMs": 1780000010000, "status": "ACTIVE" } ``` 领取失败但业务可展示的情况通过错误码返回: - `voice_room_red_packet_room_presence_required`:用户不在红包所属房间内,需要先进入房间。 - `voice_room_red_packet_not_started`:延时红包未到可领取时间。 - `voice_room_red_packet_expired`:红包已过期。 - `voice_room_red_packet_finished`:红包已抢完。 - `voice_room_region_not_match`:非同语区不可领取。 领取前校验: - 抢红包接口不创建、不刷新房间 presence,只读取 room-service 当前 presence。 - 用户必须仍在 `packetNo` 对应的 `roomId` 房间内,否则返回 `voice_room_red_packet_room_presence_required`。 - 进入房间和离开房间仍由通用房间接口负责,不在红包接口里重复实现。 幂等要求: - 同一用户重复领取同一红包,返回第一次领取结果。 - 钱包入账处理中,返回 `claim_processing`,客户端可重试或查详情。 ### 5.8 红包详情 ```http GET /app/voice-room/red-packet/detail?packetNo=VRRP2026052712340001 ``` 返回: ```json { "packetNo": "VRRP2026052712340001", "roomId": "9001", "packetMode": "DELAYED", "sendTime": "2026-05-27 21:00:00", "sendTimeMs": 1779999700000, "totalCount": 5, "totalAmount": 15000, "remainCount": 4, "remainAmount": 12000, "claimedCount": 1, "claimedAmount": 3000, "refundAmount": 0, "claimStartTime": "2026-05-27T21:05:00+08:00", "claimStartTimeMs": 1780000000000, "expireTime": "2026-05-28T21:00:00+08:00", "expireTimeMs": 1780086400000, "status": "ACTIVE", "sender": { "userId": "10001", "account": "10001", "nickname": "Tom", "avatar": "https://example.com/avatar.png" }, "currentUserClaimed": true, "currentUserAmount": 3000 } ``` ### 5.9 领取记录列表 ```http GET /app/voice-room/red-packet/claim-records?packetNo=VRRP2026052712340001&cursor=&limit=20 ``` 返回: ```json { "packetNo": "VRRP2026052712340001", "totalCount": 5, "claimedCount": 1, "claimedAmount": 3000, "records": [ { "id": "2052643780674195456", "userId": "10002", "account": "10002", "nickname": "Jerry", "avatar": "https://example.com/avatar2.png", "amount": 3000, "claimTime": "2026-05-27 21:05:10", "claimTimeMs": 1780000010000 } ], "nextCursor": "2052643780674195456", "hasMore": false } ``` ### 5.10 发送记录列表 ```http GET /app/voice-room/red-packet/sent-records?cursor=&limit=20 ``` 返回: ```json { "records": [ { "packetNo": "VRRP2026052712340001", "roomId": "9001", "packetMode": "DELAYED", "sendTime": "2026-05-27 21:00:00", "sendTimeMs": 1779999700000, "totalCount": 5, "totalAmount": 15000, "refundAmount": 0, "status": "ACTIVE" } ], "nextCursor": "VRRP2026052712340001", "hasMore": false } ``` 列表页展示字段: - 发送时间:`sendTime` - 红包个数:`totalCount` - 红包总金额:`totalAmount` - 退回金额:`refundAmount` ## 6. 后台接口 ### 6.1 查询配置 ```http GET /resident-activity/voice-room-red-packet/config ``` 返回: ```json { "enabled": true, "dailySendLimit": 10, "delaySeconds": 300, "expireSeconds": 86400, "amountOptions": [15000, 30000, 70000, 100000], "countOptions": [5, 10, 20, 50], "ruleUrl": "https://example.com/red-packet-rule" } ``` ### 6.2 保存配置 ```http POST /resident-activity/voice-room-red-packet/config ``` 请求: ```json { "enabled": true, "dailySendLimit": 10, "delaySeconds": 300, "amountOptions": [15000, 30000, 70000, 100000], "countOptions": [5, 10, 20, 50], "ruleUrl": "https://example.com/red-packet-rule" } ``` 说明: - `expireSeconds` 固定为 86400,不建议后台配置。 - `amountOptions` 和 `countOptions` 必须都是正整数数组。 - `totalAmount >= totalCount`,保证每份最小金额大于 0。 ### 6.3 红包记录查询 ```http GET /resident-activity/voice-room-red-packet/records?sysOrigin=LIKEI&roomId=&senderUserId=&status=&startTime=&endTime=&page=1&pageSize=20 ``` 返回: ```json { "page": 1, "pageSize": 20, "total": 1, "records": [ { "packetNo": "VRRP2026052712340001", "roomId": "9001", "senderUserId": "10001", "senderNickname": "Tom", "packetMode": "DELAYED", "totalAmount": 15000, "totalCount": 5, "claimedAmount": 3000, "claimedCount": 1, "refundAmount": 0, "status": "ACTIVE", "sendTime": "2026-05-27 21:00:00", "expireTime": "2026-05-28 21:00:00" } ] } ``` ### 6.4 退款重试 ```http POST /resident-activity/voice-room-red-packet/refund/retry ``` 请求: ```json { "packetNo": "VRRP2026052712340001" } ``` 返回: ```json { "packetNo": "VRRP2026052712340001", "refundAmount": 3000, "refundStatus": "SUCCESS" } ``` ## 7. 错误码 | code | 场景 | 客户端处理 | | --- | --- | --- | | `voice_room_red_packet_disabled` | 功能关闭 | 隐藏入口或提示暂不可用 | | `voice_room_red_packet_amount_invalid` | 金额不在后台档位 | 刷新配置 | | `voice_room_red_packet_count_invalid` | 个数不在后台档位 | 刷新配置 | | `voice_room_red_packet_daily_limit_exceeded` | 今日发送上限 | 提示今日发送次数已达上限 | | `voice_room_red_packet_room_presence_required` | 用户不在当前房间在线态 | 提示需在房间内操作 | | `voice_room_red_packet_insufficient_balance` | 余额不足 | 拉起充值弹窗 | | `voice_room_region_missing` | 用户区域缺失 | 提示暂不可参与 | | `voice_room_region_not_match` | 非同语区领取 | 提示非同语区不可领取 | | `voice_room_red_packet_not_found` | 红包不存在 | 刷新列表 | | `voice_room_red_packet_not_started` | 延时红包未到时间 | 展示倒计时 | | `voice_room_red_packet_expired` | 红包已过期 | 提示已过期并刷新侧边栏 | | `voice_room_red_packet_finished` | 红包已抢完 | 提示已抢完并刷新侧边栏 | | `claim_processing` | 钱包入账处理中 | 展示处理中,可重试或查详情 | | `voice_room_red_packet_wallet_deduct_failed` | 扣款失败 | 提示发送失败 | | `voice_room_red_packet_wallet_income_failed` | 入账失败 | 提示领取处理中或稍后重试 | | `voice_room_red_packet_refund_failed` | 退款失败 | 后台重试 | ## 8. 验证点 功能验证: - 发送即时红包,当前房间用户收到 `ROOM_RED_PACKET CREATED scope=ROOM`,同语区群不收到飘窗。 - 发送延时红包,当前房间用户收到 `scope=ROOM` 弹窗,同语区用户收到 `scope=REGION` 飘窗。 - 延时红包未到 `claimStartTime` 领取返回 `voice_room_red_packet_not_started`。 - 延时红包到点后房间收到 `OPEN_DUE`,已关闭弹窗的用户被强弹。 - 发送者本人可以领取自己发送的红包。 - 同一用户重复领取同一红包返回同一领取结果。 - 并发领取不会超过 `totalAmount` 和 `totalCount`。 - 红包创建 24 小时后,未领取金额退回发送者。 - 领取成功、抢完、过期后房间侧边栏未领取数量实时减少。 接口验证: - 所有成功接口同时返回 `body` 和 `data`。 - 所有失败接口 `status=false`,`body/data=null`,`code` 与 `errorCodeName` 一致。 - `requestId` 重复发送只生成一个红包。 - `packetNo + userId` 重复领取只生成一条领取记录。