实现 P0 自动分配、离线留言与可嵌入 Widget SDK
- 会话创建时按负载自动分配在线客服,无客服则进入离线模式 - 新增 /api/widget/leave-message 沉淀联系方式与留言事件 - 访客端离线留言表单;工作台展示离线留言/自动分配事件 - 提供 public/widget.js + /widget/embed 嵌入方案
This commit is contained in:
@@ -438,3 +438,105 @@ func TestWorkbenchSessionLifecycleUnreadNotesTransferAndImage(t *testing.T) {
|
||||
t.Fatalf("会话领取/转接/已读状态错误: %+v", savedSession)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWidgetAutoAssignAndOfflineLeave(t *testing.T) {
|
||||
router := setupRouter(t)
|
||||
tenant := createTenant(t, "自动分配租户", "normal")
|
||||
channel := model.Channel{
|
||||
TenantID: tenant.ID, Type: "web", Name: "网页", Status: "enabled",
|
||||
ScriptCode: `<script data-id="WK_auto_001"></script>`,
|
||||
}
|
||||
if err := model.DB.Create(&channel).Error; err != nil {
|
||||
t.Fatalf("创建渠道失败: %v", err)
|
||||
}
|
||||
|
||||
// 无在线客服 → 离线模式
|
||||
initRecorder := httptest.NewRecorder()
|
||||
initReq := httptest.NewRequest(http.MethodPost, "/api/widget/init", bytes.NewBufferString(`{"channel_key":"WK_auto_001","visitor_name":"离线访客"}`))
|
||||
initReq.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(initRecorder, initReq)
|
||||
if initRecorder.Code != http.StatusOK {
|
||||
t.Fatalf("离线初始化失败: %s", initRecorder.Body.String())
|
||||
}
|
||||
var offlineInit struct {
|
||||
Data struct {
|
||||
SessionID uint `json:"session_id"`
|
||||
VisitorToken string `json:"visitor_token"`
|
||||
AgentsOnline bool `json:"agents_online"`
|
||||
OfflinePrompt string `json:"offline_prompt"`
|
||||
SessionStatus string `json:"session_status"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(initRecorder.Body.Bytes(), &offlineInit); err != nil {
|
||||
t.Fatalf("解析离线初始化失败: %v", err)
|
||||
}
|
||||
if offlineInit.Data.AgentsOnline || offlineInit.Data.OfflinePrompt == "" || offlineInit.Data.SessionStatus != "waiting" {
|
||||
t.Fatalf("期望离线等待会话: %+v", offlineInit.Data)
|
||||
}
|
||||
|
||||
leaveRecorder := httptest.NewRecorder()
|
||||
leaveBody := fmt.Sprintf(`{"session_id":%d,"content":"请回电处理订单问题","name":"张留言","phone":"13800138000","email":"leave@example.com"}`, offlineInit.Data.SessionID)
|
||||
leaveReq := httptest.NewRequest(http.MethodPost, "/api/widget/leave-message", bytes.NewBufferString(leaveBody))
|
||||
leaveReq.Header.Set("Content-Type", "application/json")
|
||||
leaveReq.Header.Set("X-Visitor-Token", offlineInit.Data.VisitorToken)
|
||||
router.ServeHTTP(leaveRecorder, leaveReq)
|
||||
if leaveRecorder.Code != http.StatusOK {
|
||||
t.Fatalf("离线留言失败: %s", leaveRecorder.Body.String())
|
||||
}
|
||||
var session model.Session
|
||||
if err := model.DB.First(&session, offlineInit.Data.SessionID).Error; err != nil {
|
||||
t.Fatalf("查询会话失败: %v", err)
|
||||
}
|
||||
var customer model.Customer
|
||||
if err := model.DB.First(&customer, session.CustomerID).Error; err != nil {
|
||||
t.Fatalf("查询客户失败: %v", err)
|
||||
}
|
||||
if customer.Phone != "13800138000" || customer.Email != "leave@example.com" || customer.Name != "张留言" {
|
||||
t.Fatalf("联系方式未沉淀: %+v", customer)
|
||||
}
|
||||
var msgCount int64
|
||||
model.DB.Model(&model.Message{}).Where("session_id = ?", session.ID).Count(&msgCount)
|
||||
if msgCount != 1 {
|
||||
t.Fatalf("留言消息数 = %d", msgCount)
|
||||
}
|
||||
var event model.SessionEvent
|
||||
if err := model.DB.Where("session_id = ? AND action = ?", session.ID, "offline_leave").First(&event).Error; err != nil {
|
||||
t.Fatalf("未记录离线留言事件: %v", err)
|
||||
}
|
||||
|
||||
// 有在线客服 → 自动分配
|
||||
agent := createUser(t, tenant.ID, "auto-agent-1", "agent")
|
||||
init2 := httptest.NewRecorder()
|
||||
init2Req := httptest.NewRequest(http.MethodPost, "/api/widget/init", bytes.NewBufferString(`{"channel_key":"WK_auto_001","visitor_name":"在线访客"}`))
|
||||
init2Req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(init2, init2Req)
|
||||
if init2.Code != http.StatusOK {
|
||||
t.Fatalf("在线初始化失败: %s", init2.Body.String())
|
||||
}
|
||||
var onlineInit struct {
|
||||
Data struct {
|
||||
SessionID uint `json:"session_id"`
|
||||
AgentsOnline bool `json:"agents_online"`
|
||||
SessionStatus string `json:"session_status"`
|
||||
AgentID uint `json:"agent_id"`
|
||||
AgentName string `json:"agent_name"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(init2.Body.Bytes(), &onlineInit); err != nil {
|
||||
t.Fatalf("解析在线初始化失败: %v", err)
|
||||
}
|
||||
if !onlineInit.Data.AgentsOnline || onlineInit.Data.SessionStatus != "active" || onlineInit.Data.AgentID != agent.ID {
|
||||
t.Fatalf("期望自动分配给在线客服: %+v agent=%d", onlineInit.Data, agent.ID)
|
||||
}
|
||||
var assigned model.Session
|
||||
if err := model.DB.First(&assigned, onlineInit.Data.SessionID).Error; err != nil {
|
||||
t.Fatalf("查询已分配会话失败: %v", err)
|
||||
}
|
||||
if assigned.AgentID == nil || *assigned.AgentID != agent.ID || assigned.Status != "active" {
|
||||
t.Fatalf("会话分配状态不正确: %+v", assigned)
|
||||
}
|
||||
var assignEvent model.SessionEvent
|
||||
if err := model.DB.Where("session_id = ? AND action = ?", assigned.ID, "auto_assign").First(&assignEvent).Error; err != nil {
|
||||
t.Fatalf("未记录自动分配事件: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user