Files
EchoChat/backend/go-service/app/meeting/controller/meeting_ws_handler.go
bujinyuan 096563d3ea feat(phase2e-2): WebSocket 信令协议 13 事件全量落地(Task 6)
将 meeting.* 事件族从 Task 5 的 PublishToUser 循环升级为完整 WS 信令协议:
抽离统一广播层、扩容 MediaOrchestrator 接口、实现 8 个 C→S 事件业务逻辑
+ Redis 资源追踪 + host 权限校验,端到端 18/18 PASS。

核心产出:
- 新建 MeetingBroadcaster(统一广播层,REST/WS 共用)
- 新建 MeetingSignalService 8 C→S 事件 + cleanupUserResources
- 新建 MeetingWSHandler 薄层 controller
- MediaOrchestrator 扩容 9 方法 + NoopMediaOrchestrator 占位(Task 7 替换)
- C→S 白名单机制防恶意伪造广播事件
- Redis Set 资源追踪防 mediasoup 端资源泄漏

文档同步:
- docs/api/frontend/meeting.md 追加 §WebSocket 信令协议(Task 6)200 行
- docs/progress/CURRENT_STATUS.md + project-context.mdc + 实施计划 Task 6 

Made-with: Cursor
2026-04-21 17:10:32 +08:00

197 lines
6.7 KiB
Go
Raw 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.

