package service import ( "context" "fmt" "sort" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" ) // AssignableAgentService 实现查找可分配agent的业务逻辑。 // Reference: Chatwoot app/controllers/api/v1/inboxes_controller.rb #assignable_agents // 逻辑: 对每个inbox在inbox_ids[]中的成员取交集,加上account管理员,去重。 // 增强功能: 计算每个agent的workload (open conversations数量),按workload升序排序。 type AssignableAgentService struct { inboxMemberRepo *repository.InboxMemberRepo userRepo *repository.UserRepo accountRepo *repository.AccountRepo conversationRepo *repository.ConversationRepo } // NewAssignableAgentService 创建新的AssignableAgentService。 func NewAssignableAgentService(inboxMemberRepo *repository.InboxMemberRepo, userRepo *repository.UserRepo, accountRepo *repository.AccountRepo, conversationRepo *repository.ConversationRepo) *AssignableAgentService { return &AssignableAgentService{ inboxMemberRepo: inboxMemberRepo, userRepo: userRepo, accountRepo: accountRepo, conversationRepo: conversationRepo, } } // AssignableAgentDTO 是可分配agent的API响应结构,包含workload信息。 // Reference: Chatwoot assignable_agents API — 返回agent列表及其当前workload type AssignableAgentDTO struct { ID uint `json:"id"` Name string `json:"name"` Email string `json:"email"` Role string `json:"role"` Active bool `json:"active"` Available bool `json:"available"` AvatarURL string `json:"avatar_url"` AvailabilityStatus string `json:"availability_status"` // online, offline, busy Workload int64 `json:"workload"` // 当前open conversations数量 IsAdministrator bool `json:"is_administrator"` // 是否为account管理员 } // FindAssignableAgents 返回可以被分配到指定inbox对话中的agents。 // 结果是所有给定inbox成员的交集,加上account的所有管理员。 // 注意: 此方法不包含workload信息,仅返回基础User对象列表。 func (s *AssignableAgentService) FindAssignableAgents(ctx context.Context, accountID uint, inboxIDs []uint) ([]model.User, error) { if len(inboxIDs) == 0 { // 如果没有指定inbox,返回account的所有管理员 return s.findAdministrators(ctx, accountID) } // Step 1: 收集每个inbox的成员user IDs var intersectionIDs []uint for i, inboxID := range inboxIDs { members, err := s.inboxMemberRepo.FindByInbox(ctx, inboxID) if err != nil { return nil, fmt.Errorf("查找inbox %d的成员失败: %w", inboxID, err) } inboxUserIDs := make([]uint, 0, len(members)) for _, m := range members { inboxUserIDs = append(inboxUserIDs, m.UserID) } if i == 0 { intersectionIDs = inboxUserIDs } else { intersectionIDs = intersect(intersectionIDs, inboxUserIDs) } if len(intersectionIDs) == 0 { break // 所有inbox没有公共成员 } } // Step 2: 加上account管理员 adminIDs, err := s.findAdministratorIDs(ctx, accountID) if err != nil { return nil, fmt.Errorf("查找管理员失败: %w", err) } // Step 3: 交集 + 管理员的合并(去重) allIDs := union(intersectionIDs, adminIDs) // Step 4: 通过IDs获取完整的User对象 users, err := s.userRepo.FindByIDs(ctx, allIDs) if err != nil { return nil, fmt.Errorf("获取用户信息失败: %w", err) } return users, nil } // GetAssignableAgents 返回带有workload信息的可分配agent列表,并按workload升序排序。 // Reference: Chatwoot inbox.rb assignable_agents + auto assignment logic // workload = agent当前在account中的open conversations数量 // 排序: workload升序 (最少conversations的agent优先),保证负载均衡分配。 func (s *AssignableAgentService) GetAssignableAgents(ctx context.Context, accountID uint, inboxIDs []uint) ([]AssignableAgentDTO, error) { // 先获取基础agent列表 users, err := s.FindAssignableAgents(ctx, accountID, inboxIDs) if err != nil { return nil, err } if len(users) == 0 { return []AssignableAgentDTO{}, nil } // 收集所有agent IDs用于workload查询 userIDs := make([]uint, len(users)) for i, u := range users { userIDs[i] = u.ID } // 批量查询每个agent的open conversations数量 workloadMap := make(map[uint]int64) if s.conversationRepo != nil { workloadResults, err := s.conversationRepo.CountOpenConversationsByAssignees(ctx, accountID, userIDs) if err != nil { // workload查询失败不影响核心功能,仅记录错误,workload默认为0 fmt.Printf("查询agent workload失败 (accountID=%d): %v\n", accountID, err) } else { for _, wr := range workloadResults { workloadMap[wr.AssigneeID] = wr.Count } } } // 获取account的管理员IDs用于标记is_administrator adminIDs, _ := s.findAdministratorIDs(ctx, accountID) adminSet := make(map[uint]bool, len(adminIDs)) for _, id := range adminIDs { adminSet[id] = true } // 获取每个agent的availability status (从account_user表中获取) availabilityMap := make(map[uint]string) accountUsers, _, auErr := s.accountRepo.FindAgentsByAccount(ctx, accountID, 0, 1000) if auErr == nil { for _, au := range accountUsers { availabilityMap[au.UserID] = au.Availability } } // 构建DTO列表 dtos := make([]AssignableAgentDTO, len(users)) for i, u := range users { dtos[i] = AssignableAgentDTO{ ID: u.ID, Name: u.Name, Email: u.Email, Role: u.Role, Active: u.Active, Available: u.Available, AvatarURL: u.AvatarURL, AvailabilityStatus: availabilityMap[u.ID], Workload: workloadMap[u.ID], IsAdministrator: adminSet[u.ID], } } // 按workload升序排序 (workload最少的agent排在前面,优先分配) sort.Slice(dtos, func(i, j int) bool { return dtos[i].Workload < dtos[j].Workload }) return dtos, nil } // findAdministrators 返回account的所有管理员级别的用户。 func (s *AssignableAgentService) findAdministrators(ctx context.Context, accountID uint) ([]model.User, error) { adminIDs, err := s.findAdministratorIDs(ctx, accountID) if err != nil { return nil, err } return s.userRepo.FindByIDs(ctx, adminIDs) } // findAdministratorIDs 返回account的所有管理员的user IDs。 // 使用AccountRepo.FindAgentsByAccount返回AccountUsers (包含role字段)。 func (s *AssignableAgentService) findAdministratorIDs(ctx context.Context, accountID uint) ([]uint, error) { // FindAgentsByAccount返回account的所有AccountUsers (分页) // 获取大批量以包含所有管理员 accountUsers, _, err := s.accountRepo.FindAgentsByAccount(ctx, accountID, 0, 1000) if err != nil { return nil, fmt.Errorf("查找account用户失败: %w", err) } adminIDs := make([]uint, 0) for _, au := range accountUsers { if au.Role == "administrator" { adminIDs = append(adminIDs, au.UserID) } } return adminIDs, nil } // intersect 返回两个uint切片的交集。 func intersect(a, b []uint) []uint { result := make([]uint, 0) setB := make(map[uint]bool, len(b)) for _, v := range b { setB[v] = true } for _, v := range a { if setB[v] { result = append(result, v) } } return result } // union 返回两个uint切片的合并(去重)。 func union(a, b []uint) []uint { seen := make(map[uint]bool, len(a)+len(b)) result := make([]uint, 0, len(a)+len(b)) for _, v := range a { if !seen[v] { seen[v] = true result = append(result, v) } } for _, v := range b { if !seen[v] { seen[v] = true result = append(result, v) } } return result }