From cc1c0d33de8fb1a9a01e80619bc7344e00ce02f6 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 16 Jun 2026 19:57:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=A2=E6=9C=8D=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E8=81=8A=E5=A4=A9=E6=97=A0=E5=AE=9E=E6=97=B6=E5=88=B7?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后台 token 仅存 httpOnly cookie,localStorage 无 access token, useChatSSE 在空 token 时提前 return,导致后台 SSE 从未建立连接, 只有手动刷新才出消息。 - 前端:后台 SSE 不再因空 token 返回,去掉 query token, 改用 withCredentials 依赖 cookie 鉴权 - 后端:新增 Hub.NotifyAllAdmins,新消息与新会话广播给所有在线客服, 使「未分配 / 全部」视图实时刷新(参与者客服重复收到由前端按 id 去重) Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/internal/modules/chat/message.go | 8 ++++++-- backend/internal/modules/chat/repository.go | 7 +++++-- backend/internal/modules/chathub/hub.go | 19 +++++++++++++++++++ .../features/chats/composables/useChatSSE.ts | 8 +++++--- 4 files changed, 35 insertions(+), 7 deletions(-) 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