254 lines
8.2 KiB
Go
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
|
|
}
|