package service import ( "fmt" "io" "mime/multipart" "os" "path/filepath" "strings" "time" "github.com/google/uuid" ) // UploadService 文件上传服务接口 type UploadService interface { // 上传音频文件 UploadAudio(file *multipart.FileHeader) (*UploadResult, error) // 上传图片文件 UploadImage(file *multipart.FileHeader) (*UploadResult, error) // 删除文件 DeleteFile(filePath string) error // 获取文件URL GetFileURL(filePath string) string } type uploadService struct { uploadDir string baseURL string } // UploadResult 上传结果 type UploadResult struct { FileName string `json:"file_name"` FilePath string `json:"file_path"` FileURL string `json:"file_url"` FileSize int64 `json:"file_size"` ContentType string `json:"content_type"` } // NewUploadService 创建文件上传服务实例 func NewUploadService(uploadDir, baseURL string) UploadService { // 确保上传目录存在 os.MkdirAll(uploadDir, 0755) os.MkdirAll(filepath.Join(uploadDir, "audio"), 0755) os.MkdirAll(filepath.Join(uploadDir, "images"), 0755) return &uploadService{ uploadDir: uploadDir, baseURL: baseURL, } } // 允许的文件类型 var ( allowedAudioTypes = map[string]bool{ "audio/mpeg": true, // mp3 "audio/wav": true, // wav "audio/mp4": true, // m4a "audio/webm": true, // webm "audio/ogg": true, // ogg } allowedImageTypes = map[string]bool{ "image/jpeg": true, // jpg, jpeg "image/png": true, // png "image/gif": true, // gif "image/webp": true, // webp } // 文件大小限制(字节) maxAudioSize = 50 * 1024 * 1024 // 50MB maxImageSize = 10 * 1024 * 1024 // 10MB ) // UploadAudio 上传音频文件 func (s *uploadService) UploadAudio(file *multipart.FileHeader) (*UploadResult, error) { // 检查文件大小 if file.Size > int64(maxAudioSize) { return nil, fmt.Errorf("audio file too large, max size is %d MB", maxAudioSize/(1024*1024)) } // 检查文件类型 contentType := file.Header.Get("Content-Type") if !allowedAudioTypes[contentType] { return nil, fmt.Errorf("unsupported audio format: %s", contentType) } // 生成唯一文件名 ext := filepath.Ext(file.Filename) fileName := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext) relativePath := filepath.Join("audio", fileName) fullPath := filepath.Join(s.uploadDir, relativePath) // 保存文件 if err := s.saveFile(file, fullPath); err != nil { return nil, fmt.Errorf("failed to save audio file: %w", err) } return &UploadResult{ FileName: fileName, FilePath: relativePath, FileURL: s.GetFileURL(relativePath), FileSize: file.Size, ContentType: contentType, }, nil } // UploadImage 上传图片文件 func (s *uploadService) UploadImage(file *multipart.FileHeader) (*UploadResult, error) { // 检查文件大小 if file.Size > int64(maxImageSize) { return nil, fmt.Errorf("image file too large, max size is %d MB", maxImageSize/(1024*1024)) } // 检查文件类型 contentType := file.Header.Get("Content-Type") if !allowedImageTypes[contentType] { return nil, fmt.Errorf("unsupported image format: %s", contentType) } // 生成唯一文件名 ext := filepath.Ext(file.Filename) fileName := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext) relativePath := filepath.Join("images", fileName) fullPath := filepath.Join(s.uploadDir, relativePath) // 保存文件 if err := s.saveFile(file, fullPath); err != nil { return nil, fmt.Errorf("failed to save image file: %w", err) } return &UploadResult{ FileName: fileName, FilePath: relativePath, FileURL: s.GetFileURL(relativePath), FileSize: file.Size, ContentType: contentType, }, nil } // saveFile 保存文件到磁盘 func (s *uploadService) saveFile(fileHeader *multipart.FileHeader, destPath string) error { // 确保目录存在 dir := filepath.Dir(destPath) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("failed to create directory: %w", err) } // 打开上传的文件 src, err := fileHeader.Open() if err != nil { return fmt.Errorf("failed to open uploaded file: %w", err) } defer src.Close() // 创建目标文件 dst, err := os.Create(destPath) if err != nil { return fmt.Errorf("failed to create destination file: %w", err) } defer dst.Close() // 复制文件内容 _, err = io.Copy(dst, src) if err != nil { return fmt.Errorf("failed to copy file content: %w", err) } return nil } // DeleteFile 删除文件 func (s *uploadService) DeleteFile(filePath string) error { // 安全检查:确保文件路径在上传目录内 if !strings.HasPrefix(filePath, s.uploadDir) { fullPath := filepath.Join(s.uploadDir, filePath) filePath = fullPath } // 检查文件是否存在 if _, err := os.Stat(filePath); os.IsNotExist(err) { return fmt.Errorf("file does not exist: %s", filePath) } // 删除文件 if err := os.Remove(filePath); err != nil { return fmt.Errorf("failed to delete file: %w", err) } return nil } // GetFileURL 获取文件URL func (s *uploadService) GetFileURL(filePath string) string { // 清理路径分隔符 cleanPath := strings.ReplaceAll(filePath, "\\", "/") return fmt.Sprintf("%s/uploads/%s", s.baseURL, cleanPath) } // ValidateFileType 验证文件类型 func ValidateFileType(filename string, allowedTypes map[string]bool) bool { ext := strings.ToLower(filepath.Ext(filename)) switch ext { case ".mp3": return allowedTypes["audio/mpeg"] case ".wav": return allowedTypes["audio/wav"] case ".m4a": return allowedTypes["audio/mp4"] case ".webm": return allowedTypes["audio/webm"] case ".ogg": return allowedTypes["audio/ogg"] case ".jpg", ".jpeg": return allowedTypes["image/jpeg"] case ".png": return allowedTypes["image/png"] case ".gif": return allowedTypes["image/gif"] case ".webp": return allowedTypes["image/webp"] default: return false } }