209 lines
6.0 KiB
Go
209 lines
6.0 KiB
Go
package handler
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
"kefu-cloud/server/internal/model"
|
||
"kefu-cloud/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
|
||
}
|