| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- package controller
- import (
- "log"
- "net/http"
- "path/filepath"
- "strconv"
- "strings"
- "github.com/2930134478/AI-CS/backend/infra"
- "github.com/2930134478/AI-CS/backend/service"
- "github.com/gin-gonic/gin"
- )
- // MessageController 负责处理消息相关的 HTTP 请求。
- type MessageController struct {
- messageService *service.MessageService
- storageService infra.StorageService
- }
- // NewMessageController 创建 MessageController 实例。
- func NewMessageController(messageService *service.MessageService, storageService infra.StorageService) *MessageController {
- return &MessageController{
- messageService: messageService,
- storageService: storageService,
- }
- }
- type createMessageRequest struct {
- ConversationID uint `json:"conversation_id"`
- Content string `json:"content"`
- SenderIsAgent bool `json:"sender_is_agent"`
- SenderID uint `json:"sender_id"`
- // 文件相关字段(可选)
- FileURL *string `json:"file_url"`
- FileType *string `json:"file_type"`
- FileName *string `json:"file_name"`
- FileSize *int64 `json:"file_size"`
- MimeType *string `json:"mime_type"`
- }
- // CreateMessage 处理发送消息的请求。
- func (mc *MessageController) CreateMessage(c *gin.Context) {
- var req createMessageRequest
- if err := c.ShouldBindJSON(&req); err != nil || req.ConversationID == 0 {
- c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
- return
- }
- // 验证:必须有内容或文件
- if req.Content == "" && req.FileURL == nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "消息内容或文件不能同时为空"})
- return
- }
- _, err := mc.messageService.CreateMessage(service.CreateMessageInput{
- ConversationID: req.ConversationID,
- Content: req.Content,
- SenderID: req.SenderID,
- SenderIsAgent: req.SenderIsAgent,
- FileURL: req.FileURL,
- FileType: req.FileType,
- FileName: req.FileName,
- FileSize: req.FileSize,
- MimeType: req.MimeType,
- })
- if err != nil {
- log.Printf("❌ 创建消息失败: 对话ID=%d, 错误=%v", req.ConversationID, err)
- switch err {
- case service.ErrConversationClosed:
- c.JSON(http.StatusBadRequest, gin.H{"error": "会话已关闭"})
- case service.ErrConversationNotFound:
- c.JSON(http.StatusBadRequest, gin.H{"error": "会话不存在"})
- default:
- c.JSON(http.StatusInternalServerError, gin.H{"error": "创建消息失败"})
- }
- return
- }
- c.JSON(http.StatusOK, gin.H{"message": "创建消息成功"})
- }
- // ListMessages 返回指定会话的消息列表。
- // 查询参数:
- // - conversation_id: 会话ID(必需)
- // - include_ai_messages: 是否包含 AI 消息(可选,默认 false)
- func (mc *MessageController) ListMessages(c *gin.Context) {
- conversationIDStr := c.Query("conversation_id")
- if conversationIDStr == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "会话ID不能为空"})
- return
- }
- conversationID, err := strconv.ParseUint(conversationIDStr, 10, 64)
- if err != nil || conversationID == 0 {
- c.JSON(http.StatusBadRequest, gin.H{"error": "会话ID不合法"})
- return
- }
- // 解析 include_ai_messages 参数(默认 false)
- includeAIMessages := c.DefaultQuery("include_ai_messages", "false") == "true"
- messages, err := mc.messageService.ListMessages(uint(conversationID), includeAIMessages)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "查询消息失败"})
- return
- }
- c.JSON(http.StatusOK, messages)
- }
- type markMessagesReadRequest struct {
- ConversationID uint `json:"conversation_id"`
- ReaderIsAgent bool `json:"reader_is_agent"`
- }
- // MarkMessagesRead 将指定会话的消息标记为已读。
- func (mc *MessageController) MarkMessagesRead(c *gin.Context) {
- var req markMessagesReadRequest
- if err := c.ShouldBindJSON(&req); err != nil || req.ConversationID == 0 {
- c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"})
- return
- }
- result, err := mc.messageService.MarkMessagesRead(req.ConversationID, req.ReaderIsAgent)
- if err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": "更新消息状态失败"})
- return
- }
- c.JSON(http.StatusOK, gin.H{
- "updated": len(result.MessageIDs),
- "message_ids": result.MessageIDs,
- "conversation_id": result.ConversationID,
- "unread_count": result.UnreadCount,
- "read_at": formatTimeValue(result.ReadAt),
- })
- }
- // UploadFile 处理文件上传请求。
- // 请求格式:multipart/form-data
- // - file: 文件内容(必需)
- // - conversation_id: 对话ID(可选,用于组织目录)
- func (mc *MessageController) UploadFile(c *gin.Context) {
- // 解析文件
- file, err := c.FormFile("file")
- if err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": "文件不能为空"})
- return
- }
- // 验证文件大小(10MB)
- const maxFileSize = 10 * 1024 * 1024 // 10MB
- if file.Size > maxFileSize {
- c.JSON(http.StatusBadRequest, gin.H{"error": "文件大小超过限制(最大10MB)"})
- return
- }
- // 验证文件类型
- ext := strings.ToLower(filepath.Ext(file.Filename))
- allowedExts := map[string]bool{
- ".jpg": true,
- ".jpeg": true,
- ".png": true,
- ".gif": true,
- ".webp": true,
- ".pdf": true,
- ".doc": true,
- ".docx": true,
- ".txt": true,
- }
- if !allowedExts[ext] {
- c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的文件类型"})
- return
- }
- // 获取对话ID(可选)
- var conversationID uint
- if conversationIDStr := c.PostForm("conversation_id"); conversationIDStr != "" {
- if id, err := strconv.ParseUint(conversationIDStr, 10, 64); err == nil {
- conversationID = uint(id)
- }
- }
- // 打开文件
- src, err := file.Open()
- if err != nil {
- log.Printf("❌ 打开文件失败: %v", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": "打开文件失败"})
- return
- }
- defer src.Close()
- // 保存文件
- fileURL, err := mc.storageService.SaveMessageFile(conversationID, src, file.Filename)
- if err != nil {
- log.Printf("❌ 保存文件失败: %v", err)
- c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
- return
- }
- // 判断文件类型
- fileType := "document"
- mimeType := file.Header.Get("Content-Type")
- if strings.HasPrefix(mimeType, "image/") {
- fileType = "image"
- }
- // 返回文件信息
- c.JSON(http.StatusOK, gin.H{
- "success": true,
- "data": gin.H{
- "file_url": fileURL,
- "file_type": fileType,
- "file_name": file.Filename,
- "file_size": file.Size,
- "mime_type": mimeType,
- },
- })
- }
|