package chat import ( "context" "fmt" "gorm.io/gorm" "hfb_sys/backend/internal/model" "time" ) func (r *Repository) TransferConversation(ctx context.Context, principal Principal, conversationID uint64, toAdminID uint64) error { return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 验证当前操作者是会话参与者 if _, err := r.findParticipant(tx, principal, conversationID, false); err != nil { return err } // 验证目标客服存在、活跃且拥有客服角色,避免转接给超级管理员。 if !adminIsSupport(tx, toAdminID) { return fmt.Errorf("目标客服不存在、已禁用或不是客服角色") } // 检查目标客服是否已有该会话 var count int64 if err := tx.Model(&model.ChatParticipant{}). Where("conversation_id = ? AND participant_type = ? AND participant_id = ?", conversationID, "admin", toAdminID). Count(&count).Error; err != nil { return err } if count > 0 { return fmt.Errorf("该客服已在会话中") } // 删除原客服参与者 if err := tx.Where("conversation_id = ? AND participant_type = ? AND role = ?", conversationID, "admin", "support"). Delete(&model.ChatParticipant{}).Error; err != nil { return err } // 添加新客服参与者 participant := model.ChatParticipant{ ConversationID: conversationID, ParticipantType: "admin", ParticipantID: toAdminID, Role: "support", JoinedAt: time.Now(), } if err := tx.Create(&participant).Error; err != nil { return err } // 添加系统消息记录转接 message := model.ChatMessage{ ConversationID: conversationID, SenderType: "system", SenderRole: "system", ContentType: "system", Content: "会话已转接给其他客服", AttachmentURLS: emptyJSONList(), } if err := tx.Create(&message).Error; err != nil { return err } return nil }) } func (r *Repository) GetAvailableSupportAdmins(ctx context.Context) ([]SupportAdminDTO, error) { db := r.db.WithContext(ctx) // 仅展示客服角色管理员,超级管理员即使有 chat:view 权限也不作为客服候选。 type adminRow struct { ID uint64 Nickname string SupportStatus string } var admins []adminRow err := db.Table("admin_users AS au"). Select("au.id, COALESCE(NULLIF(au.nickname, ''), au.username) AS nickname, au.support_status"). Joins("JOIN admin_user_roles AS aur ON aur.admin_user_id = au.id"). Joins("JOIN roles AS r ON r.id = aur.role_id"). Where("au.status = ? AND r.code = ?", "active", defaultSupportRoleCode). Order("CASE au.support_status WHEN 'online' THEN 0 WHEN 'busy' THEN 1 ELSE 2 END, au.id ASC"). Scan(&admins).Error if err != nil { return nil, err } // 统计每个客服的会话数 type loadRow struct { AdminID uint64 Count int64 } var loads []loadRow adminIDs := make([]uint64, len(admins)) for i, a := range admins { adminIDs[i] = a.ID } if len(adminIDs) > 0 { db.Table("chat_participants"). Select("participant_id AS admin_id, COUNT(*) AS count"). Where("participant_type = ? AND role = ? AND participant_id IN ?", "admin", "support", adminIDs). Group("participant_id"). Scan(&loads) } loadMap := make(map[uint64]int64) for _, l := range loads { loadMap[l.AdminID] = l.Count } result := make([]SupportAdminDTO, len(admins)) for i, a := range admins { result[i] = SupportAdminDTO{ ID: a.ID, Nickname: a.Nickname, SupportStatus: a.SupportStatus, ChatCount: loadMap[a.ID], } } return result, nil } func (r *Repository) ListConversationsWithFilter(ctx context.Context, principal Principal, page, pageSize int, filter string) (*PaginatedResult, error) { page, pageSize = normalizePagination(page, pageSize) db := r.db.WithContext(ctx) // 管理员在"全部"模式下直接查询所有会话 if principal.Type == "admin" && filter == "all" { var total int64 if err := db.Model(&model.ChatConversation{}).Count(&total).Error; err != nil { return nil, err } var conversations []model.ChatConversation offset := (page - 1) * pageSize if err := 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(ctx, 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 := db.Table("chat_conversations AS c"). Joins("JOIN chat_participants AS cp ON cp.conversation_id = c.id") switch filter { case "mine": // 只看我的会话 countDB = countDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) case "unassigned": // 未分配客服的会话 countDB = countDB.Where("c.id NOT IN (?)", db.Table("chat_participants").Select("conversation_id").Where("participant_type = ? AND role = ?", "admin", "support")) default: // 普通用户的全部会话 countDB = countDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) } if err := countDB.Count(&total).Error; err != nil { return nil, err } var rows []conversationRow offset := (page - 1) * pageSize queryDB := r.conversationQuery(ctx, principal) switch filter { case "mine": queryDB = queryDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) case "unassigned": queryDB = queryDB.Where("c.id NOT IN (?)", db.Table("chat_participants").Select("conversation_id").Where("participant_type = ? AND role = ?", "admin", "support")) default: // 普通用户的全部会话 queryDB = queryDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID) } err := queryDB. Order("COALESCE(c.last_message_at, c.created_at) DESC, c.id DESC"). Offset(offset). Limit(pageSize). Scan(&rows).Error if err != nil { return nil, err } items := make([]ConversationDTO, 0, len(rows)) for _, row := range rows { participants, err := r.participants(ctx, row.ID) if err != nil { return nil, err } items = append(items, row.toDTO(participants)) } return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil }