package services import ( "context" "crypto/md5" "encoding/hex" "fmt" "io" "mime/multipart" "os" "path/filepath" "strings" "time" "server/models" "github.com/qiniu/go-sdk/v7/auth/qbox" "github.com/qiniu/go-sdk/v7/storage" ) // StorageService 存储服务接口 type StorageService interface { Upload(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) GetPublicURL(key string) string Delete(key string) error } // UploadResult 上传结果 type UploadResult struct { URL string // 完整访问URL Key string // 存储key/路径 Size int64 // 文件大小 MD5 string // 文件MD5 MimeType string // 文件类型 } // LocalStorage 本地存储实现 type LocalStorage struct { BaseDir string // 基础目录,默认 "uploads" BaseURL string // 基础URL,默认 "/" } // NewLocalStorage 创建本地存储服务 func NewLocalStorage() *LocalStorage { return &LocalStorage{ BaseDir: "uploads", BaseURL: "/", } } // Upload 上传文件到本地 func (s *LocalStorage) Upload(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) { // 生成存储路径 ext := filepath.Ext(header.Filename) datePath := time.Now().Format("2006/01/02") fileName := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext) savePath := filepath.Join(datePath, fileName) // 创建目录 destDir := filepath.Join(s.BaseDir, filepath.FromSlash(datePath)) if err := os.MkdirAll(destDir, 0755); err != nil { return nil, fmt.Errorf("创建目录失败: %w", err) } // 保存文件 destPath := filepath.Join(s.BaseDir, filepath.FromSlash(savePath)) dst, err := os.Create(destPath) if err != nil { return nil, fmt.Errorf("创建文件失败: %w", err) } defer dst.Close() // 计算MD5并复制文件 hash := md5.New() size, err := io.Copy(io.MultiWriter(dst, hash), file) if err != nil { _ = os.Remove(destPath) return nil, fmt.Errorf("保存文件失败: %w", err) } md5Sum := hex.EncodeToString(hash.Sum(nil)) webURL := s.BaseURL + strings.ReplaceAll(filepath.ToSlash(destPath), "\\", "/") return &UploadResult{ URL: webURL, Key: savePath, Size: size, MD5: md5Sum, MimeType: header.Header.Get("Content-Type"), }, nil } // GetPublicURL 获取公开访问URL func (s *LocalStorage) GetPublicURL(key string) string { return s.BaseURL + filepath.ToSlash(filepath.Join(s.BaseDir, key)) } // Delete 删除本地文件 func (s *LocalStorage) Delete(key string) error { filePath := filepath.Join(s.BaseDir, filepath.FromSlash(key)) return os.Remove(filePath) } // QiniuStorage 七牛云存储实现 type QiniuStorage struct { AccessKey string SecretKey string Bucket string Domain string Region string } // NewQiniuStorage 创建七牛云存储服务 func NewQiniuStorage(cfg *models.StorageConfig) *QiniuStorage { return &QiniuStorage{ AccessKey: cfg.QiniuAccessKey, SecretKey: cfg.QiniuSecretKey, Bucket: cfg.QiniuBucket, Domain: cfg.QiniuDomain, Region: cfg.QiniuRegion, } } // getZone 根据区域代码获取存储区域 func (s *QiniuStorage) getZone() *storage.Region { switch s.Region { case "z0": return &storage.ZoneHuadong case "z1": return &storage.ZoneHuabei case "z2": return &storage.ZoneHuanan case "na0": return &storage.ZoneBeimei case "as0": return &storage.ZoneXinjiapo case "cn-east-2": return &storage.ZoneHuadongZheJiang2 default: return &storage.ZoneHuadong // 默认华东 } } // Upload 上传文件到七牛云 func (s *QiniuStorage) Upload(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) { // 生成存储key ext := filepath.Ext(header.Filename) datePath := time.Now().Format("2006/01/02") fileName := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext) key := filepath.ToSlash(filepath.Join(datePath, fileName)) // 创建上传凭证 mac := qbox.NewMac(s.AccessKey, s.SecretKey) putPolicy := storage.PutPolicy{ Scope: s.Bucket, } upToken := putPolicy.UploadToken(mac) // 配置上传参数 cfg := storage.Config{ Region: s.getZone(), UseHTTPS: true, UseCdnDomains: false, } // 创建表单上传器 formUploader := storage.NewFormUploader(&cfg) ret := storage.PutRet{} putExtra := storage.PutExtra{} // 计算文件大小和MD5 tmpFile, err := os.CreateTemp("", "qiniu_upload_*") if err != nil { return nil, fmt.Errorf("创建临时文件失败: %w", err) } defer os.Remove(tmpFile.Name()) defer tmpFile.Close() hash := md5.New() size, err := io.Copy(io.MultiWriter(tmpFile, hash), file) if err != nil { return nil, fmt.Errorf("读取文件失败: %w", err) } md5Sum := hex.EncodeToString(hash.Sum(nil)) // 重置文件指针 if _, err := tmpFile.Seek(0, 0); err != nil { return nil, fmt.Errorf("重置文件指针失败: %w", err) } // 执行上传 err = formUploader.Put(context.Background(), &ret, upToken, key, tmpFile, size, &putExtra) if err != nil { return nil, fmt.Errorf("上传到七牛云失败: %w", err) } // 构建完整URL domain := strings.TrimRight(s.Domain, "/") url := fmt.Sprintf("%s/%s", domain, ret.Key) return &UploadResult{ URL: url, Key: ret.Key, Size: size, MD5: md5Sum, MimeType: header.Header.Get("Content-Type"), }, nil } // GetPublicURL 获取七牛云公开访问URL func (s *QiniuStorage) GetPublicURL(key string) string { domain := strings.TrimRight(s.Domain, "/") return fmt.Sprintf("%s/%s", domain, key) } // Delete 删除七牛云文件 func (s *QiniuStorage) Delete(key string) error { mac := qbox.NewMac(s.AccessKey, s.SecretKey) cfg := storage.Config{ Region: s.getZone(), UseHTTPS: true, } bucketManager := storage.NewBucketManager(mac, &cfg) err := bucketManager.Delete(s.Bucket, key) if err != nil { return fmt.Errorf("删除七牛云文件失败: %w", err) } return nil } // GetStorageService 根据配置获取存储服务 func GetStorageService() (StorageService, error) { cfg, err := models.GetStorageConfig() if err != nil { // 默认使用本地存储 return NewLocalStorage(), nil } switch cfg.StorageType { case "qiniu": if cfg.QiniuAccessKey == "" || cfg.QiniuSecretKey == "" || cfg.QiniuBucket == "" || cfg.QiniuDomain == "" { return nil, fmt.Errorf("七牛云配置不完整") } return NewQiniuStorage(cfg), nil case "local": return NewLocalStorage(), nil default: return NewLocalStorage(), nil } }