完成客服工作台闭环

This commit is contained in:
yml2213
2026-07-15 10:49:52 +08:00
parent 8cff2a5824
commit 1d31c9eba3
12 changed files with 727 additions and 235 deletions
@@ -336,3 +336,105 @@ func TestKnowledgeEntryRespectsPlanCapacity(t *testing.T) {
t.Fatalf("超出知识库容量状态码 = %d,期望 %d,响应 = %s", recorder.Code, http.StatusConflict, recorder.Body.String())
}
}
func TestWorkbenchSessionLifecycleUnreadNotesTransferAndImage(t *testing.T) {
router := setupRouter(t)
tenant := createTenant(t, "工作台租户", "normal")
agentOne := createUser(t, tenant.ID, "workbench-agent-one", "agent")
agentTwo := createUser(t, tenant.ID, "workbench-agent-two", "agent")
customer := model.Customer{TenantID: tenant.ID, Name: "工作台客户", Source: "网页"}
if err := model.DB.Create(&customer).Error; err != nil {
t.Fatalf("创建工作台客户失败: %v", err)
}
session := model.Session{TenantID: tenant.ID, CustomerID: customer.ID, Status: "waiting", Priority: "normal"}
if err := model.DB.Create(&session).Error; err != nil {
t.Fatalf("创建等待会话失败: %v", err)
}
visitorMessage := model.Message{SessionID: session.ID, SenderType: "visitor", Content: "需要咨询", Type: "text", Seq: 1, SentAt: time.Now()}
if err := model.DB.Create(&visitorMessage).Error; err != nil {
t.Fatalf("创建访客消息失败: %v", err)
}
availableRecorder := httptest.NewRecorder()
router.ServeHTTP(availableRecorder, bearerRequest(t, http.MethodGet, "/api/agents/available", nil, agentOne))
if availableRecorder.Code != http.StatusOK {
t.Fatalf("查询在线客服失败: %d %s", availableRecorder.Code, availableRecorder.Body.String())
}
claimRecorder := httptest.NewRecorder()
router.ServeHTTP(claimRecorder, bearerRequest(t, http.MethodPost, fmt.Sprintf("/api/sessions/%d/assign", session.ID), []byte(`{}`), agentOne))
if claimRecorder.Code != http.StatusOK {
t.Fatalf("领取会话失败: %d %s", claimRecorder.Code, claimRecorder.Body.String())
}
unreadRecorder := httptest.NewRecorder()
router.ServeHTTP(unreadRecorder, bearerRequest(t, http.MethodGet, "/api/sessions", nil, agentOne))
var unreadResponse struct {
List []struct {
ID uint `json:"id"`
UnreadCount int `json:"unread_count"`
} `json:"list"`
}
if err := json.Unmarshal(unreadRecorder.Body.Bytes(), &unreadResponse); err != nil {
t.Fatalf("解析未读列表失败: %v", err)
}
if len(unreadResponse.List) != 1 || unreadResponse.List[0].UnreadCount != 1 {
t.Fatalf("领取后的未读数不正确: %s", unreadRecorder.Body.String())
}
readRecorder := httptest.NewRecorder()
router.ServeHTTP(readRecorder, bearerRequest(t, http.MethodPost, fmt.Sprintf("/api/sessions/%d/read", session.ID), []byte(`{}`), agentOne))
if readRecorder.Code != http.StatusOK {
t.Fatalf("标记已读失败: %d %s", readRecorder.Code, readRecorder.Body.String())
}
readListRecorder := httptest.NewRecorder()
router.ServeHTTP(readListRecorder, bearerRequest(t, http.MethodGet, "/api/sessions", nil, agentOne))
var readListResponse struct {
List []struct {
UnreadCount int `json:"unread_count"`
} `json:"list"`
}
if err := json.Unmarshal(readListRecorder.Body.Bytes(), &readListResponse); err != nil || len(readListResponse.List) != 1 || readListResponse.List[0].UnreadCount != 0 {
t.Fatalf("标记已读后的未读数不正确: %s", readListRecorder.Body.String())
}
noteRecorder := httptest.NewRecorder()
router.ServeHTTP(noteRecorder, bearerRequest(t, http.MethodPost, fmt.Sprintf("/api/sessions/%d/notes", session.ID), []byte(`{"content":"已核对客户需求"}`), agentOne))
if noteRecorder.Code != http.StatusOK {
t.Fatalf("添加内部备注失败: %d %s", noteRecorder.Code, noteRecorder.Body.String())
}
transferRecorder := httptest.NewRecorder()
transferBody := []byte(fmt.Sprintf(`{"agent_id":%d}`, agentTwo.ID))
router.ServeHTTP(transferRecorder, bearerRequest(t, http.MethodPost, fmt.Sprintf("/api/sessions/%d/transfer", session.ID), transferBody, agentOne))
if transferRecorder.Code != http.StatusOK {
t.Fatalf("转接会话失败: %d %s", transferRecorder.Code, transferRecorder.Body.String())
}
oldAgentMessageRecorder := httptest.NewRecorder()
router.ServeHTTP(oldAgentMessageRecorder, bearerRequest(t, http.MethodPost, fmt.Sprintf("/api/sessions/%d/messages", session.ID), []byte(`{"content":"越权回复"}`), agentOne))
if oldAgentMessageRecorder.Code != http.StatusForbidden {
t.Fatalf("转接后原客服仍可回复: %d %s", oldAgentMessageRecorder.Code, oldAgentMessageRecorder.Body.String())
}
imageRecorder := httptest.NewRecorder()
imageBody := []byte(`{"content":"data:image/png;base64,aGVsbG8=","type":"image"}`)
router.ServeHTTP(imageRecorder, bearerRequest(t, http.MethodPost, fmt.Sprintf("/api/sessions/%d/messages", session.ID), imageBody, agentTwo))
if imageRecorder.Code != http.StatusOK {
t.Fatalf("发送图片消息失败: %d %s", imageRecorder.Code, imageRecorder.Body.String())
}
invalidEndRecorder := httptest.NewRecorder()
router.ServeHTTP(invalidEndRecorder, bearerRequest(t, http.MethodPost, fmt.Sprintf("/api/sessions/%d/end?reason=bad_reason", session.ID), []byte(`{}`), agentTwo))
if invalidEndRecorder.Code != http.StatusBadRequest {
t.Fatalf("无效结束原因未被拦截: %d %s", invalidEndRecorder.Code, invalidEndRecorder.Body.String())
}
var savedSession model.Session
if err := model.DB.First(&savedSession, session.ID).Error; err != nil {
t.Fatalf("读取工作台会话失败: %v", err)
}
if savedSession.AgentID == nil || *savedSession.AgentID != agentTwo.ID || savedSession.LastReadSeq != 2 {
t.Fatalf("会话领取/转接/已读状态错误: %+v", savedSession)
}
}