go-platform/services/storage_service.go
2026-04-09 16:26:38 +08:00

253 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}