直播间的IM消息是如何区分礼物消息和正常消息的

70 阅读3分钟

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;即使发了也被网关丢弃。