1) 一条“房间时间线(Room-Timeline)”
-
所有进入直播间的事件(聊天、进场、点赞、礼物、系统公告……)都写入同一条按房间有序的时间线,每条事件都有:
- roomId, roomSeq(单房间严格递增), messageId, ts
- 类型枚举 eventType
- 载荷 payload(oneof)
-
区分礼物与普通消息,本质就是看 eventType(以及 source)和对应的 payload schema。
2) 区分策略(两层)
传输信封层(Envelope,所有事件通用)
{
"roomId":"r1",
"roomSeq":10543,
"messageId":"m_...","schemaVer":2,
"eventType":"GIFT" | "CHAT_TEXT" | "CHAT_EMOJI" | "SYSTEM" | "ENTER" | "LIKE" | ...,
"source":"SERVER" | "CLIENT",
"ts": 1731301123123,
"payload":{...}
}
业务载荷层(强类型 oneof)****
-
eventType = CHAT_TEXT → payload = { text, sender, atList, ... }
-
eventType = GIFT → payload = { giftEventId, orderId, giftId, count, price, payer, comboId, comboSeq, ... }
-
其它类型同理
结论:识别礼物= eventType == GIFT(且 source=SERVER);识别普通消息= eventType ∈ {CHAT_TEXT, CHAT_EMOJI, ...}(通常 source=CLIENT)。
3) 单流 vs 双流
- 单流(推荐) :统一时间线(更好排序与补偿)。客户端按 eventType 分支渲染。
- 双流(可选) :IM 文本一流、礼物/交易一流,入房时并行补偿后在端上合并按 roomSeq 排序。适合礼物吞吐高时解耦服务。
4) Schema 示例(Proto)
enum EventType { CHAT_TEXT=0; GIFT=1; SYSTEM=2; ENTER=3; LIKE=4; }
message Envelope {
string roomId = 1;
uint64 roomSeq = 2;
string messageId = 3;
EventType eventType = 4;
string source = 5; // SERVER / CLIENT
int64 ts = 6;
oneof body {
ChatText chatText = 10;
GiftEvent gift = 11;
SystemMsg system = 12;
}
}
message ChatText { string senderId=1; string text=2; repeated string at=3; }
message GiftEvent {
string giftEventId = 1; // = orderId,幂等键
string orderId = 2;
string payerId = 3;
string giftId = 4;
uint32 count = 5;
string currency = 6;
uint64 priceTotal = 7; // 分/厘等最小币值
string comboId = 8; // 连击聚合用
uint32 comboSeq = 9;
bytes serverSig = 99; // 服务器签名,防伪造
}
5) 服务器怎么保证“礼物一定是礼物”
- 礼物事件只能由服务器产生:支付/扣费成功 → Outbox 同事务写出 GiftEvent → 推送到 Room-Timeline。
- 防伪造:礼物事件携带 serverSig 或走受信通道(WS 服务端注入),拒绝客户端自报礼物。
- 幂等:giftEventId = orderId;下游计数榜单、端侧渲染都按此 去重。
- 离线补偿:入房带 sinceSeq 拉取 [sinceSeq+1, hiSeq] 段,礼物与聊天一起补齐。
6) 客户端处理流程(伪码)
when (env.eventType) {
CHAT_TEXT -> renderChat(env.chatText)
GIFT -> if (verifyServerSig(env.gift)) {
if (dedupSet.add(env.gift.giftEventId)) renderGiftAnim(env.gift)
giftCounter.merge(env.gift) // 更新礼物计数/榜单
}
SYSTEM -> renderSystem(env.system)
}
- 去重窗口:礼物用 giftEventId,聊天用 messageId(或服务器生成)。
- 聚合/节流:补偿阶段的礼物合并连击,动画队列限长,超出只更计数。
7) 细节与边界
- 排序:统一用 roomSeq,不要用客户端时间。
- 分层语义:可再加 category(PERSISTENT vs TRANSIENT),把“点赞心跳”类事件标记为非持久,入房仅给快照。
- 多端一致:如果用户多设备登录,服务器可维护 lastSeenSeq(user, room),入房先对齐。
- 鉴权/风控:聊天走敏感词/风控管道;礼物走订单风控,两条独立链最终汇入时间线。
8) 速查清单
- 区分礼物/普通消息的唯一可靠方式:eventType + 服务器生成/签名。
- 礼物幂等键:giftEventId(=orderId);端/服都要去重。
- 离线补偿:统一 roomSeq,入房带 sinceSeq 精准补。
- 渲染:礼物动画可聚合、限队列;聊天直接落列表。
- 安全:客户端不得直接发送 eventType=GIFT;即使发了也被网关丢弃。