Files
kefu_cloud/server/internal/handler/assignment.go
T
yml2213 9485c28abd 实现 P0 自动分配、离线留言与可嵌入 Widget SDK
- 会话创建时按负载自动分配在线客服,无客服则进入离线模式
- 新增 /api/widget/leave-message 沉淀联系方式与留言事件
- 访客端离线留言表单;工作台展示离线留言/自动分配事件
- 提供 public/widget.js + /widget/embed 嵌入方案
2026-07-15 11:09:50 +08:00

97 lines
2.7 KiB
Go

package handler
import (
"fmt"
"kefu-sys/server/internal/model"
"kefu-sys/server/internal/ws"
)
const offlineLeavePrompt = "当前无客服在线,请留言并留下联系方式,我们上线后会尽快回复您。"
// pickLeastLoadedAgent 在在线一线客服中选择当前进行中会话最少的一位。
func pickLeastLoadedAgent(tenantID uint) (*model.User, error) {
var agents []model.User
if err := model.DB.Where("tenant_id = ? AND role = ? AND status = ?", tenantID, "agent", "online").
Order("id asc").Find(&agents).Error; err != nil {
return nil, err
}
if len(agents) == 0 {
return nil, nil
}
best := &agents[0]
bestCount := int64(-1)
for i := range agents {
var count int64
if err := model.DB.Model(&model.Session{}).
Where("tenant_id = ? AND agent_id = ? AND status = ?", tenantID, agents[i].ID, "active").
Count(&count).Error; err != nil {
return nil, err
}
if bestCount < 0 || count < bestCount {
bestCount = count
best = &agents[i]
}
}
return best, 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 将会话自动分配给负载最低的在线客服;无可分配客服时返回 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
}
agent, err := pickLeastLoadedAgent(session.TenantID)
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"
detail := fmt.Sprintf("系统自动分配给 %s", agent.Nickname)
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
}