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