Files
gochat/internal/service/assignable_agent_service.go
T

254 lines
8.2 KiB
Go

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"`
AccountID uint `json:"account_id"`
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"`
Active bool `json:"active"`
Available bool `json:"available"`
Provider string `json:"provider"`
DisplayName string `json:"available_name"`
AvatarURL string `json:"avatar_url"`
AutoOffline bool `json:"auto_offline"`
Confirmed bool `json:"confirmed"`
CustomRoleID uint `json:"custom_role_id,omitempty"`
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
}
}
}
// 获取每个agent的AccountUser字段,用于复用Chatwoot _agent serializer shape。
accountUserMap := make(map[uint]model.AccountUser)
accountUsers, _, auErr := s.accountRepo.FindAgentsByAccount(ctx, accountID, 0, 1000)
if auErr == nil {
for _, au := range accountUsers {
accountUserMap[au.UserID] = au
}
}
// 构建DTO列表
dtos := make([]AssignableAgentDTO, len(users))
for i, u := range users {
accountUser := accountUserMap[u.ID]
role := accountUser.Role
if role == "" {
role = u.Role
}
availabilityStatus := accountUser.Availability
if availabilityStatus == "" {
if u.Available {
availabilityStatus = "online"
} else {
availabilityStatus = "offline"
}
}
dtos[i] = AssignableAgentDTO{
ID: u.ID,
AccountID: accountID,
Name: u.Name,
Email: u.Email,
Role: role,
Active: u.Active,
Available: u.Available,
Provider: u.Provider,
DisplayName: u.DisplayName,
AvatarURL: u.AvatarURL,
AutoOffline: accountUser.AutoOffline,
Confirmed: u.ConfirmedAt != nil,
CustomRoleID: accountUser.CustomRoleID,
AvailabilityStatus: availabilityStatus,
Workload: workloadMap[u.ID],
IsAdministrator: role == "administrator",
}
}
// 按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
}