From 92c603794ac769543b747f8243616e5733014268 Mon Sep 17 00:00:00 2001 From: yml Date: Fri, 5 Jun 2026 19:43:55 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E5=8F=AF=E4=BB=A5?= =?UTF-8?q?=E7=9C=8B=E5=88=B0=E6=89=80=E6=9C=89=E4=BC=9A=E8=AF=9D=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/chat/repository.go | 178 +++++++++++++++--- .../features/admin/views/AdminChatsView.vue | 25 ++- frontend/src/features/chats/api/chats.ts | 6 +- .../src/features/chats/views/ChatView.vue | 29 ++- .../features/chats/views/MobileChatView.vue | 29 ++- 5 files changed, 236 insertions(+), 31 deletions(-) diff --git a/backend/internal/modules/chat/repository.go b/backend/internal/modules/chat/repository.go index 6d2a1e2..b695320 100644 --- a/backend/internal/modules/chat/repository.go +++ b/backend/internal/modules/chat/repository.go @@ -149,6 +149,38 @@ func (r *Repository) ListConversations(principal Principal, page, pageSize int) } func (r *Repository) FindConversation(principal Principal, id uint64) (*ConversationDTO, error) { + // 管理员可以查看任意会话,无需是 participant + if principal.Type == "admin" { + var conversation model.ChatConversation + if err := r.db.First(&conversation, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrConversationNotFound + } + return nil, err + } + participants, err := r.participants(conversation.ID) + if err != nil { + return nil, err + } + dto := ConversationDTO{ + ID: conversation.ID, + OrderID: conversation.OrderID, + Type: conversation.Type, + Title: conversation.Title, + Status: conversation.Status, + Role: "admin", // 管理员角色 + Participants: participants, + LastMessageID: conversation.LastMessageID, + LastMessagePreview: conversation.LastMessagePreview, + LastMessageAt: conversation.LastMessageAt, + UnreadCount: 0, // 管理员不计未读 + CreatedAt: conversation.CreatedAt, + UpdatedAt: conversation.UpdatedAt, + } + return &dto, nil + } + + // 普通用户需要是 participant var row conversationRow err := r.conversationQuery(principal).Where("c.id = ?", id).First(&row).Error if err != nil { @@ -269,9 +301,23 @@ func (r *Repository) EnsureSupportConversation(userID uint64) (*ConversationDTO, func (r *Repository) Messages(principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) { page, pageSize = normalizePagination(page, pageSize) - if _, err := r.findParticipant(r.db, principal, conversationID, false); err != nil { - return nil, err + + // 管理员可以查看任意会话的消息,普通用户需要是 participant + if principal.Type != "admin" { + if _, err := r.findParticipant(r.db, principal, conversationID, false); err != nil { + return nil, err + } + } else { + // 管理员需要验证会话存在 + var count int64 + if err := r.db.Model(&model.ChatConversation{}).Where("id = ?", conversationID).Count(&count).Error; err != nil { + return nil, err + } + if count == 0 { + return nil, ErrConversationNotFound + } } + var total int64 if err := r.db.Model(&model.ChatMessage{}).Where("conversation_id = ?", conversationID).Count(&total).Error; err != nil { return nil, err @@ -295,10 +341,6 @@ func (r *Repository) Messages(principal Principal, conversationID uint64, page, func (r *Repository) SendMessage(principal Principal, conversationID uint64, req SendMessageRequest) (*MessageDTO, error) { var messageID uint64 err := r.db.Transaction(func(tx *gorm.DB) error { - participant, err := r.findParticipant(tx, principal, conversationID, true) - if err != nil { - return err - } var conversation model.ChatConversation if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&conversation, conversationID).Error; err != nil { return err @@ -306,11 +348,41 @@ func (r *Repository) SendMessage(principal Principal, conversationID uint64, req if conversation.Status != "active" { return ErrPermissionDenied } + + var senderRole string + var participant *model.ChatParticipant + + // 管理员可以在任意会话发送消息,无需是 participant + if principal.Type == "admin" { + // 尝试查找管理员的 participant 记录 + var p model.ChatParticipant + err := tx.Where("conversation_id = ? AND participant_type = ? AND participant_id = ?", + conversationID, principal.Type, principal.ID).First(&p).Error + if err == nil { + // 管理员是 participant,使用其角色 + participant = &p + senderRole = p.Role + } else if errors.Is(err, gorm.ErrRecordNotFound) { + // 管理员不是 participant,使用特殊角色 "admin" + senderRole = "admin" + } else { + return err + } + } else { + // 普通用户必须是 participant + p, err := r.findParticipant(tx, principal, conversationID, true) + if err != nil { + return err + } + participant = p + senderRole = p.Role + } + message := model.ChatMessage{ ConversationID: conversation.ID, SenderType: principal.Type, SenderID: principal.ID, - SenderRole: participant.Role, + SenderRole: senderRole, ContentType: "text", Content: req.Content, AttachmentURLS: encodeStringList(req.AttachmentURLS), @@ -324,10 +396,15 @@ func (r *Repository) SendMessage(principal Principal, conversationID uint64, req if err := tx.Save(&conversation).Error; err != nil { return err } - now := time.Now() - if err := tx.Model(participant).Update("last_read_at", now).Error; err != nil { - return err + + // 更新 participant 的已读时间(仅当是 participant 时) + if participant != nil { + now := time.Now() + if err := tx.Model(participant).Update("last_read_at", now).Error; err != nil { + return err + } } + messageID = message.ID return nil }) @@ -370,6 +447,24 @@ func (r *Repository) SendMessage(principal Principal, conversationID uint64, req func (r *Repository) MarkRead(principal Principal, conversationID uint64) error { return r.db.Transaction(func(tx *gorm.DB) error { + // 管理员可以不是 participant,直接返回成功 + if principal.Type == "admin" { + // 尝试查找 participant 记录,如果有就更新 + var participant model.ChatParticipant + err := tx.Where("conversation_id = ? AND participant_type = ? AND participant_id = ?", + conversationID, principal.Type, principal.ID).First(&participant).Error + if err == nil { + // 有 participant 记录,更新已读时间 + now := time.Now() + return tx.Model(&participant).Update("last_read_at", now).Error + } else if errors.Is(err, gorm.ErrRecordNotFound) { + // 没有 participant 记录,直接返回成功(管理员无需记录已读) + return nil + } + return err + } + + // 普通用户必须是 participant participant, err := r.findParticipant(tx, principal, conversationID, true) if err != nil { return err @@ -475,6 +570,10 @@ func (r *Repository) toMessageDTOs(principal Principal, rows []model.ChatMessage } if row.SenderType == "admin" { name = adminNames[row.SenderID] + // 如果角色是 "admin"(不是 participant 的管理员),在名字后添加标识 + if row.SenderRole == "admin" { + name = name + " (管理员)" + } } items = append(items, MessageDTO{ ID: row.ID, @@ -830,8 +929,50 @@ func (r *Repository) GetAvailableSupportAdmins() ([]SupportAdminDTO, error) { // ListConversationsWithFilter 支持筛选的会话列表 func (r *Repository) ListConversationsWithFilter(principal Principal, page, pageSize int, filter string) (*PaginatedResult, error) { page, pageSize = normalizePagination(page, pageSize) - var total int64 + // 管理员在"全部"模式下直接查询所有会话 + if principal.Type == "admin" && filter == "all" { + var total int64 + if err := r.db.Model(&model.ChatConversation{}).Count(&total).Error; err != nil { + return nil, err + } + + var conversations []model.ChatConversation + offset := (page - 1) * pageSize + if err := r.db.Order("COALESCE(last_message_at, created_at) DESC, id DESC"). + Offset(offset). + Limit(pageSize). + Find(&conversations).Error; err != nil { + return nil, err + } + + items := make([]ConversationDTO, 0, len(conversations)) + for _, conv := range conversations { + participants, err := r.participants(conv.ID) + if err != nil { + return nil, err + } + items = append(items, ConversationDTO{ + ID: conv.ID, + OrderID: conv.OrderID, + Type: conv.Type, + Title: conv.Title, + Status: conv.Status, + Role: "admin", // 管理员角色 + Participants: participants, + LastMessageID: conv.LastMessageID, + LastMessagePreview: conv.LastMessagePreview, + LastMessageAt: conv.LastMessageAt, + UnreadCount: 0, // 管理员不计未读 + CreatedAt: conv.CreatedAt, + UpdatedAt: conv.UpdatedAt, + }) + } + return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil + } + + // 其他情况使用原有逻辑 + var total int64 countDB := r.db.Table("chat_conversations AS c"). Joins("JOIN chat_participants AS cp ON cp.conversation_id = c.id") @@ -844,12 +985,8 @@ func (r *Repository) ListConversationsWithFilter(principal Principal, page, page countDB = countDB.Where("c.id NOT IN (?)", r.db.Table("chat_participants").Select("conversation_id").Where("participant_type = ? AND role = ?", "admin", "support")) default: - // 全部会话(admin 可以看所有) - if principal.Type == "admin" { - // 管理员看所有会话 - } else { - countDB = countDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) - } + // 普通用户的全部会话 + countDB = countDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) } if err := countDB.Count(&total).Error; err != nil { @@ -867,11 +1004,8 @@ func (r *Repository) ListConversationsWithFilter(principal Principal, page, page queryDB = queryDB.Where("c.id NOT IN (?)", r.db.Table("chat_participants").Select("conversation_id").Where("participant_type = ? AND role = ?", "admin", "support")) default: - if principal.Type == "admin" { - // 管理员看所有会话 - } else { - queryDB = queryDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) - } + // 普通用户的全部会话 + queryDB = queryDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) } err := queryDB. diff --git a/frontend/src/features/admin/views/AdminChatsView.vue b/frontend/src/features/admin/views/AdminChatsView.vue index 403ae0b..a85636d 100644 --- a/frontend/src/features/admin/views/AdminChatsView.vue +++ b/frontend/src/features/admin/views/AdminChatsView.vue @@ -276,12 +276,17 @@ function roleLabel(role: string) { support: '客服', customer: '咨询', system: '系统', + admin: '管理员', } return map[role] || '成员' } function senderLabel(item: ChatMessage) { if (item.sender_type === 'system') return '系统' + // 管理员消息特殊标识 + if (item.sender_role === 'admin') { + return `管理员 · ${item.sender_name}` + } return `${roleLabel(item.sender_role)} · ${item.sender_name}` } @@ -374,13 +379,19 @@ function getSupportName(item: ChatConversation) { v-for="item in messages" :key="item.id" class="message-row" - :class="{ self: item.is_self, system: item.sender_type === 'system' }" + :class="{ + self: item.is_self, + system: item.sender_type === 'system', + admin: item.sender_role === 'admin' + }" >