- handler 只保留请求绑定、权限检查与响应映射;业务校验顺序、状态码与文案逐字保留 - 新增 service.Error 语义错误,由 handler 统一映射 HTTP 响应 - session 875→361 行、widget 745→321 行、settings 540→73 行 - 自动分配/租户设置(工作时间、欢迎语)/消息校验/黑名单检查/联系方式提取随迁 service 包 - 相关单元测试随迁 service 包,go test ./... 全部通过
362 lines
10 KiB
Go
362 lines
10 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"kefu-cloud/server/internal/middleware"
|
|
"kefu-cloud/server/internal/service"
|
|
)
|
|
|
|
type SessionHandler struct{}
|
|
|
|
func NewSessionHandler() *SessionHandler { return &SessionHandler{} }
|
|
|
|
type SendMessageReq struct {
|
|
Content string `json:"content" binding:"required"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
type CreateNoteReq struct {
|
|
Content string `json:"content" binding:"required"`
|
|
}
|
|
|
|
type CreateSessionReq struct {
|
|
ChannelID uint `json:"channel_id"`
|
|
CustomerID uint `json:"customer_id"`
|
|
Priority string `json:"priority"`
|
|
}
|
|
|
|
type AssignSessionReq struct {
|
|
AgentID uint `json:"agent_id"`
|
|
}
|
|
|
|
// sessionActor 从鉴权中间件结果构造 service 层发起者。
|
|
func sessionActor(c *gin.Context) service.Actor {
|
|
return service.Actor{
|
|
UserID: middleware.GetUserID(c),
|
|
SessionAll: middleware.CanAccessAllData(c, "session"),
|
|
HistoryAll: middleware.CanAccessAllData(c, "chat_history"),
|
|
}
|
|
}
|
|
|
|
// respondServiceError 将 service 业务错误映射为 HTTP 响应。
|
|
func respondServiceError(c *gin.Context, err error) {
|
|
var be *service.Error
|
|
if errors.As(err, &be) {
|
|
c.JSON(be.Status, gin.H{"code": be.Status, "message": be.Message})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "服务器内部错误"})
|
|
}
|
|
|
|
func (h *SessionHandler) SendMessage(c *gin.Context) {
|
|
var req SendMessageReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
|
return
|
|
}
|
|
if req.Type == "" {
|
|
req.Type = "text"
|
|
}
|
|
// 先校验内容,再加载会话(与原校验顺序一致)
|
|
content, err := service.ValidateMessageContent(req.Type, req.Content)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
|
|
return
|
|
}
|
|
session, err := service.LoadTenantSession(c.Param("id"), middleware.GetTenantID(c))
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
msg, err := service.AgentSendMessage(session, sessionActor(c), req.Type, content)
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, msg)
|
|
}
|
|
|
|
func (h *SessionHandler) List(c *gin.Context) {
|
|
page, pageSize := middleware.GetPageParams(c)
|
|
f := service.SessionFilter{
|
|
Status: c.Query("status"),
|
|
Priority: c.Query("priority"),
|
|
AgentID: c.Query("agent_id"),
|
|
ChannelID: c.Query("channel_id"),
|
|
Search: strings.TrimSpace(c.Query("search")),
|
|
From: c.Query("from"),
|
|
To: c.Query("to"),
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
}
|
|
items, total, err := service.ListSessions(sessionActor(c), middleware.GetTenantID(c), f)
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSONList(c, items, total, page, pageSize)
|
|
}
|
|
|
|
func (h *SessionHandler) Get(c *gin.Context) {
|
|
session, err := service.LoadTenantSession(c.Param("id"), middleware.GetTenantID(c))
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
if !service.CanReadSession(sessionActor(c), session) {
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权查看该会话"})
|
|
return
|
|
}
|
|
detail, err := service.SessionDetail(session)
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, gin.H{
|
|
"session": session,
|
|
"messages": detail.Messages,
|
|
"events": detail.Events,
|
|
"page_views": detail.PageViews,
|
|
"pending_count": detail.PendingCount,
|
|
"max_seq": detail.MaxSeq,
|
|
})
|
|
}
|
|
|
|
// ListMessages 按 seq 增量拉取消息:after_seq 之后的新消息(重连 catch-up)。
|
|
// GET /sessions/:id/messages?after_seq=12
|
|
func (h *SessionHandler) ListMessages(c *gin.Context) {
|
|
session, err := service.LoadTenantSession(c.Param("id"), middleware.GetTenantID(c))
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
if !service.CanReadSession(sessionActor(c), session) {
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权查看该会话消息"})
|
|
return
|
|
}
|
|
|
|
afterSeq := 0
|
|
if raw := strings.TrimSpace(c.Query("after_seq")); raw != "" {
|
|
parsed, err := strconv.Atoi(raw)
|
|
if err != nil || parsed < 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "after_seq 无效"})
|
|
return
|
|
}
|
|
afterSeq = parsed
|
|
}
|
|
|
|
limit := 200
|
|
if raw := strings.TrimSpace(c.Query("limit")); raw != "" {
|
|
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 {
|
|
if parsed > 500 {
|
|
parsed = 500
|
|
}
|
|
limit = parsed
|
|
}
|
|
}
|
|
|
|
page, err := service.SessionMessagePage(session.ID, afterSeq, limit)
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, gin.H{
|
|
"messages": page.Messages,
|
|
"after_seq": page.AfterSeq,
|
|
"max_seq": page.MaxSeq,
|
|
"has_more": page.HasMore,
|
|
})
|
|
}
|
|
|
|
func (h *SessionHandler) MarkRead(c *gin.Context) {
|
|
session, err := service.LoadTenantSession(c.Param("id"), middleware.GetTenantID(c))
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
if !service.CanReadSession(sessionActor(c), session) {
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权标记该会话已读"})
|
|
return
|
|
}
|
|
lastReadSeq, err := service.MarkSessionRead(session, sessionActor(c))
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, gin.H{"last_read_seq": lastReadSeq})
|
|
}
|
|
|
|
func (h *SessionHandler) AddNote(c *gin.Context) {
|
|
session, err := service.LoadTenantSession(c.Param("id"), middleware.GetTenantID(c))
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
if !service.CanOperateSession(sessionActor(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
|
|
}
|
|
event, err := service.AddSessionNote(session, middleware.GetUserID(c), req.Content)
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, event)
|
|
}
|
|
|
|
func (h *SessionHandler) ListAvailableAgents(c *gin.Context) {
|
|
// all=1 返回租户全部坐席(含离线),用于对话记录筛选;
|
|
// 默认:可转接坐席(role=agent,在线/忙碌),便于前端展示状态
|
|
items, err := service.ListAvailableAgents(middleware.GetTenantID(c), c.Query("all") == "1")
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, items)
|
|
}
|
|
|
|
func (h *SessionHandler) Create(c *gin.Context) {
|
|
actor := sessionActor(c)
|
|
if !actor.SessionAll {
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "仅主管或管理员可创建会话"})
|
|
return
|
|
}
|
|
var req CreateSessionReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
|
return
|
|
}
|
|
session, err := service.CreateSession(middleware.GetTenantID(c), req.ChannelID, req.CustomerID, req.Priority)
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, session)
|
|
}
|
|
|
|
func (h *SessionHandler) Assign(c *gin.Context) {
|
|
session, err := service.LoadTenantSession(c.Param("id"), middleware.GetTenantID(c))
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
var req AssignSessionReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
|
return
|
|
}
|
|
detail, err := service.AssignSession(session, sessionActor(c), middleware.HasPermission(c, "session.transfer"), req.AgentID)
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, gin.H{"message": "分配成功", "detail": detail})
|
|
}
|
|
|
|
func (h *SessionHandler) Transfer(c *gin.Context) {
|
|
session, err := service.LoadTenantSession(c.Param("id"), middleware.GetTenantID(c))
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
if !service.CanOperateSession(sessionActor(c), session) {
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权转接该会话"})
|
|
return
|
|
}
|
|
var req AssignSessionReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
|
return
|
|
}
|
|
detail, err := service.TransferSession(session, sessionActor(c), req.AgentID)
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, gin.H{"message": "转接成功", "detail": detail})
|
|
}
|
|
|
|
func (h *SessionHandler) End(c *gin.Context) {
|
|
session, err := service.LoadTenantSession(c.Param("id"), middleware.GetTenantID(c))
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
if !service.CanOperateSession(sessionActor(c), session) {
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权结束该会话"})
|
|
return
|
|
}
|
|
if err := service.EndSession(session, middleware.GetUserID(c), c.Query("reason")); err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, gin.H{"message": "已结束"})
|
|
}
|
|
|
|
func (h *SessionHandler) UpdatePriority(c *gin.Context) {
|
|
session, err := service.LoadTenantSession(c.Param("id"), middleware.GetTenantID(c))
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
if !service.CanOperateSession(sessionActor(c), session) {
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权更新该会话"})
|
|
return
|
|
}
|
|
if err := service.UpdateSessionPriority(session, sessionActor(c), c.Query("priority")); err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, gin.H{"message": "已更新"})
|
|
}
|
|
|
|
// Archive 将已结束会话归档(主管/管理员)
|
|
func (h *SessionHandler) Archive(c *gin.Context) {
|
|
if !middleware.HasPermission(c, "chat_history.batch_archive") {
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "仅主管或管理员可归档"})
|
|
return
|
|
}
|
|
session, err := service.LoadTenantSession(c.Param("id"), middleware.GetTenantID(c))
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
archived, err := service.ArchiveSession(session, middleware.GetUserID(c))
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, gin.H{"message": "已归档", "session": archived})
|
|
}
|
|
|
|
// BatchArchive 批量归档已结束会话
|
|
func (h *SessionHandler) BatchArchive(c *gin.Context) {
|
|
if !middleware.HasPermission(c, "chat_history.batch_archive") {
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "仅主管或管理员可归档"})
|
|
return
|
|
}
|
|
var req struct {
|
|
IDs []uint `json:"ids" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "请选择要归档的会话"})
|
|
return
|
|
}
|
|
count, err := service.BatchArchiveSessions(middleware.GetTenantID(c), middleware.GetUserID(c), req.IDs)
|
|
if err != nil {
|
|
respondServiceError(c, err)
|
|
return
|
|
}
|
|
middleware.JSON(c, gin.H{"message": "已归档", "count": count})
|
|
}
|