diff --git a/backend/internal/modules/chat/message.go b/backend/internal/modules/chat/message.go index e530689..f422074 100644 --- a/backend/internal/modules/chat/message.go +++ b/backend/internal/modules/chat/message.go @@ -136,7 +136,7 @@ func (r *Repository) SendMessage(ctx context.Context, principal Principal, conve // 推送新消息事件给会话中的在线参与者 if r.hub != nil { msg := items[0] - r.hub.NotifyConversation(conversationID, &chathub.ChatEvent{ + event := &chathub.ChatEvent{ Type: "new_message", ConversationID: conversationID, Message: &chathub.MessageData{ @@ -151,7 +151,11 @@ func (r *Repository) SendMessage(ctx context.Context, principal Principal, conve AttachmentURLS: msg.AttachmentURLS, CreatedAt: msg.CreatedAt.Format(time.RFC3339), }, - }) + } + r.hub.NotifyConversation(conversationID, event) + // 同时广播给所有在线客服,使「未分配 / 全部」视图实时刷新; + // 参与者客服会重复收到,由前端按消息 id 去重。 + r.hub.NotifyAllAdmins(event) } return &items[0], nil } diff --git a/backend/internal/modules/chat/repository.go b/backend/internal/modules/chat/repository.go index 9bb8b67..21d73e1 100644 --- a/backend/internal/modules/chat/repository.go +++ b/backend/internal/modules/chat/repository.go @@ -102,8 +102,11 @@ func (r *Repository) NotifyNewConversation(conversationID uint64) { if r.hub == nil { return } - r.hub.NotifyConversation(conversationID, &chathub.ChatEvent{ + event := &chathub.ChatEvent{ Type: "conversation_updated", ConversationID: conversationID, - }) + } + r.hub.NotifyConversation(conversationID, event) + // 新会话(如未分配的平台咨询)需让所有在线客服的列表实时出现。 + r.hub.NotifyAllAdmins(event) } diff --git a/backend/internal/modules/chathub/hub.go b/backend/internal/modules/chathub/hub.go index 53e8b84..4f33090 100644 --- a/backend/internal/modules/chathub/hub.go +++ b/backend/internal/modules/chathub/hub.go @@ -135,6 +135,25 @@ func (h *Hub) NotifyUser(pType string, pID uint64, event *ChatEvent) { } } +// NotifyAllAdmins 向所有在线管理员(客服)推送事件。 +// 客服工作台的「未分配 / 全部」视图会展示当前客服并非参与者的会话, +// 仅靠 NotifyConversation(只推参与者)无法实时刷新,故对所有在线客服广播。 +func (h *Hub) NotifyAllAdmins(event *ChatEvent) { + h.mu.RLock() + defer h.mu.RUnlock() + for key, clients := range h.clients { + if key.Type != "admin" { + continue + } + for ch := range clients { + select { + case ch <- event: + default: + } + } + } +} + // MarshalEvent 将事件序列化为 SSE data 行。 func MarshalEvent(event *ChatEvent) string { raw, _ := json.Marshal(event) diff --git a/frontend/src/features/chats/composables/useChatSSE.ts b/frontend/src/features/chats/composables/useChatSSE.ts index 0f67518..4f65fce 100644 --- a/frontend/src/features/chats/composables/useChatSSE.ts +++ b/frontend/src/features/chats/composables/useChatSSE.ts @@ -40,10 +40,12 @@ export function useChatSSE(scope: AuthScope, endpoint: string) { function connect() { if (stopped || source) return const token = getAccessToken(scope) - if (!token) return + // 用户端通过 query token 鉴权;后台 token 存于 httpOnly cookie, + // localStorage 无 token,连接时依赖浏览器自动携带的 cookie,不能因空 token 提前返回。 + if (scope !== 'admin' && !token) return - const url = `${endpoint}?token=${encodeURIComponent(token)}` - source = new EventSource(url) + const url = scope === 'admin' ? endpoint : `${endpoint}?token=${encodeURIComponent(token)}` + source = new EventSource(url, { withCredentials: true }) source.addEventListener('connected', () => { connected.value = true