518 lines
19 KiB
Markdown
518 lines
19 KiB
Markdown
# IM 全局/区域播报群架构
|
||
|
||
本文定义腾讯云 IM 全局/区域播报群的架构。它服务红包、区域运营通知、贵重礼物播报等实时入口消息,不替代房间群、公屏、系统 inbox、钱包账务或 Room Cell 状态。
|
||
|
||
相关文档:
|
||
|
||
```text
|
||
docs/语音房基础闭环实现.md
|
||
docs/语音房客户端接口流程.md
|
||
docs/App消息Tab架构.md
|
||
docs/房间Outbox补偿开发.md
|
||
```
|
||
|
||
## Current State
|
||
|
||
当前仓库只有房间 IM 群:
|
||
|
||
| Capability | Current Owner | Current Rule |
|
||
| --- | --- | --- |
|
||
| IM UserSig | `gateway-service` | `/api/v1/im/usersig` 只签发 `sdk_app_id/user_id/user_sig/expire_at_ms` |
|
||
| 房间群建群 | `room-service` | `CreateRoom` 返回前同步调用腾讯 IM REST `create_group`,`GroupID = room_id`;`RoomCreated` outbox/IM bridge 后续只做幂等补偿 |
|
||
| 房间系统消息 | `room-service` | Room Cell 命令成功后写 `room_outbox`,worker direct 投递或 IM bridge consumer 从 MQ 消费后向 `GroupID = room_id` 发 `room_*` 自定义消息 |
|
||
| IM 入群/发言回调 | `gateway-service` | 当前把所有 `GroupId` 都当作 `room_id`,再回查 `room-service` guard |
|
||
| 系统/活动 inbox | `activity-service` | backend inbox/fanout,非实时 IM 群 |
|
||
|
||
因此,直接新建 `global` 或 `region` 群会被现有 IM callback 误判为房间群并拒绝。必须先增加 GroupID 类型识别和 callback 分流。
|
||
|
||
## Scope
|
||
|
||
本阶段新增:
|
||
|
||
| Capability | Owner | Required Result |
|
||
| --- | --- | --- |
|
||
| 全局/区域 GroupID 规则 | shared package | 任何服务都用同一套解析和生成规则 |
|
||
| 播报群建群 reconciler | `activity-service` | 启动和定时补齐全局群、active 区域群 |
|
||
| `/im/usersig` join 列表 | `gateway-service` | 返回当前用户应加入的全局/区域播报群 |
|
||
| IM callback 分流 | `gateway-service` | 房间群走 room guard;播报群走用户状态/区域校验 |
|
||
| 播报发送能力 | `activity-service` | 服务端向全局/区域播报群发送自定义消息 |
|
||
| 旧区域群成员移除 | `activity-service` | 用户区域变化后,对旧区域群执行 `delete_group_member(user_id)` |
|
||
| 贵重礼物播报触发 | `activity-service` consuming room outbox | 基于 `RoomGiftSent` 事实判断阈值并发区域播报 |
|
||
| 宝箱倒计时播报触发 | `activity-service` consuming room outbox | 基于 `RoomTreasureCountdownStarted` 事实按后台配置发区域或全局播报 |
|
||
|
||
本阶段不做:
|
||
|
||
| Excluded | Reason |
|
||
| --- | --- |
|
||
| 把全局/区域群状态放进 `room-service` | `room-service` 只拥有房间状态和房间群桥接 |
|
||
| 用 IM 群承载红包资金事实 | 红包金额、领取、退款和并发扣减必须由 wallet/red-packet 领域持久化 |
|
||
| 为每个红包建群 | 红包是业务消息,不是会话边界 |
|
||
| 客户端自报区域入群 | 用户区域归属只来自 `user-service` |
|
||
| 普通用户向播报群发消息 | 播报群是服务端只写通道 |
|
||
|
||
## GroupID Rules
|
||
|
||
GroupID 必须稳定、短、可解析,并满足腾讯 IM 自定义 GroupID 长度约束。推荐规则:
|
||
|
||
| Group Type | GroupID | Meaning |
|
||
| --- | --- | --- |
|
||
| room | `<room_id>` | 现有房间群,必须满足 `pkg/roomid` 规则 |
|
||
| global_broadcast | `hy_<app_code>_bc_g` | App 内全局播报群 |
|
||
| region_broadcast | `hy_<app_code>_bc_r_<region_id>` | App 内指定区域播报群 |
|
||
|
||
规则:
|
||
|
||
- `app_code` 必须使用 `appcode.Normalize` 后的值。
|
||
- `region_id <= 0` 不能生成区域播报群。
|
||
- GroupID 解析必须返回 `{type, app_code, region_id}`,不能靠调用方手写字符串判断。
|
||
- 全局/区域播报 GroupID 不得被 `roomid.ValidStringID` 误判后进入 room guard;callback 必须先识别 broadcast 前缀。
|
||
- 如果未来 `app_code` 可能很长,需要在 App 注册表中约束可用于 GroupID 的短码,不能截断后拼接。
|
||
|
||
建议新增共享包:
|
||
|
||
```text
|
||
pkg/imgroup
|
||
```
|
||
|
||
核心 API:
|
||
|
||
```go
|
||
type Kind string
|
||
|
||
const (
|
||
KindRoom Kind = "room"
|
||
KindGlobalBroadcast Kind = "global_broadcast"
|
||
KindRegionBroadcast Kind = "region_broadcast"
|
||
)
|
||
|
||
type ParsedGroup struct {
|
||
Kind Kind
|
||
AppCode string
|
||
RegionID int64
|
||
RoomID string
|
||
}
|
||
|
||
func GlobalBroadcastGroupID(appCode string) (string, error)
|
||
func RegionBroadcastGroupID(appCode string, regionID int64) (string, error)
|
||
func Parse(groupID string) ParsedGroup
|
||
```
|
||
|
||
## Ownership Boundaries
|
||
|
||
| Area | Owner | Rule |
|
||
| --- | --- | --- |
|
||
| 用户身份、资料完成、区域归属 | `user-service` | gateway callback 和 `/im/usersig` 只信 user-service |
|
||
| 房间群、公屏、房间系统消息 | `room-service` | `GroupID = room_id`,仍由 Room Cell 和 room outbox 驱动 |
|
||
| 全局/区域播报群 | `activity-service` | 作为消息/播报能力,不写 Room Cell 核心状态 |
|
||
| 红包资金事实 | `wallet-service` 或独立 red-packet domain | IM 只携带入口和展示字段 |
|
||
| 客户端 IM 长连接 | Tencent IM SDK | 本仓库不自建 WebSocket |
|
||
|
||
`room-service` 只需要继续产出 `RoomGiftSent`、`RoomTreasureCountdownStarted` 等 outbox 事实。贵重礼物和宝箱倒计时是否需要区域/全局播报,由 `activity-service` 通过 direct gRPC 或 RocketMQ `hyapp_room_outbox` consumer group 消费后判断,这样不会把区域播报策略塞进 Room Cell 主链路。
|
||
|
||
## Client Flow
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as Client
|
||
participant G as gateway-service
|
||
participant U as user-service
|
||
participant IM as Tencent IM SDK
|
||
|
||
C->>G: GET /api/v1/im/usersig
|
||
G->>U: GetUser(user_id)
|
||
U-->>G: user.region_id, profile_completed
|
||
G-->>C: UserSig + join_groups
|
||
C->>IM: login(user_id, user_sig)
|
||
C->>IM: joinGroup(hy_<app>_bc_g)
|
||
C->>IM: joinGroup(hy_<app>_bc_r_<region_id>)
|
||
```
|
||
|
||
`/api/v1/im/usersig` response 增加 `join_groups`,客户端必须按返回列表加入播报群:
|
||
|
||
```json
|
||
{
|
||
"sdk_app_id": 20036101,
|
||
"user_id": "42",
|
||
"user_sig": "xxx",
|
||
"expire_at_ms": 1770000000000,
|
||
"join_groups": [
|
||
{
|
||
"group_id": "hy_lalu_bc_g",
|
||
"type": "global_broadcast"
|
||
},
|
||
{
|
||
"group_id": "hy_lalu_bc_r_1001",
|
||
"type": "region_broadcast",
|
||
"region_id": 1001
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
客户端规则:
|
||
|
||
- 用户完成资料后才允许请求 IM UserSig。
|
||
- 登录 IM 后先 join `join_groups`,进入房间时再 join 房间群。
|
||
- 区域变化后,下一次 `/im/usersig` 或 profile refresh 应返回新区域群;客户端应退出旧区域群并加入新区域群。
|
||
- 全局群可配置为默认返回或按开关返回;区域群是主要实时播报通道。
|
||
|
||
## User Region Change Flow
|
||
|
||
用户国家变化可能改变 `users.region_id`。区域变化时要更新的是“这个用户在旧区域群里的成员关系”,不是区域群生命周期:
|
||
|
||
```text
|
||
不能做:
|
||
destroy_group(hy_<app>_bc_r_<old_region_id>)
|
||
或清空旧区域群成员
|
||
|
||
应该做:
|
||
delete_group_member(group_id=hy_<app>_bc_r_<old_region_id>, user_id=<user_id>)
|
||
```
|
||
|
||
职责拆分:
|
||
|
||
| Step | Owner | Rule |
|
||
| --- | --- | --- |
|
||
| App 用户改国家 | `gateway-service -> user-service` | `user-service` 更新 `country/region_id` 并返回 old/new region |
|
||
| 后台改用户国家 | `hyapp-admin-server` | 后台写 `users.country/region_id` 后对比 old/new region |
|
||
| 移除旧区域成员关系 | `activity-service` | 调腾讯云 IM `delete_group_member`,只移除该 user,不删除群 |
|
||
| 加入新区群 | App | 重新拉 profile 或 `/im/usersig`,按新的 `join_groups` join 新区群 |
|
||
| 准入兜底 | `gateway-service` callback | 用户再次尝试 join 旧区域群时按当前 `user.region_id` 拒绝 |
|
||
|
||
实现规则:
|
||
|
||
- `activity-service` 暴露 `BroadcastService.RemoveRegionBroadcastMember`,入参是 `user_id + old_region_id`。
|
||
- `old_region_id <= 0` 是 no-op,因为 GLOBAL/未知区域没有区域播报群。
|
||
- `gateway-service` 和 `hyapp-admin-server` 在国家修改成功后 best-effort 调用该 RPC;IM 外部副作用失败不回滚已提交的用户主数据。
|
||
- 失败必须写结构化日志,后续可由后台补偿或用户下次刷新 join 列表修正;callback 仍然保证用户不能重新加入错误区域群。
|
||
- 区域群由 reconciler 长期维护,不因为单个用户离开而解散。
|
||
|
||
## Group Lifecycle
|
||
|
||
播报群建群不应该在 `/im/usersig` 请求路径同步调用腾讯 REST,避免登录入口被外部建群耗时拖垮。
|
||
|
||
推荐由 `activity-service` 增加 broadcast group reconciler:
|
||
|
||
| Trigger | Action |
|
||
| --- | --- |
|
||
| service startup | ensure `hy_<app>_bc_g` and all active region groups |
|
||
| periodic reconcile | 扫 active regions,补齐缺失群 |
|
||
| region created/enabled | ensure corresponding region group |
|
||
| region disabled | 停止返回该区域群;是否解散群按运营策略单独处理 |
|
||
|
||
Tencent REST 建群必须幂等:
|
||
|
||
- 已存在视为成功。
|
||
- 建群失败写结构化日志和指标,等待下一轮 reconcile。
|
||
- 不把 SecretKey、UserSig、完整外部响应写入业务错误。
|
||
|
||
播报群建议使用腾讯 IM 普通群能力,并开启 callback:
|
||
|
||
- `ApplyJoinOption` 允许用户申请/加入,但最终由 callback 判定。
|
||
- 普通用户发言由 `CallbackBeforeSendMsg` 拒绝。
|
||
- 服务端通过管理员 REST `send_group_msg` 发布自定义消息。
|
||
|
||
## Callback Routing
|
||
|
||
gateway 的腾讯 IM callback 必须先解析 `GroupId`:
|
||
|
||
```text
|
||
GroupID = room_id
|
||
Join: VerifyRoomPresence(room_id, user_id)
|
||
Send: CheckSpeakPermission(room_id, user_id)
|
||
|
||
GroupID = hy_<app>_bc_g
|
||
Join: user exists + profile_completed + app_code matched
|
||
Send: reject ordinary users; allow Tencent admin/server sender only
|
||
|
||
GroupID = hy_<app>_bc_r_<region_id>
|
||
Join: user exists + profile_completed + user.region_id == region_id + app_code matched
|
||
Send: reject ordinary users; allow Tencent admin/server sender only
|
||
```
|
||
|
||
失败策略:
|
||
|
||
- callback 鉴权失败必须 fail-closed。
|
||
- SDKAppID 不匹配必须 fail-closed。
|
||
- 无法解析用户 ID 必须拒绝。
|
||
- user-service 不可用时,播报群 join 必须拒绝;不能用客户端缓存区域兜底。
|
||
- 未知 GroupID 类型可按 room 群处理,但不得绕过现有 room guard。
|
||
|
||
## Broadcast Message Contract
|
||
|
||
所有播报都使用 `TIMCustomElem`,`CloudCustomData` 带完整 JSON。客户端只根据 `broadcast_type` 和 `action.type` 分发 UI 行为。
|
||
|
||
公共字段:
|
||
|
||
```json
|
||
{
|
||
"event_id": "stable-id",
|
||
"broadcast_type": "super_gift",
|
||
"scope": "region",
|
||
"app_code": "lalu",
|
||
"region_id": 1001,
|
||
"room_id": "lalu_room_xxx",
|
||
"sender_user_id": 42,
|
||
"sent_at_ms": 1770000000000,
|
||
"action": {
|
||
"type": "enter_room",
|
||
"room_id": "lalu_room_xxx"
|
||
}
|
||
}
|
||
```
|
||
|
||
贵重礼物播报:
|
||
|
||
```json
|
||
{
|
||
"event_id": "gift_broadcast:lalu_room_xxx:cmd_send_gift_1",
|
||
"broadcast_type": "super_gift",
|
||
"scope": "region",
|
||
"app_code": "lalu",
|
||
"region_id": 1001,
|
||
"room_id": "lalu_room_xxx",
|
||
"sender_user_id": 42,
|
||
"target_user_id": 43,
|
||
"gift_id": "gift_rocket",
|
||
"gift_count": 1,
|
||
"gift_value": 999999,
|
||
"room_title": "Room title",
|
||
"sent_at_ms": 1770000000000,
|
||
"action": {
|
||
"type": "enter_room",
|
||
"room_id": "lalu_room_xxx"
|
||
}
|
||
}
|
||
```
|
||
|
||
红包播报:
|
||
|
||
```json
|
||
{
|
||
"event_id": "red_packet:rp_1000001",
|
||
"broadcast_type": "red_packet",
|
||
"scope": "region",
|
||
"app_code": "lalu",
|
||
"region_id": 1001,
|
||
"room_id": "lalu_room_xxx",
|
||
"red_packet_id": "rp_1000001",
|
||
"sender_user_id": 42,
|
||
"sent_at_ms": 1770000000000,
|
||
"action": {
|
||
"type": "open_red_packet",
|
||
"red_packet_id": "rp_1000001",
|
||
"room_id": "lalu_room_xxx"
|
||
}
|
||
}
|
||
```
|
||
|
||
客户端点击播报进入房间时,仍必须走正常房间入口:
|
||
|
||
```text
|
||
JoinRoom(room_id) -> join room IM group -> RTC token if needed
|
||
```
|
||
|
||
播报消息不能直接授予进房、抢红包或发言权限。
|
||
|
||
## Server Broadcast Sending
|
||
|
||
建议 `activity-service` 增加 `BroadcastService` 内部能力:
|
||
|
||
```text
|
||
PublishRegionBroadcast(app_code, region_id, payload)
|
||
PublishGlobalBroadcast(app_code, payload)
|
||
EnsureBroadcastGroups(app_code)
|
||
```
|
||
|
||
实现规则:
|
||
|
||
- 发送前通过 `pkg/imgroup` 生成 GroupID。
|
||
- payload 必须包含稳定 `event_id`。
|
||
- REST 发送失败不能影响原业务事实提交。
|
||
- 重试必须依赖本地 outbox,不能只依赖 Tencent REST 返回。
|
||
|
||
建议新增表:
|
||
|
||
```sql
|
||
CREATE TABLE im_broadcast_outbox (
|
||
app_code VARCHAR(32) NOT NULL,
|
||
event_id VARCHAR(128) NOT NULL,
|
||
scope VARCHAR(32) NOT NULL,
|
||
group_id VARCHAR(64) NOT NULL,
|
||
payload_json JSON NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
attempt_count INT NOT NULL DEFAULT 0,
|
||
next_retry_at_ms BIGINT NOT NULL DEFAULT 0,
|
||
last_error VARCHAR(512) NOT NULL DEFAULT '',
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
PRIMARY KEY (app_code, event_id),
|
||
KEY idx_im_broadcast_outbox_pending (app_code, status, next_retry_at_ms)
|
||
);
|
||
```
|
||
|
||
如果第一阶段不落表,也必须明确只支持 best-effort 实时播报;红包入口和重要运营消息不能依赖 best-effort。
|
||
|
||
## High Value Gift Broadcast
|
||
|
||
推荐链路:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as Client
|
||
participant G as gateway-service
|
||
participant R as room-service
|
||
participant W as wallet-service
|
||
participant MQ as RocketMQ
|
||
participant A as activity-service
|
||
participant IM as Tencent IM
|
||
|
||
C->>G: POST /api/v1/rooms/gift/send
|
||
G->>R: SendGift
|
||
R->>W: DebitGift
|
||
W-->>R: receipt + settlement
|
||
R->>R: Room Cell heat/rank + RoomGiftSent outbox
|
||
R-->>G: SendGiftResponse
|
||
G-->>C: room result
|
||
R->>MQ: publish hyapp_room_outbox when MQ mode is enabled
|
||
MQ->>A: consume RoomGiftSent by hyapp-activity-room-outbox
|
||
A->>A: threshold decision
|
||
A->>IM: send_group_msg(region broadcast group)
|
||
```
|
||
|
||
本地未启用 MQ 时,`room-service` outbox worker 可以 direct gRPC 投递 activity-service;线上推荐使用 RocketMQ,activity-service 不需要回查 room-service 主链路。
|
||
|
||
阈值策略属于播报配置,不属于 Room Cell:
|
||
|
||
| Config | Meaning |
|
||
| --- | --- |
|
||
| `super_gift_min_value` | 单次礼物总价值达到该值才播报 |
|
||
| `broadcast_scope` | `region` by default, `global` only for special campaign |
|
||
| `cooldown_ms` | 同一房间/同一用户短时间内去重,避免刷屏 |
|
||
|
||
`RoomGiftSent` 必须携带足够字段让 activity-service 判断:
|
||
|
||
- `room_id`
|
||
- `visible_region_id`
|
||
- `sender_user_id`
|
||
- `target_user_id`
|
||
- `gift_id`
|
||
- `gift_count`
|
||
- `gift_value` or `gift_point_added`
|
||
- `command_id`
|
||
- `room_version`
|
||
|
||
如果现有事件字段不足,应优先扩展 protobuf event,而不是让 activity-service 反查多个服务拼凑事实。
|
||
|
||
## Room Treasure Broadcast
|
||
|
||
宝箱满能量进入倒计时时,Room Cell 写 `RoomTreasureCountdownStarted` outbox。activity-service 消费该事实后按后台配置决定发区域播报还是全局播报;开箱到点、奖励结算和宝箱状态推进仍由 room-service 负责。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as Client
|
||
participant G as gateway-service
|
||
participant R as room-service
|
||
participant W as wallet-service
|
||
participant MQ as RocketMQ
|
||
participant A as activity-service
|
||
participant IM as Tencent IM
|
||
|
||
C->>G: POST /api/v1/rooms/gift/send
|
||
G->>R: SendGift
|
||
R->>W: DebitGift
|
||
W-->>R: receipt + heat/gift point
|
||
R->>R: add effective treasure energy
|
||
R->>R: status=countdown when threshold reached
|
||
R-->>G: SendGiftResponse
|
||
R->>MQ: RoomTreasureCountdownStarted
|
||
MQ->>A: consume by hyapp-activity-room-outbox
|
||
A->>IM: send_group_msg(region/global broadcast group)
|
||
```
|
||
|
||
播报 payload 的核心字段:
|
||
|
||
```json
|
||
{
|
||
"event_id": "room_treasure:lalu_room_xxx:box_1",
|
||
"broadcast_type": "room_treasure",
|
||
"scope": "region",
|
||
"app_code": "lalu",
|
||
"region_id": 1001,
|
||
"room_id": "lalu_room_xxx",
|
||
"box_id": "box_1",
|
||
"level": 3,
|
||
"open_at_ms": 1770000000000,
|
||
"action": {
|
||
"type": "enter_room",
|
||
"room_id": "lalu_room_xxx"
|
||
}
|
||
}
|
||
```
|
||
|
||
倒计时期间继续送礼不会累加到下一宝箱,溢出能量也不会跨级;这些规则由 room-service 在 `RoomTreasureProgressChanged` 和 `RoomTreasureCountdownStarted` 事实中表达,activity-service 不能自行推导宝箱进度。
|
||
|
||
## Red Packet Broadcast
|
||
|
||
红包资金事实必须由 wallet/red-packet 领域持久化:
|
||
|
||
| Fact | Owner |
|
||
| --- | --- |
|
||
| 红包创建、总金额、份数 | wallet/red-packet domain |
|
||
| 抢红包并发扣减 | wallet/red-packet domain |
|
||
| 领取记录 | wallet/red-packet domain |
|
||
| 过期退款 | wallet/red-packet domain + cron |
|
||
| IM 播报 | activity-service broadcast |
|
||
|
||
红包创建成功后,红包领域写 `RedPacketCreated` 事件,activity-service 转成区域/全局播报。客户端点击播报只打开红包入口,领取时必须调用后端接口,不能信任 IM payload 中的金额。
|
||
|
||
## App Code And Region Safety
|
||
|
||
- `app_code` 必须从 request context 或服务配置取得,不能从客户端 body 读取。
|
||
- 区域群 join 校验必须使用 user-service 当前 `region_id`。
|
||
- 用户改国家/区域后,应在下一次 token refresh 或 profile refresh 中返回新区域群。
|
||
- 旧区域群成员可能短期残留,因此敏感消息不能只靠 IM 群成员做授权;客户端点击后的后端接口仍要校验当前用户区域/资格。
|
||
|
||
## Observability
|
||
|
||
必须打点:
|
||
|
||
| Metric / Log | Labels |
|
||
| --- | --- |
|
||
| `im_broadcast_group_ensure_total` | `app_code, group_type, result` |
|
||
| `im_broadcast_send_total` | `app_code, scope, result, broadcast_type` |
|
||
| `im_broadcast_outbox_pending` | `app_code, scope` |
|
||
| `im_callback_route_total` | `group_type, command, result` |
|
||
| `im_callback_region_denied_total` | `app_code, region_id, reason` |
|
||
|
||
日志必须包含 `request_id/event_id/group_id/app_code/region_id`,不能包含 Tencent SecretKey、UserSig 或完整私密响应。
|
||
|
||
## Implementation Sequence
|
||
|
||
1. 新增 `pkg/imgroup`,覆盖 global/region/room GroupID 生成和解析单测。
|
||
2. 扩展 `pkg/tencentim.RESTClient`,增加通用 `EnsureGroup` 和 `PublishGroupCustomMessage`,保留现有 `EnsureRoomGroup/PublishRoomEvent` wrapper。
|
||
3. `gateway-service` 扩展 `/api/v1/im/usersig`,查询 user-service 并返回 `join_groups`。
|
||
4. `gateway-service` 改造 Tencent IM callback 分流,房间群和播报群走不同 guard。
|
||
5. `activity-service` 增加 broadcast group reconciler,启动时确保全局和 active 区域群。
|
||
6. `activity-service` 增加 `PublishRegionBroadcast/PublishGlobalBroadcast` 和 broadcast outbox。
|
||
7. 扩展 `RoomGiftSent` 事件字段,activity-service 消费后按阈值发 `super_gift` 区域播报。
|
||
8. 消费 `RoomTreasureCountdownStarted`,按后台配置发 `room_treasure` 区域或全局播报。
|
||
9. 红包领域落账务事实后,复用同一播报能力发送 `red_packet` 消息。
|
||
|
||
## Test Expectations
|
||
|
||
必须覆盖:
|
||
|
||
| Test | Expected |
|
||
| --- | --- |
|
||
| GroupID parse/generate | global、region、room 可稳定识别;非法 region 拒绝 |
|
||
| `/im/usersig` | 已完成资料用户返回 global + current region group |
|
||
| IM callback room group | 仍走 `VerifyRoomPresence` / `CheckSpeakPermission` |
|
||
| IM callback region join | region 匹配允许,不匹配拒绝 |
|
||
| IM callback broadcast send | 普通用户拒绝,服务端 sender 允许 |
|
||
| group reconciler | 已存在群视为成功,失败可重试 |
|
||
| broadcast outbox | 同一 `event_id` 幂等,失败后 retry |
|
||
| super gift | 未达阈值不播报,达阈值发送到用户房间所在区域群 |
|
||
| room treasure | 宝箱满能量后只按 `RoomTreasureCountdownStarted` 播报一次;区域/全局 scope 来自后台配置 |
|
||
| red packet | IM payload 不影响领取接口的后端金额校验 |
|