Files
kefu_cloud/server/internal/handler/session.go
T
yml2213 8b9f3d710f refactor(server): 会话/访客/设置业务逻辑下沉 internal/service 层,handler 瘦身
- handler 只保留请求绑定、权限检查与响应映射;业务校验顺序、状态码与文案逐字保留
- 新增 service.Error 语义错误,由 handler 统一映射 HTTP 响应
- session 875→361 行、widget 745→321 行、settings 540→73 行
- 自动分配/租户设置(工作时间、欢迎语)/消息校验/黑名单检查/联系方式提取随迁 service 包
- 相关单元测试随迁 service 包,go test ./... 全部通过
2026-09-05 14:39:34 +08:00

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})
}