package upload import ( "fmt" "io" "mime/multipart" "os" "path/filepath" "strings" "time" "photography-backend/internal/config" ) // UploadService 文件上传服务 type UploadService struct { config *config.Config uploadDir string baseURL string } // UploadResult 上传结果 type UploadResult struct { Filename string `json:"filename"` OriginalName string `json:"original_name"` FilePath string `json:"file_path"` FileURL string `json:"file_url"` FileSize int64 `json:"file_size"` MimeType string `json:"mime_type"` } // NewUploadService 创建文件上传服务 func NewUploadService(cfg *config.Config) *UploadService { uploadDir := cfg.Storage.Local.BasePath if uploadDir == "" { uploadDir = "./uploads" } baseURL := cfg.Storage.Local.BaseURL if baseURL == "" { baseURL = fmt.Sprintf("http://localhost:%d/uploads", cfg.App.Port) } // 确保上传目录存在 os.MkdirAll(uploadDir, 0755) // 创建子目录 subdirs := []string{"photos", "thumbnails", "temp"} for _, subdir := range subdirs { os.MkdirAll(filepath.Join(uploadDir, subdir), 0755) } return &UploadService{ config: cfg, uploadDir: uploadDir, baseURL: baseURL, } } // UploadPhoto 上传照片 func (s *UploadService) UploadPhoto(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) { // 验证文件类型 if !s.isValidImageType(header.Header.Get("Content-Type")) { return nil, fmt.Errorf("不支持的文件类型: %s", header.Header.Get("Content-Type")) } // 验证文件大小 if header.Size > s.config.Upload.MaxFileSize { return nil, fmt.Errorf("文件大小超过限制: %d bytes", header.Size) } // 生成唯一文件名 filename := s.generateUniqueFilename(header.Filename) // 保存文件到 photos 目录 photoPath := filepath.Join(s.uploadDir, "photos", filename) // 创建目标文件 dst, err := os.Create(photoPath) if err != nil { return nil, fmt.Errorf("创建文件失败: %w", err) } defer dst.Close() // 复制文件内容 _, err = io.Copy(dst, file) if err != nil { return nil, fmt.Errorf("保存文件失败: %w", err) } // 构造文件URL fileURL := fmt.Sprintf("%s/photos/%s", s.baseURL, filename) return &UploadResult{ Filename: filename, OriginalName: header.Filename, FilePath: photoPath, FileURL: fileURL, FileSize: header.Size, MimeType: header.Header.Get("Content-Type"), }, nil } // DeletePhoto 删除照片 func (s *UploadService) DeletePhoto(filename string) error { // 删除原图 photoPath := filepath.Join(s.uploadDir, "photos", filename) if err := os.Remove(photoPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("删除文件失败: %w", err) } // 删除缩略图 thumbnailPath := filepath.Join(s.uploadDir, "thumbnails", filename) os.Remove(thumbnailPath) // 忽略错误,因为缩略图可能不存在 return nil } // GetPhotoURL 获取照片URL func (s *UploadService) GetPhotoURL(filename string) string { return fmt.Sprintf("%s/photos/%s", s.baseURL, filename) } // isValidImageType 验证是否为有效的图片类型 func (s *UploadService) isValidImageType(mimeType string) bool { allowedTypes := s.config.Upload.AllowedTypes if len(allowedTypes) == 0 { // 默认允许的图片类型 allowedTypes = []string{ "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", "image/tiff", } } for _, allowedType := range allowedTypes { if mimeType == allowedType { return true } } return false } // generateUniqueFilename 生成唯一文件名 func (s *UploadService) generateUniqueFilename(originalName string) string { ext := filepath.Ext(originalName) timestamp := time.Now().Unix() // 清理原文件名 baseName := strings.TrimSuffix(originalName, ext) baseName = strings.ReplaceAll(baseName, " ", "_") return fmt.Sprintf("%s_%d%s", baseName, timestamp, ext) } // GetUploadStats 获取上传统计信息 func (s *UploadService) GetUploadStats() (map[string]interface{}, error) { photosDir := filepath.Join(s.uploadDir, "photos") var totalFiles int var totalSize int64 err := filepath.Walk(photosDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() { totalFiles++ totalSize += info.Size() } return nil }) if err != nil { return nil, err } return map[string]interface{}{ "total_files": totalFiles, "total_size": totalSize, "upload_dir": s.uploadDir, "base_url": s.baseURL, }, nil }