完成客服工作台闭环
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
maxTextMessageLength = 2000
|
||||
maxImageMessageSize = 5 * 1024 * 1024
|
||||
)
|
||||
|
||||
func validateMessageContent(messageType, content string) (string, error) {
|
||||
switch messageType {
|
||||
case "text":
|
||||
content = strings.TrimSpace(content)
|
||||
if content == "" {
|
||||
return "", errors.New("消息内容不能为空")
|
||||
}
|
||||
if utf8.RuneCountInString(content) > maxTextMessageLength {
|
||||
return "", errors.New("单条文本消息不能超过 2000 字")
|
||||
}
|
||||
return content, nil
|
||||
case "image":
|
||||
parts := strings.SplitN(content, ",", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", errors.New("图片格式无效")
|
||||
}
|
||||
if parts[0] != "data:image/jpeg;base64" && parts[0] != "data:image/png;base64" && parts[0] != "data:image/gif;base64" {
|
||||
return "", errors.New("仅支持 jpg、png、gif 图片")
|
||||
}
|
||||
bytes, err := base64.StdEncoding.DecodeString(parts[1])
|
||||
if err != nil || len(bytes) == 0 {
|
||||
return "", errors.New("图片内容无效")
|
||||
}
|
||||
if len(bytes) > maxImageMessageSize {
|
||||
return "", errors.New("图片不能超过 5 MB")
|
||||
}
|
||||
return content, nil
|
||||
default:
|
||||
return "", errors.New("不支持的消息类型")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateMessageContent(t *testing.T) {
|
||||
validImage := "data:image/png;base64,aGVsbG8="
|
||||
if content, err := validateMessageContent("image", validImage); err != nil || content != validImage {
|
||||
t.Fatalf("合法图片校验失败: content=%q err=%v", content, err)
|
||||
}
|
||||
if _, err := validateMessageContent("image", "data:image/webp;base64,aGVsbG8="); err == nil {
|
||||
t.Fatal("不支持的图片格式未被拦截")
|
||||
}
|
||||
if _, err := validateMessageContent("text", " "); err == nil {
|
||||
t.Fatal("空白文本未被拦截")
|
||||
}
|
||||
if _, err := validateMessageContent("text", strings.Repeat("字", maxTextMessageLength+1)); err == nil {
|
||||
t.Fatal("超长文本未被拦截")
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ func SetupRoutes(r *gin.Engine) {
|
||||
authRequired.GET("/ws", ws.Connect)
|
||||
|
||||
// 会话管理
|
||||
authRequired.GET("/agents/available", session.ListAvailableAgents)
|
||||
sessions := authRequired.Group("/sessions")
|
||||
sessions.GET("", session.List)
|
||||
sessions.GET("/:id", session.Get)
|
||||
@@ -46,6 +47,8 @@ func SetupRoutes(r *gin.Engine) {
|
||||
sessions.POST("/:id/end", session.End)
|
||||
sessions.PUT("/:id/priority", session.UpdatePriority)
|
||||
sessions.POST("/:id/messages", session.SendMessage)
|
||||
sessions.POST("/:id/read", session.MarkRead)
|
||||
sessions.POST("/:id/notes", session.AddNote)
|
||||
|
||||
// 客户管理
|
||||
customers := authRequired.Group("/customers")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -20,6 +22,15 @@ type SendMessageReq struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type SessionListItem struct {
|
||||
model.Session
|
||||
UnreadCount int `json:"unread_count"`
|
||||
}
|
||||
|
||||
type CreateNoteReq struct {
|
||||
Content string `json:"content" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *SessionHandler) SendMessage(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
id := c.Param("id")
|
||||
@@ -32,6 +43,11 @@ func (h *SessionHandler) SendMessage(c *gin.Context) {
|
||||
if req.Type == "" {
|
||||
req.Type = "text"
|
||||
}
|
||||
content, err := validateMessageContent(req.Type, req.Content)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
session, ok := loadTenantSession(c, id)
|
||||
if !ok {
|
||||
@@ -50,7 +66,7 @@ func (h *SessionHandler) SendMessage(c *gin.Context) {
|
||||
SessionID: session.ID,
|
||||
SenderType: "agent",
|
||||
SenderID: &userID,
|
||||
Content: req.Content,
|
||||
Content: content,
|
||||
Type: req.Type,
|
||||
SentAt: time.Now(),
|
||||
}
|
||||
@@ -59,6 +75,10 @@ func (h *SessionHandler) SendMessage(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "发送失败"})
|
||||
return
|
||||
}
|
||||
if middleware.GetRole(c) == "agent" {
|
||||
model.DB.Model(&model.Session{}).Where("id = ?", session.ID).Update("last_read_seq", msg.Seq)
|
||||
session.LastReadSeq = msg.Seq
|
||||
}
|
||||
broadcastSessionMessage(session, msg)
|
||||
|
||||
middleware.JSON(c, msg)
|
||||
@@ -134,6 +154,14 @@ func loadAssignableAgent(tenantID, agentID uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func unreadCount(session model.Session) int {
|
||||
var count int64
|
||||
model.DB.Model(&model.Message{}).
|
||||
Where("session_id = ? AND sender_type = ? AND seq > ?", session.ID, "visitor", session.LastReadSeq).
|
||||
Count(&count)
|
||||
return int(count)
|
||||
}
|
||||
|
||||
func (h *SessionHandler) List(c *gin.Context) {
|
||||
tenantID := middleware.GetTenantID(c)
|
||||
page, pageSize := middleware.GetPageParams(c)
|
||||
@@ -155,9 +183,21 @@ func (h *SessionHandler) List(c *gin.Context) {
|
||||
}
|
||||
|
||||
query.Model(&model.Session{}).Count(&total)
|
||||
query.Order("created_at desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&sessions)
|
||||
if err := query.Order("created_at desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&sessions).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询会话失败"})
|
||||
return
|
||||
}
|
||||
|
||||
middleware.JSONList(c, sessions, total, page, pageSize)
|
||||
items := make([]SessionListItem, 0, len(sessions))
|
||||
for _, session := range sessions {
|
||||
item := SessionListItem{Session: session}
|
||||
if middleware.GetRole(c) == "agent" && session.AgentID != nil && *session.AgentID == middleware.GetUserID(c) {
|
||||
item.UnreadCount = unreadCount(session)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
middleware.JSONList(c, items, total, page, pageSize)
|
||||
}
|
||||
|
||||
func (h *SessionHandler) Get(c *gin.Context) {
|
||||
@@ -179,8 +219,81 @@ func (h *SessionHandler) Get(c *gin.Context) {
|
||||
}
|
||||
var events []model.SessionEvent
|
||||
model.DB.Where("session_id = ?", session.ID).Order("created_at asc").Find(&events)
|
||||
var pendingCount int64
|
||||
model.DB.Model(&model.Session{}).Where("customer_id = ? AND tenant_id = ? AND status = ?", session.CustomerID, session.TenantID, "waiting").Count(&pendingCount)
|
||||
|
||||
middleware.JSON(c, gin.H{"session": session, "messages": messages, "events": events})
|
||||
middleware.JSON(c, gin.H{"session": session, "messages": messages, "events": events, "pending_count": pendingCount})
|
||||
}
|
||||
|
||||
func (h *SessionHandler) MarkRead(c *gin.Context) {
|
||||
session, ok := loadTenantSession(c, c.Param("id"))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !canReadSession(c, session) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权标记该会话已读"})
|
||||
return
|
||||
}
|
||||
if session.AgentID == nil || middleware.GetRole(c) != "agent" || *session.AgentID != middleware.GetUserID(c) {
|
||||
middleware.JSON(c, gin.H{"last_read_seq": session.LastReadSeq})
|
||||
return
|
||||
}
|
||||
var maxSeq int
|
||||
if err := model.DB.Model(&model.Message{}).Where("session_id = ?", session.ID).Select("COALESCE(MAX(seq), 0)").Scan(&maxSeq).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "标记已读失败"})
|
||||
return
|
||||
}
|
||||
if err := model.DB.Model(&model.Session{}).Where("id = ?", session.ID).Update("last_read_seq", maxSeq).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "标记已读失败"})
|
||||
return
|
||||
}
|
||||
middleware.JSON(c, gin.H{"last_read_seq": maxSeq})
|
||||
}
|
||||
|
||||
func (h *SessionHandler) AddNote(c *gin.Context) {
|
||||
session, ok := loadTenantSession(c, c.Param("id"))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !canOperateSession(c, session) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权添加内部备注"})
|
||||
return
|
||||
}
|
||||
var req CreateNoteReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
content := strings.TrimSpace(req.Content)
|
||||
if content == "" || utf8.RuneCountInString(content) > 500 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "内部备注需为 1 至 500 字"})
|
||||
return
|
||||
}
|
||||
event := model.SessionEvent{SessionID: session.ID, OperatorID: middleware.GetUserID(c), Action: "note", Detail: content}
|
||||
if err := model.DB.Create(&event).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "保存内部备注失败"})
|
||||
return
|
||||
}
|
||||
middleware.JSON(c, event)
|
||||
}
|
||||
|
||||
func (h *SessionHandler) ListAvailableAgents(c *gin.Context) {
|
||||
type agentItem struct {
|
||||
ID uint `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
var users []model.User
|
||||
if err := model.DB.Where("tenant_id = ? AND role = ? AND status = ?", middleware.GetTenantID(c), "agent", "online").
|
||||
Order("nickname asc").Find(&users).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询在线客服失败"})
|
||||
return
|
||||
}
|
||||
items := make([]agentItem, 0, len(users))
|
||||
for _, user := range users {
|
||||
items = append(items, agentItem{ID: user.ID, Nickname: user.Nickname, Status: user.Status})
|
||||
}
|
||||
middleware.JSON(c, items)
|
||||
}
|
||||
|
||||
func (h *SessionHandler) Create(c *gin.Context) {
|
||||
@@ -222,6 +335,8 @@ func (h *SessionHandler) Create(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "创建会话失败"})
|
||||
return
|
||||
}
|
||||
model.DB.Model(&model.Customer{}).Where("id = ? AND tenant_id = ?", customer.ID, tenantID).
|
||||
Updates(map[string]interface{}{"conversation_count": gorm.Expr("conversation_count + 1"), "last_contact_at": time.Now()})
|
||||
|
||||
middleware.JSON(c, session)
|
||||
}
|
||||
@@ -259,7 +374,7 @@ func (h *SessionHandler) Assign(c *gin.Context) {
|
||||
|
||||
result := model.DB.Model(&model.Session{}).
|
||||
Where("id = ? AND tenant_id = ? AND status = ?", session.ID, session.TenantID, "waiting").
|
||||
Updates(map[string]interface{}{"agent_id": req.AgentID, "status": "active"})
|
||||
Updates(map[string]interface{}{"agent_id": req.AgentID, "status": "active", "last_read_seq": 0})
|
||||
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "分配失败"})
|
||||
@@ -299,7 +414,7 @@ func (h *SessionHandler) Transfer(c *gin.Context) {
|
||||
}
|
||||
result := model.DB.Model(&model.Session{}).
|
||||
Where("id = ? AND tenant_id = ? AND status = ?", session.ID, session.TenantID, "active").
|
||||
Update("agent_id", req.AgentID)
|
||||
Updates(map[string]interface{}{"agent_id": req.AgentID, "last_read_seq": 0})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "转接失败"})
|
||||
return
|
||||
@@ -336,6 +451,10 @@ func (h *SessionHandler) End(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "请填写结束原因"})
|
||||
return
|
||||
}
|
||||
if reason != "resolved" && reason != "no_response" && reason != "visitor_left" && reason != "transferred" && reason != "other" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "结束原因无效"})
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
|
||||
result := model.DB.Model(&model.Session{}).
|
||||
|
||||
@@ -105,6 +105,8 @@ func (h *WidgetHandler) Init(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "创建会话失败"})
|
||||
return
|
||||
}
|
||||
model.DB.Model(&model.Customer{}).Where("id = ? AND tenant_id = ?", customer.ID, channel.TenantID).
|
||||
Updates(map[string]interface{}{"conversation_count": gorm.Expr("conversation_count + 1"), "last_contact_at": time.Now()})
|
||||
if payload, err := ws.NewEvent("session_created", session.ID, session); err == nil {
|
||||
ws.DefaultHub.BroadcastToTenantStaff(session.TenantID, payload)
|
||||
}
|
||||
@@ -150,6 +152,11 @@ func (h *WidgetHandler) SendMessage(c *gin.Context) {
|
||||
if req.Type == "" {
|
||||
req.Type = "text"
|
||||
}
|
||||
content, err := validateMessageContent(req.Type, req.Content)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
session, ok := loadVisitorSession(c, req.SessionID, visitorTokenFromRequest(c, req.VisitorToken))
|
||||
if !ok {
|
||||
@@ -160,21 +167,12 @@ func (h *WidgetHandler) SendMessage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是等待中的会话,更新为活跃
|
||||
if session.Status == "waiting" {
|
||||
if err := model.DB.Model(session).Update("status", "active").Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "更新会话失败"})
|
||||
return
|
||||
}
|
||||
session.Status = "active"
|
||||
}
|
||||
|
||||
custID := session.CustomerID
|
||||
msg := model.Message{
|
||||
SessionID: session.ID,
|
||||
SenderType: "visitor",
|
||||
SenderID: &custID,
|
||||
Content: req.Content,
|
||||
Content: content,
|
||||
Type: req.Type,
|
||||
SentAt: time.Now(),
|
||||
}
|
||||
@@ -183,8 +181,14 @@ func (h *WidgetHandler) SendMessage(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "发送失败"})
|
||||
return
|
||||
}
|
||||
model.DB.Model(&model.Customer{}).Where("id = ? AND tenant_id = ?", session.CustomerID, session.TenantID).Update("last_contact_at", time.Now())
|
||||
if payload, err := ws.NewEvent("message", session.ID, msg); err == nil {
|
||||
ws.DefaultHub.BroadcastToSession(session.TenantID, session.ID, session.AgentID, payload)
|
||||
if session.AgentID == nil {
|
||||
// 等待会话的消息要通知全部客服,便于任一在线客服及时领取。
|
||||
ws.DefaultHub.BroadcastToTenantStaff(session.TenantID, payload)
|
||||
} else {
|
||||
ws.DefaultHub.BroadcastToSession(session.TenantID, session.ID, session.AgentID, payload)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": msg})
|
||||
|
||||
Reference in New Issue
Block a user