完成客服工作台闭环
This commit is contained in:
@@ -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{}).
|
||||
|
||||
Reference in New Issue
Block a user