package controller import ( "net/http" "strconv" "github.com/2930134478/AI-CS/backend/service" "github.com/2930134478/AI-CS/backend/utils" "github.com/gin-gonic/gin" ) // ConversationController 负责处理会话相关的 HTTP 请求。 type ConversationController struct { conversationService *service.ConversationService aiConfigService *service.AIConfigService // 用于获取开放的模型列表 users *service.UserService } // NewConversationController 创建 ConversationController 实例。 func NewConversationController( conversationService *service.ConversationService, aiConfigService *service.AIConfigService, users *service.UserService, ) *ConversationController { return &ConversationController{ conversationService: conversationService, aiConfigService: aiConfigService, users: users, } } type initConversationRequest struct { VisitorID uint `json:"visitor_id"` Website string `json:"website"` Referrer string `json:"referrer"` Browser string `json:"browser"` OS string `json:"os"` Language string `json:"language"` ChatMode string `json:"chat_mode"` // 对话模式:human(人工客服)、ai(AI客服) AIConfigID *uint `json:"ai_config_id"` // AI 配置 ID(访客选择的模型配置,AI 模式时必需) } type updateContactRequest struct { Email *string `json:"email"` Phone *string `json:"phone"` Notes *string `json:"notes"` } // InitConversation 为访客初始化或恢复会话。 func (cc *ConversationController) InitConversation(c *gin.Context) { var req initConversationRequest if err := c.ShouldBindJSON(&req); err != nil || req.VisitorID == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) return } browser := req.Browser os := req.OS if browser == "" || os == "" { parsedBrowser, parsedOS := utils.ParseUserAgent(c.GetHeader("User-Agent")) if browser == "" { browser = parsedBrowser } if os == "" { os = parsedOS } } result, err := cc.conversationService.InitConversation(service.InitConversationInput{ VisitorID: req.VisitorID, Website: req.Website, Referrer: req.Referrer, Browser: browser, OS: os, Language: req.Language, IPAddress: utils.GetClientIP(c), ChatMode: req.ChatMode, AIConfigID: req.AIConfigID, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "conversation_id": result.ConversationID, "status": result.Status, }) } // InitInternalConversation 为当前客服创建一条新的内部对话(知识库测试)。需要 query user_id。 func (cc *ConversationController) InitInternalConversation(c *gin.Context) { if !requirePermission(c, cc.users, string(service.PermKBTest)) { return } userIDStr := c.Query("user_id") if userIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "需要 user_id"}) return } userID, err := strconv.ParseUint(userIDStr, 10, 32) if err != nil || userID == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "user_id 不合法"}) return } result, err := cc.conversationService.InitInternalConversation(uint(userID)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "conversation_id": result.ConversationID, "status": result.Status, }) } // GetPublicAIModels 获取所有开放的模型配置(供访客选择)。 func (cc *ConversationController) GetPublicAIModels(c *gin.Context) { modelType := c.DefaultQuery("model_type", "text") models, err := cc.aiConfigService.GetPublicModels(modelType) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"models": models}) } // UpdateContactInfo 用于更新访客的联系信息。 func (cc *ConversationController) UpdateContactInfo(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "会话ID不合法"}) return } var req updateContactRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) return } if req.Email == nil && req.Phone == nil && req.Notes == nil { c.JSON(http.StatusBadRequest, gin.H{"error": "至少提供一个需要更新的字段"}) return } result, err := cc.conversationService.UpdateConversationContact(service.UpdateConversationContactInput{ ConversationID: uint(id), Email: req.Email, Phone: req.Phone, Notes: req.Notes, }) if err != nil { if err == service.ErrConversationNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "会话不存在"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"}) } return } c.JSON(http.StatusOK, gin.H{ "email": result.Email, "phone": result.Phone, "notes": result.Notes, }) } // CloseConversation 客服关闭会话(进入历史/归档)。 // POST /conversations/:id/close func (cc *ConversationController) CloseConversation(c *gin.Context) { if !requirePermission(c, cc.users, string(service.PermChat)) { return } id, err := parseUintParam(c, "id") if err != nil || id == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "会话ID不合法"}) return } userID := getUserIDFromHeader(c) if err := cc.conversationService.CloseConversation(uint(id), userID); err != nil { if err == service.ErrConversationNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "会话不存在"}) return } c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // ListConversations 返回当前活跃会话的列表。type=internal 时返回该客服的内部对话(知识库测试)。 func (cc *ConversationController) ListConversations(c *gin.Context) { var userID uint if userIDStr := c.Query("user_id"); userIDStr != "" { if parsed, err := strconv.ParseUint(userIDStr, 10, 32); err == nil { userID = uint(parsed) } } conversationType := c.DefaultQuery("type", "visitor") status := c.DefaultQuery("status", "open") var conversations []service.ConversationSummary var err error if conversationType == "internal" { if !requirePermission(c, cc.users, string(service.PermKBTest)) { return } if userID == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "内部对话列表需要 user_id"}) return } conversations, err = cc.conversationService.ListInternalConversations(userID, status) } else { conversations, err = cc.conversationService.ListConversations(userID, status) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "查询对话列表失败"}) return } items := make([]gin.H, 0, len(conversations)) for _, conv := range conversations { item := gin.H{ "id": conv.ID, "conversation_type": conv.ConversationType, "visitor_id": conv.VisitorID, "agent_id": conv.AgentID, "status": conv.Status, "chat_mode": conv.ChatMode, "created_at": formatTimeValue(conv.CreatedAt), "updated_at": formatTimeValue(conv.UpdatedAt), "unread_count": conv.UnreadCount, "has_participated": conv.HasParticipated, } // 添加 last_seen_at 字段(用于判断在线状态) if lastSeen := formatTimePointer(conv.LastSeenAt); lastSeen != "" { item["last_seen_at"] = lastSeen } if conv.LastMessage != nil { item["last_message"] = gin.H{ "id": conv.LastMessage.ID, "content": conv.LastMessage.Content, "sender_is_agent": conv.LastMessage.SenderIsAgent, "message_type": conv.LastMessage.MessageType, "is_read": conv.LastMessage.IsRead, "read_at": formatTimePointer(conv.LastMessage.ReadAt), "created_at": formatTimeValue(conv.LastMessage.CreatedAt), } } items = append(items, item) } c.JSON(http.StatusOK, items) } // GetConversationDetail 返回会话的详细信息。 func (cc *ConversationController) GetConversationDetail(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "会话ID不合法"}) return } // 从查询参数获取 user_id(可选,用于检查参与状态) var userID uint if userIDStr := c.Query("user_id"); userIDStr != "" { // 使用 strconv 解析查询参数(不是路径参数) if parsed, err := strconv.ParseUint(userIDStr, 10, 32); err == nil { userID = uint(parsed) } } detail, err := cc.conversationService.GetConversationDetail(uint(id), userID) if err != nil { if err == service.ErrConversationNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "会话不存在"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"}) } return } response := gin.H{ "id": detail.ID, "visitor_id": detail.VisitorID, "agent_id": detail.AgentID, "status": detail.Status, "website": detail.Website, "referrer": detail.Referrer, "browser": detail.Browser, "os": detail.OS, "language": detail.Language, "ip_address": detail.IPAddress, "location": detail.Location, "email": detail.Email, "phone": detail.Phone, "notes": detail.Notes, "created_at": formatTimeValue(detail.CreatedAt), "updated_at": formatTimeValue(detail.UpdatedAt), "unread_count": detail.UnreadCount, } if lastSeen := formatTimePointer(detail.LastSeen); lastSeen != "" { response["last_seen_at"] = lastSeen } if detail.LastMessage != nil { response["last_message"] = gin.H{ "id": detail.LastMessage.ID, "content": detail.LastMessage.Content, "sender_is_agent": detail.LastMessage.SenderIsAgent, "message_type": detail.LastMessage.MessageType, "is_read": detail.LastMessage.IsRead, "read_at": formatTimePointer(detail.LastMessage.ReadAt), "created_at": formatTimeValue(detail.LastMessage.CreatedAt), } } c.JSON(http.StatusOK, response) } // SearchConversations 根据关键字进行会话的模糊搜索。 func (cc *ConversationController) SearchConversations(c *gin.Context) { query := c.Query("q") if query == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "搜索关键词不能为空"}) return } status := c.DefaultQuery("status", "open") // 从查询参数获取 user_id(可选,用于检查参与状态) var userID uint if userIDStr := c.Query("user_id"); userIDStr != "" { // 使用 strconv 解析查询参数(不是路径参数) if parsed, err := strconv.ParseUint(userIDStr, 10, 32); err == nil { userID = uint(parsed) } } conversations, err := cc.conversationService.SearchConversations(query, userID, status) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "搜索失败"}) return } items := make([]gin.H, 0, len(conversations)) for _, conv := range conversations { item := gin.H{ "id": conv.ID, "visitor_id": conv.VisitorID, "agent_id": conv.AgentID, "status": conv.Status, "created_at": formatTimeValue(conv.CreatedAt), "updated_at": formatTimeValue(conv.UpdatedAt), "unread_count": conv.UnreadCount, "has_participated": conv.HasParticipated, // 当前用户是否参与过该会话 } // 添加 last_seen_at 字段(用于判断在线状态) if lastSeen := formatTimePointer(conv.LastSeenAt); lastSeen != "" { item["last_seen_at"] = lastSeen } if conv.LastMessage != nil { item["last_message"] = gin.H{ "id": conv.LastMessage.ID, "content": conv.LastMessage.Content, "sender_is_agent": conv.LastMessage.SenderIsAgent, "message_type": conv.LastMessage.MessageType, "is_read": conv.LastMessage.IsRead, "read_at": formatTimePointer(conv.LastMessage.ReadAt), "created_at": formatTimeValue(conv.LastMessage.CreatedAt), } } items = append(items, item) } c.JSON(http.StatusOK, items) }