feat(call): enhance call handling with user connection events and active call management
- Updated CallService to handle user connection and disconnection events, allowing for automatic management of active calls based on user status. - Introduced methods to retrieve active calls for users and send pending call invites when users reconnect. - Modified the Invite method to return the callee's online status, improving user experience during call invitations. - Enhanced WSHandler to support connection and disconnection event handlers, ensuring accurate call state management.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"carrot_bbs/internal/pkg/ws"
|
||||
"carrot_bbs/internal/repository"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -33,7 +34,7 @@ const (
|
||||
|
||||
// CallService 通话服务接口
|
||||
type CallService interface {
|
||||
Invite(ctx context.Context, callerID, calleeID, conversationID, mediaType string) (*model.CallSession, error)
|
||||
Invite(ctx context.Context, callerID, calleeID, conversationID, mediaType string) (*model.CallSession, bool, error)
|
||||
Accept(ctx context.Context, callID, userID string) (*model.CallSession, error)
|
||||
Reject(ctx context.Context, callID, userID string) error
|
||||
Busy(ctx context.Context, callID, userID string) error
|
||||
@@ -42,7 +43,7 @@ type CallService interface {
|
||||
SetMuted(ctx context.Context, callID, userID string, muted bool) error
|
||||
GetCallHistory(ctx context.Context, userID string, page, pageSize int) ([]model.CallSession, int64, error)
|
||||
GetICEServers() []config.ICEServerConfig
|
||||
StartCleanupTicker() // 新增:启动超时清理
|
||||
StartCleanupTicker()
|
||||
}
|
||||
|
||||
type callService struct {
|
||||
@@ -66,21 +67,22 @@ func NewCallService(
|
||||
db: db,
|
||||
}
|
||||
|
||||
// 订阅 WebSocket 断开事件
|
||||
hub.OnDisconnect(svc.handleUserDisconnect)
|
||||
|
||||
// 订阅 WebSocket 连接事件
|
||||
hub.OnConnect(svc.handleUserConnect)
|
||||
|
||||
// 自动启动超时清理
|
||||
svc.StartCleanupTicker()
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func (s *callService) Invite(ctx context.Context, callerID, calleeID, conversationID, mediaType string) (*model.CallSession, error) {
|
||||
// === Telegram: 检查被叫方是否在线 ===
|
||||
if !s.hub.HasClients(calleeID) {
|
||||
return nil, ErrCalleeOffline
|
||||
}
|
||||
|
||||
func (s *callService) Invite(ctx context.Context, callerID, calleeID, conversationID, mediaType string) (*model.CallSession, bool, error) {
|
||||
active, err := s.callRepo.GetActiveCallByConversationID(conversationID)
|
||||
if err == nil && active != nil {
|
||||
return nil, fmt.Errorf("%w: call %s", ErrCallInProgress, active.ID)
|
||||
return nil, false, fmt.Errorf("%w: call %s", ErrCallInProgress, active.ID)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
@@ -99,14 +101,17 @@ func (s *callService) Invite(ctx context.Context, callerID, calleeID, conversati
|
||||
}
|
||||
|
||||
if err := s.callRepo.CreateCallWithParticipants(ctx, call, participants); err != nil {
|
||||
return nil, fmt.Errorf("create call session: %w", err)
|
||||
return nil, false, fmt.Errorf("create call session: %w", err)
|
||||
}
|
||||
|
||||
call, err = s.callRepo.GetCallByIDWithParticipants(call.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get call with participants: %w", err)
|
||||
return nil, false, fmt.Errorf("get call with participants: %w", err)
|
||||
}
|
||||
|
||||
// 检查被叫方是否在线
|
||||
calleeOnline := s.hub.HasClients(calleeID)
|
||||
|
||||
// === Element: 包含 lifetime 字段 ===
|
||||
payload := map[string]interface{}{
|
||||
"call_id": call.ID,
|
||||
@@ -119,16 +124,29 @@ func (s *callService) Invite(ctx context.Context, callerID, calleeID, conversati
|
||||
"ice_servers": s.config.WebRTC.ICEServers,
|
||||
}
|
||||
|
||||
// === Telegram: 不存 history ===
|
||||
// 尝试向被叫方发送来电通知
|
||||
// 如果对方在线,立即收到来电
|
||||
// 如果对方不在线,依然返回成功,让呼叫方正常等待
|
||||
// 后端超时清理机制会在超时后自动结束通话并通知呼叫方
|
||||
online := s.hub.PublishToUserOnline(calleeID, "call_incoming", payload)
|
||||
if !online {
|
||||
call.Status = model.CallStatusCancelled
|
||||
call.EndedAt = &now
|
||||
s.callRepo.UpdateCall(call)
|
||||
return nil, ErrCalleeOffline
|
||||
|
||||
if online {
|
||||
zap.L().Debug("Call invite sent to online callee",
|
||||
zap.String("call_id", call.ID),
|
||||
zap.String("callee_id", calleeID),
|
||||
zap.Bool("online", true),
|
||||
)
|
||||
} else {
|
||||
zap.L().Debug("Call invite created for offline callee, waiting for reconnect",
|
||||
zap.String("call_id", call.ID),
|
||||
zap.String("callee_id", calleeID),
|
||||
zap.Bool("online", false),
|
||||
)
|
||||
}
|
||||
|
||||
return call, nil
|
||||
// 返回通话会话和被叫方在线状态
|
||||
// 前端可以根据 calleeOnline 决定是否显示"等待对方上线"的提示
|
||||
return call, calleeOnline, nil
|
||||
}
|
||||
|
||||
func (s *callService) Accept(ctx context.Context, callID, userID string) (*model.CallSession, error) {
|
||||
@@ -416,3 +434,122 @@ func isParticipant(call *model.CallSession, userID string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// handleUserDisconnect 处理用户 WebSocket 断开连接事件
|
||||
// 当用户完全离线时(remainingCount == 0),结束该用户参与的所有活跃通话
|
||||
func (s *callService) handleUserDisconnect(userID string, remainingCount int) {
|
||||
// 如果用户还有其他连接,不处理
|
||||
if remainingCount > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 获取用户参与的所有活跃通话
|
||||
calls, err := s.callRepo.GetActiveCallsByUserID(userID)
|
||||
if err != nil {
|
||||
zap.L().Error("Failed to get active calls for disconnected user",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if len(calls) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
zap.L().Info("User disconnected, ending active calls",
|
||||
zap.String("user_id", userID),
|
||||
zap.Int("active_calls", len(calls)),
|
||||
)
|
||||
|
||||
// 结束所有活跃通话
|
||||
for _, call := range calls {
|
||||
_, err := s.End(ctx, call.ID, userID, "disconnect")
|
||||
if err != nil {
|
||||
zap.L().Error("Failed to end call on user disconnect",
|
||||
zap.String("user_id", userID),
|
||||
zap.String("call_id", call.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleUserConnect 处理用户 WebSocket 连接事件
|
||||
// 当用户从离线状态变为在线时,检查是否有待处理的通话邀请并推送
|
||||
func (s *callService) handleUserConnect(userID string, isFirstConnection bool) {
|
||||
// 只有当用户是从完全离线变为在线时才处理
|
||||
if !isFirstConnection {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户作为被叫方的所有活跃通话
|
||||
calls, err := s.callRepo.GetActiveCallsByUserID(userID)
|
||||
if err != nil {
|
||||
zap.L().Error("Failed to get active calls for connected user",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if len(calls) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
zap.L().Info("User connected, sending pending call invites",
|
||||
zap.String("user_id", userID),
|
||||
zap.Int("pending_calls", len(calls)),
|
||||
)
|
||||
|
||||
// 推送所有待处理的通话邀请
|
||||
for _, call := range calls {
|
||||
// 检查用户是否是被叫方(status == invited)
|
||||
isCallee := false
|
||||
for _, p := range call.Participants {
|
||||
if p.UserID == userID && p.Status == model.ParticipantStatusInvited {
|
||||
isCallee = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isCallee {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查通话是否还在有效期内
|
||||
elapsed := time.Since(call.CreatedAt)
|
||||
if elapsed.Milliseconds() > CallLifetimeMs {
|
||||
zap.L().Debug("Skipping expired call invite",
|
||||
zap.String("call_id", call.ID),
|
||||
zap.Duration("elapsed", elapsed),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
remainingLifetime := CallLifetimeMs - elapsed.Milliseconds()
|
||||
|
||||
// 推送来电通知
|
||||
payload := map[string]interface{}{
|
||||
"call_id": call.ID,
|
||||
"conversation_id": call.ConversationID,
|
||||
"caller_id": call.CallerID,
|
||||
"call_type": call.CallType,
|
||||
"media_type": "voice", // 默认语音通话
|
||||
"created_at": call.CreatedAt.UnixMilli(),
|
||||
"lifetime": remainingLifetime,
|
||||
"ice_servers": s.config.WebRTC.ICEServers,
|
||||
}
|
||||
|
||||
sent := s.hub.PublishToUserOnline(userID, "call_incoming", payload)
|
||||
if sent {
|
||||
zap.L().Debug("Pending call invite sent to newly connected user",
|
||||
zap.String("call_id", call.ID),
|
||||
zap.String("user_id", userID),
|
||||
zap.Int64("remaining_lifetime_ms", remainingLifetime),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user