package infra import ( "fmt" "io" "net/url" "os" "path/filepath" "strings" "time" ) // StorageService 文件存储服务接口(可扩展为云存储) type StorageService interface { // SaveAvatar 保存头像文件,返回文件URL SaveAvatar(userID uint, file io.Reader, filename string) (string, error) // SaveMessageFile 保存消息文件,返回文件URL // conversationID: 对话ID,用于组织文件目录 // file: 文件内容 // filename: 原始文件名 SaveMessageFile(conversationID uint, file io.Reader, filename string) (string, error) // ReadMessageFile 根据消息文件的 URL 或路径读取文件内容(用于多模态:识图等) // fileURLOrPath 为创建消息时返回的 file_url,可为相对路径如 /uploads/messages/1/xxx.jpg 或完整 URL ReadMessageFile(fileURLOrPath string) ([]byte, error) // DeleteFile 删除文件 DeleteFile(fileURL string) error // GetFileURL 获取文件的完整URL GetFileURL(filePath string) string } // LocalStorageService 本地文件存储服务 type LocalStorageService struct { baseDir string // 基础目录 publicPath string // 公共访问路径 } // NewLocalStorageService 创建本地存储服务实例 func NewLocalStorageService(baseDir, publicPath string) *LocalStorageService { // 确保基础目录存在 if err := os.MkdirAll(baseDir, 0755); err != nil { panic(fmt.Sprintf("创建存储目录失败: %v", err)) } // 确保头像目录存在 avatarDir := filepath.Join(baseDir, "avatars") if err := os.MkdirAll(avatarDir, 0755); err != nil { panic(fmt.Sprintf("创建头像目录失败: %v", err)) } return &LocalStorageService{ baseDir: baseDir, publicPath: publicPath, } } // SaveAvatar 保存头像文件 func (s *LocalStorageService) SaveAvatar(userID uint, file io.Reader, filename string) (string, error) { // 获取文件扩展名 ext := filepath.Ext(filename) if ext == "" { ext = ".jpg" // 默认使用 jpg } // 生成唯一文件名:user_{userID}_{timestamp}{ext} timestamp := time.Now().Unix() newFilename := fmt.Sprintf("user_%d_%d%s", userID, timestamp, ext) // 保存到 avatars 目录 avatarDir := filepath.Join(s.baseDir, "avatars") filePath := filepath.Join(avatarDir, newFilename) // 创建文件 dst, err := os.Create(filePath) if err != nil { return "", fmt.Errorf("创建文件失败: %w", err) } defer dst.Close() // 复制文件内容 if _, err := io.Copy(dst, file); err != nil { return "", fmt.Errorf("保存文件失败: %w", err) } // 返回相对路径(用于构建URL) relativePath := filepath.Join("avatars", newFilename) return s.GetFileURL(relativePath), nil } // ReadMessageFile 根据消息文件的 URL 或路径读取文件内容。 func (s *LocalStorageService) ReadMessageFile(fileURLOrPath string) ([]byte, error) { pathPart := fileURLOrPath if strings.Contains(fileURLOrPath, "://") { u, err := url.Parse(fileURLOrPath) if err != nil { return nil, fmt.Errorf("解析文件 URL 失败: %w", err) } pathPart = u.Path } pathPart = strings.TrimPrefix(pathPart, "/") publicPathTrimmed := strings.TrimPrefix(strings.TrimSuffix(s.publicPath, "/"), "/") if publicPathTrimmed != "" && strings.HasPrefix(pathPart, publicPathTrimmed+"/") { pathPart = strings.TrimPrefix(pathPart, publicPathTrimmed+"/") } else if publicPathTrimmed != "" && pathPart == publicPathTrimmed { pathPart = "" } if pathPart == "" { return nil, fmt.Errorf("无法从 URL 解析出相对路径: %s", fileURLOrPath) } fullPath := filepath.Join(s.baseDir, pathPart) return os.ReadFile(fullPath) } // DeleteFile 删除文件 func (s *LocalStorageService) DeleteFile(fileURL string) error { // 从URL中提取文件路径 // 假设URL格式为: /uploads/avatars/filename.jpg // 需要去掉 /uploads/ 前缀,得到相对路径 relativePath := fileURL if len(s.publicPath) > 0 && len(fileURL) > len(s.publicPath) { if fileURL[:len(s.publicPath)] == s.publicPath { relativePath = fileURL[len(s.publicPath):] // 去掉开头的 / if len(relativePath) > 0 && relativePath[0] == '/' { relativePath = relativePath[1:] } } } filePath := filepath.Join(s.baseDir, relativePath) if err := os.Remove(filePath); err != nil { if os.IsNotExist(err) { return nil // 文件不存在,认为删除成功 } return fmt.Errorf("删除文件失败: %w", err) } return nil } // SaveMessageFile 保存消息文件 func (s *LocalStorageService) SaveMessageFile(conversationID uint, file io.Reader, filename string) (string, error) { // 获取文件扩展名 ext := filepath.Ext(filename) if ext == "" { ext = ".bin" // 默认扩展名 } // 生成唯一文件名:{timestamp}_{原始文件名} timestamp := time.Now().Unix() // 清理文件名,移除特殊字符 safeFilename := filepath.Base(filename) if len(safeFilename) > 100 { // 文件名过长,截断 safeFilename = safeFilename[:100] } newFilename := fmt.Sprintf("%d_%s", timestamp, safeFilename) // 按对话ID组织目录:messages/{conversationID}/ messageDir := filepath.Join(s.baseDir, "messages", fmt.Sprintf("%d", conversationID)) if err := os.MkdirAll(messageDir, 0755); err != nil { return "", fmt.Errorf("创建消息文件目录失败: %w", err) } filePath := filepath.Join(messageDir, newFilename) // 创建文件 dst, err := os.Create(filePath) if err != nil { return "", fmt.Errorf("创建文件失败: %w", err) } defer dst.Close() // 复制文件内容 if _, err := io.Copy(dst, file); err != nil { return "", fmt.Errorf("保存文件失败: %w", err) } // 返回相对路径(用于构建URL) relativePath := filepath.Join("messages", fmt.Sprintf("%d", conversationID), newFilename) return s.GetFileURL(relativePath), nil } // GetFileURL 获取文件的完整URL func (s *LocalStorageService) GetFileURL(filePath string) string { // 确保路径使用正斜杠(用于URL) urlPath := filepath.ToSlash(filePath) // 如果 publicPath 为空,返回相对路径 if s.publicPath == "" { return "/" + urlPath } // 确保 publicPath 以 / 结尾 publicPath := s.publicPath if publicPath[len(publicPath)-1] != '/' { publicPath += "/" } // 确保 urlPath 不以 / 开头 if len(urlPath) > 0 && urlPath[0] == '/' { urlPath = urlPath[1:] } return publicPath + urlPath }