package handler import ( "fmt" "strings" "kefu-sys/server/internal/model" "kefu-sys/server/internal/ws" ) const ( assignStrategyLeastLoad = "least_load" assignStrategyRoundRobin = "round_robin" offlineLeavePrompt = "当前无客服在线,请留言并留下联系方式,我们上线后会尽快回复您。" ) func normalizeAssignStrategy(raw string) string { switch strings.TrimSpace(raw) { case assignStrategyRoundRobin: return assignStrategyRoundRobin default: return assignStrategyLeastLoad } } // listOnlineAgents 租户内可自动接待的一线客服(online + role=agent)。 func listOnlineAgents(tenantID uint) ([]model.User, error) { var agents []model.User err := model.DB.Where("tenant_id = ? AND role = ? AND status = ?", tenantID, "agent", "online"). Order("id asc").Find(&agents).Error return agents, err } func agentActiveSessionCount(tenantID, agentID uint) (int64, error) { var count int64 err := model.DB.Model(&model.Session{}). Where("tenant_id = ? AND agent_id = ? AND status = ?", tenantID, agentID, "active"). Count(&count).Error return count, err } // filterAssignable 按最大并发过滤;maxActive<=0 表示不限制。 func filterAssignable(tenantID uint, agents []model.User, maxActive int) ([]model.User, map[uint]int64, error) { counts := make(map[uint]int64, len(agents)) out := make([]model.User, 0, len(agents)) for i := range agents { n, err := agentActiveSessionCount(tenantID, agents[i].ID) if err != nil { return nil, nil, err } counts[agents[i].ID] = n if maxActive > 0 && int(n) >= maxActive { continue } out = append(out, agents[i]) } return out, counts, nil } // pickLeastLoadedAgent 在候选中选 active 会话最少者;平局取 id 更小。 func pickLeastLoadedAgent(candidates []model.User, counts map[uint]int64) *model.User { if len(candidates) == 0 { return nil } best := &candidates[0] bestCount := counts[best.ID] for i := 1; i < len(candidates); i++ { c := counts[candidates[i].ID] if c < bestCount || (c == bestCount && candidates[i].ID < best.ID) { bestCount = c best = &candidates[i] } } return best } // pickRoundRobinAgent 在按 id 排序的候选中,从 lastAgentID 之后轮转下一位。 func pickRoundRobinAgent(candidates []model.User, lastAgentID *uint) *model.User { if len(candidates) == 0 { return nil } if lastAgentID == nil || *lastAgentID == 0 { return &candidates[0] } // 找到 last 在全序中的位置,取其后第一个仍在候选中的 start := 0 for i, a := range candidates { if a.ID > *lastAgentID { start = i return &candidates[start] } if a.ID == *lastAgentID { start = (i + 1) % len(candidates) return &candidates[start] } } // last 已不在列表(离线/满载),从头开始 return &candidates[0] } // pickAgent 按租户策略选择可分配坐席。 func pickAgent(tenantID uint, strategy string, maxActive int, rrLast *uint) (*model.User, error) { agents, err := listOnlineAgents(tenantID) if err != nil { return nil, err } if len(agents) == 0 { return nil, nil } candidates, counts, err := filterAssignable(tenantID, agents, maxActive) if err != nil { return nil, err } if len(candidates) == 0 { return nil, nil } strategy = normalizeAssignStrategy(strategy) if strategy == assignStrategyRoundRobin { return pickRoundRobinAgent(candidates, rrLast), nil } return pickLeastLoadedAgent(candidates, counts), nil } // countOnlineAgents 统计租户当前可接待的在线客服数。 func countOnlineAgents(tenantID uint) (int64, error) { var count int64 err := model.DB.Model(&model.User{}). Where("tenant_id = ? AND role = ? AND status = ?", tenantID, "agent", "online"). Count(&count).Error return count, err } // tryAutoAssign 按租户分配策略自动分配 waiting 会话;无可分配客服时返回 nil。 func tryAutoAssign(session *model.Session) (*model.User, error) { if session == nil || session.ID == 0 { return nil, nil } if session.Status != "waiting" || session.AgentID != nil { return nil, nil } setting, err := getOrCreateTenantSettings(session.TenantID) if err != nil { // 设置失败时回退默认 least_load、不限制并发 setting = &model.TenantSetting{AssignStrategy: assignStrategyLeastLoad} } strategy := normalizeAssignStrategy(setting.AssignStrategy) maxActive := setting.MaxActivePerAgent if maxActive < 0 { maxActive = 0 } agent, err := pickAgent(session.TenantID, strategy, maxActive, setting.RRLastAgentID) if err != nil || agent == nil { return nil, err } result := model.DB.Model(&model.Session{}). Where("id = ? AND tenant_id = ? AND status = ? AND agent_id IS NULL", session.ID, session.TenantID, "waiting"). Updates(map[string]interface{}{ "agent_id": agent.ID, "status": "active", "last_read_seq": 0, }) if result.Error != nil { return nil, result.Error } if result.RowsAffected == 0 { // 并发下可能已被他人领取 if err := model.DB.First(session, session.ID).Error; err != nil { return nil, err } return nil, nil } session.AgentID = &agent.ID session.Status = "active" // 推进轮询游标(仅 round_robin) if strategy == assignStrategyRoundRobin && setting.ID > 0 { aid := agent.ID _ = model.DB.Model(&model.TenantSetting{}). Where("id = ?", setting.ID). Update("rr_last_agent_id", aid).Error setting.RRLastAgentID = &aid } strategyLabel := "负载最低" if strategy == assignStrategyRoundRobin { strategyLabel = "轮询" } detail := fmt.Sprintf("系统自动分配给 %s(%s)", agent.Nickname, strategyLabel) if agent.Nickname == "" { detail = fmt.Sprintf("系统自动分配给 %s(%s)", agent.Username, strategyLabel) } model.DB.Create(&model.SessionEvent{ SessionID: session.ID, OperatorID: 0, Action: "auto_assign", Detail: detail, }) if payload, err := ws.NewEvent("session_updated", session.ID, session); err == nil { ws.DefaultHub.BroadcastToTenantStaff(session.TenantID, payload) ws.DefaultHub.BroadcastToVisitor(session.TenantID, session.ID, payload) } return agent, nil }