// Package controller 会议模块 HTTP / WS 入口
package controller
import (
"context"
"encoding/json"
"github.com/echochat/backend/app/constants"
"github.com/echochat/backend/app/meeting/service"
"github.com/echochat/backend/pkg/logs"
"github.com/echochat/backend/pkg/ws"
"go.uber.org/zap"
)
// MeetingWSHandler 会议 WS 事件入口
// Task 6 落地:注册设计 §6.3 的 11 个 meeting.* 事件到 Hub 路由表
// 薄层设计:
// - 仅负责 JSON Unmarshal → 调 SignalService 业务方法 → 组装 ACK 响应
// - 权限校验全部下沉到 SignalServiceservice 层统一 assertIsActiveParticipant / assertIsHost
// - 广播副作用由 SignalService 自行发起ACK 不携带广播数据)
type MeetingWSHandler struct {
signalSvc *service.MeetingSignalService
hub *ws.Hub
}
// NewMeetingWSHandler 构造 WS Handler 并自动注册事件到 Hub
// 与 im.handler.EventHandler 保持一致的风格:构造时注册,启动时由 DI 容器持有
func NewMeetingWSHandler(signalSvc *service.MeetingSignalService, hub *ws.Hub) *MeetingWSHandler {
h := &MeetingWSHandler{
signalSvc: signalSvc,
hub: hub,
}
h.registerEvents()
return h
}
// registerEvents 将所有 meeting.* C→S 事件注册到 Hub 路由表
// S→C 事件ended/joined/left/host.changed/producer.new 等)不在此处注册,由 broadcaster 发出
func (h *MeetingWSHandler) registerEvents() {
// 房间组
h.hub.RegisterEvent(constants.MeetingWSEventRoomJoin, h.handleRoomJoin)
h.hub.RegisterEvent(constants.MeetingWSEventRoomLeave, h.handleRoomLeave)
// 成员组
h.hub.RegisterEvent(constants.MeetingWSEventMemberStateChange, h.handleMemberStateChange)
// 媒体组5 个)
h.hub.RegisterEvent(constants.MeetingWSEventTransportCreate, h.handleTransportCreate)
h.hub.RegisterEvent(constants.MeetingWSEventTransportConnect, h.handleTransportConnect)
h.hub.RegisterEvent(constants.MeetingWSEventProduceStart, h.handleProduceStart)
h.hub.RegisterEvent(constants.MeetingWSEventConsumeStart, h.handleConsumeStart)
h.hub.RegisterEvent(constants.MeetingWSEventProducerClose, h.handleProducerClose)
}
// ====== 通用工具 ======
// simpleRoomPayload 房间组仅需 room_code 的请求体
type simpleRoomPayload struct {
RoomCode string `json:"room_code"`
}
// sendACK 统一发送 ACK 响应
func (h *MeetingWSHandler) sendACK(client *ws.Client, msg *ws.Message, code int, message string, data interface{}) {
resp := ws.NewResponse(msg.Event, msg.Seq, code, message, data)
bytes, err := ws.MarshalResponse(resp)
if err != nil {
logs.Error(context.Background(), "controller.meeting_ws_handler.sendACK", "序列化 ACK 失败",
zap.String("event", msg.Event), zap.Error(err))
return
}
client.Send(bytes)
}
// unmarshal 通用反序列化 + 错误 ACK
func (h *MeetingWSHandler) unmarshal(client *ws.Client, msg *ws.Message, target interface{}) bool {
if err := json.Unmarshal(msg.Data, target); err != nil {
logs.Warn(nil, "controller.meeting_ws_handler.unmarshal", "反序列化失败",
zap.String("event", msg.Event),
zap.Int64("user_id", client.UserID),
zap.Error(err))
h.sendACK(client, msg, -1, "请求参数格式错误", nil)
return false
}
return true
}
// ====== 房间组 ======
// handleRoomJoin 处理 meeting.room.join
func (h *MeetingWSHandler) handleRoomJoin(client *ws.Client, msg *ws.Message) {
var payload simpleRoomPayload
if !h.unmarshal(client, msg, &payload) {
return
}
if err := h.signalSvc.OnRoomJoin(context.Background(), client.UserID, payload.RoomCode); err != nil {
h.sendACK(client, msg, -1, err.Error(), nil)
return
}
h.sendACK(client, msg, 0, "ok", nil)
}
// handleRoomLeave 处理 meeting.room.leave
func (h *MeetingWSHandler) handleRoomLeave(client *ws.Client, msg *ws.Message) {
var payload simpleRoomPayload
if !h.unmarshal(client, msg, &payload) {
return
}
if err := h.signalSvc.OnRoomLeave(context.Background(), client.UserID, payload.RoomCode); err != nil {
h.sendACK(client, msg, -1, err.Error(), nil)
return
}
h.sendACK(client, msg, 0, "ok", nil)
}
// ====== 成员组 ======
// handleMemberStateChange 处理 meeting.member.state.changed
func (h *MeetingWSHandler) handleMemberStateChange(client *ws.Client, msg *ws.Message) {
var payload service.MemberStateChangePayload
if !h.unmarshal(client, msg, &payload) {
return
}
if err := h.signalSvc.OnMemberStateChanged(context.Background(), client.UserID, &payload); err != nil {
h.sendACK(client, msg, -1, err.Error(), nil)
return
}
h.sendACK(client, msg, 0, "ok", nil)
}
// ====== 媒体组 ======
// handleTransportCreate 处理 meeting.transport.create
func (h *MeetingWSHandler) handleTransportCreate(client *ws.Client, msg *ws.Message) {
var payload service.TransportCreatePayload
if !h.unmarshal(client, msg, &payload) {
return
}
info, err := h.signalSvc.OnTransportCreate(context.Background(), client.UserID, &payload)
if err != nil {
h.sendACK(client, msg, -1, err.Error(), nil)
return
}
h.sendACK(client, msg, 0, "ok", info)
}
// handleTransportConnect 处理 meeting.transport.connect
func (h *MeetingWSHandler) handleTransportConnect(client *ws.Client, msg *ws.Message) {
var payload service.TransportConnectPayload
if !h.unmarshal(client, msg, &payload) {
return
}
if err := h.signalSvc.OnTransportConnect(context.Background(), client.UserID, &payload); err != nil {
h.sendACK(client, msg, -1, err.Error(), nil)
return
}
h.sendACK(client, msg, 0, "ok", nil)
}
// handleProduceStart 处理 meeting.produce.start
func (h *MeetingWSHandler) handleProduceStart(client *ws.Client, msg *ws.Message) {
var payload service.ProduceStartPayload
if !h.unmarshal(client, msg, &payload) {
return
}
result, err := h.signalSvc.OnProduceStart(context.Background(), client.UserID, &payload)
if err != nil {
h.sendACK(client, msg, -1, err.Error(), nil)
return
}
h.sendACK(client, msg, 0, "ok", result)
}
// handleConsumeStart 处理 meeting.consume.start
func (h *MeetingWSHandler) handleConsumeStart(client *ws.Client, msg *ws.Message) {
var payload service.ConsumeStartPayload
if !h.unmarshal(client, msg, &payload) {
return
}
info, err := h.signalSvc.OnConsumeStart(context.Background(), client.UserID, &payload)
if err != nil {
h.sendACK(client, msg, -1, err.Error(), nil)
return
}
h.sendACK(client, msg, 0, "ok", info)
}
// handleProducerClose 处理 meeting.producer.close
func (h *MeetingWSHandler) handleProducerClose(client *ws.Client, msg *ws.Message) {
var payload service.ProducerClosePayload
if !h.unmarshal(client, msg, &payload) {
return
}
if err := h.signalSvc.OnProducerClose(context.Background(), client.UserID, &payload); err != nil {
h.sendACK(client, msg, -1, err.Error(), nil)
return
}
h.sendACK(client, msg, 0, "ok", nil)
}