hyapp-server/docs/房内红包需求接口拆分.md

746 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 房内红包需求接口拆分
## 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` 重复领取只生成一条领取记录。