feat(call): implement group calling and enhance call management
All checks were successful
Build Backend / build (push) Successful in 2m14s
Build Backend / build-docker (push) Successful in 1m15s

Introduces group calling capabilities, including group invites and participant
lifecycle management. Enhances the existing call system with support for
media types (voice/video) and improved participant tracking.

- Add `GroupInvite` and `ParticipantJoin/Leave` to `CallService`.
- Implement WebSocket handlers for `call_group_invite` and
  `call_participant_join`.
- Update `CallSession` model to support group IDs, media types, and
  tracking who ended the call.
- Integrate `GroupService` into `CallService` via dependency injection.
- Add error constants for group call limitations and participant capacity.
- Update LiveKit webhook handling to process participant leave events.

Refactor grade synchronization to remove semester-based filtering in favor
of a more flexible user-based approach and improve GPA summary updates.
This commit is contained in:
2026-06-07 00:11:47 +08:00
parent c741a7d61d
commit 60654bc0f4
11 changed files with 394 additions and 69 deletions

View File

@@ -377,6 +377,8 @@ func (h *WSHandler) handleMessage(client *ws.Client, msg *ws.Message) {
// 通话信令
case "call_invite":
h.handleCallInvite(ctx, client, msg.Payload)
case "call_group_invite":
h.handleCallGroupInvite(ctx, client, msg.Payload)
case "call_answer":
h.handleCallAnswer(ctx, client, msg.Payload)
case "call_reject":
@@ -389,6 +391,8 @@ func (h *WSHandler) handleMessage(client *ws.Client, msg *ws.Message) {
h.handleCallEnd(ctx, client, msg.Payload)
case "call_mute":
h.handleCallMute(ctx, client, msg.Payload)
case "call_participant_join":
h.handleCallParticipantJoin(ctx, client, msg.Payload)
default:
h.publisher.SendError(client, "unknown_type", fmt.Sprintf("unknown message type: %s", msg.Type))
}
@@ -648,6 +652,61 @@ func (h *WSHandler) handleCallInvite(ctx context.Context, client *ws.Client, pay
}
}
// handleCallGroupInvite 处理群组通话邀请
func (h *WSHandler) handleCallGroupInvite(ctx context.Context, client *ws.Client, payload json.RawMessage) {
if !h.isVerified(ctx, client) {
return
}
var req struct {
GroupID string `json:"group_id"`
ConversationID string `json:"conversation_id"`
MediaType string `json:"call_type"`
}
if err := json.Unmarshal(payload, &req); err != nil {
h.publisher.SendError(client, "parse_error", "invalid call_group_invite format")
return
}
if req.GroupID == "" || req.ConversationID == "" {
h.publisher.SendError(client, "invalid_params", "group_id and conversation_id are required")
return
}
mediaType := req.MediaType
if mediaType != "video" {
mediaType = "voice"
}
call, err := h.callService.GroupInvite(ctx, client.UserID, req.GroupID, req.ConversationID, mediaType)
if err != nil {
zap.L().Warn("Failed to invite group call",
zap.String("caller_id", client.UserID),
zap.String("group_id", req.GroupID),
zap.Error(err),
)
h.publisher.SendError(client, "call_group_invite_failed", err.Error())
return
}
resp := ws.ResponseMessage{
EventID: h.publisher.NextID(),
Type: "call_invited",
TS: time.Now().UnixMilli(),
Payload: map[string]any{
"call_id": call.ID,
"conversation_id": req.ConversationID,
"group_id": req.GroupID,
"call_type": "group",
"lifetime": service.CallLifetimeMs,
},
}
data, _ := json.Marshal(resp)
select {
case <-client.Quit:
case client.Send <- data:
}
}
// handleCallAnswer 处理接听
func (h *WSHandler) handleCallAnswer(ctx context.Context, client *ws.Client, payload json.RawMessage) {
var req struct {
@@ -778,3 +837,18 @@ func (h *WSHandler) handleCallMute(ctx context.Context, client *ws.Client, paylo
h.publisher.SendError(client, "call_mute_failed", err.Error())
}
}
// handleCallParticipantJoin 处理群组通话参与者加入
func (h *WSHandler) handleCallParticipantJoin(ctx context.Context, client *ws.Client, payload json.RawMessage) {
var req struct {
CallID string `json:"call_id"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.CallID == "" {
h.publisher.SendError(client, "invalid_params", "call_id is required")
return
}
if err := h.callService.ParticipantJoin(ctx, req.CallID, client.UserID); err != nil {
h.publisher.SendError(client, "call_participant_join_failed", err.Error())
}
}