Files
tyapi-server/internal/infrastructure/external/storage/qiniu_storage_service.go
2025-07-13 16:36:20 +08:00

282 lines
7.8 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 storage
import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"path/filepath"
"strings"
"time"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
"go.uber.org/zap"
sharedStorage "tyapi-server/internal/shared/storage"
)
// QiNiuStorageService 七牛云存储服务
type QiNiuStorageService struct {
accessKey string
secretKey string
bucket string
domain string
logger *zap.Logger
mac *qbox.Mac
bucketManager *storage.BucketManager
}
// QiNiuStorageConfig 七牛云存储配置
type QiNiuStorageConfig struct {
AccessKey string `yaml:"access_key"`
SecretKey string `yaml:"secret_key"`
Bucket string `yaml:"bucket"`
Domain string `yaml:"domain"`
}
// NewQiNiuStorageService 创建七牛云存储服务
func NewQiNiuStorageService(accessKey, secretKey, bucket, domain string, logger *zap.Logger) *QiNiuStorageService {
mac := qbox.NewMac(accessKey, secretKey)
// 使用默认配置不需要指定region
cfg := storage.Config{}
bucketManager := storage.NewBucketManager(mac, &cfg)
return &QiNiuStorageService{
accessKey: accessKey,
secretKey: secretKey,
bucket: bucket,
domain: domain,
logger: logger,
mac: mac,
bucketManager: bucketManager,
}
}
// UploadFile 上传文件到七牛云
func (s *QiNiuStorageService) UploadFile(ctx context.Context, fileBytes []byte, fileName string) (*sharedStorage.UploadResult, error) {
s.logger.Info("开始上传文件到七牛云",
zap.String("file_name", fileName),
zap.Int("file_size", len(fileBytes)),
)
// 生成唯一的文件key
key := s.generateFileKey(fileName)
// 创建上传凭证
putPolicy := storage.PutPolicy{
Scope: s.bucket,
}
upToken := putPolicy.UploadToken(s.mac)
// 配置上传参数
cfg := storage.Config{}
formUploader := storage.NewFormUploader(&cfg)
ret := storage.PutRet{}
// 上传文件
err := formUploader.Put(ctx, &ret, upToken, key, strings.NewReader(string(fileBytes)), int64(len(fileBytes)), &storage.PutExtra{})
if err != nil {
s.logger.Error("文件上传失败",
zap.String("file_name", fileName),
zap.String("key", key),
zap.Error(err),
)
return nil, fmt.Errorf("文件上传失败: %w", err)
}
// 构建文件URL
fileURL := s.GetFileURL(ctx, key)
s.logger.Info("文件上传成功",
zap.String("file_name", fileName),
zap.String("key", key),
zap.String("url", fileURL),
)
return &sharedStorage.UploadResult{
Key: key,
URL: fileURL,
MimeType: s.getMimeType(fileName),
Size: int64(len(fileBytes)),
Hash: ret.Hash,
}, nil
}
// GenerateUploadToken 生成上传凭证
func (s *QiNiuStorageService) GenerateUploadToken(ctx context.Context, key string) (string, error) {
putPolicy := storage.PutPolicy{
Scope: s.bucket,
// 设置过期时间1小时
Expires: uint64(time.Now().Add(time.Hour).Unix()),
}
token := putPolicy.UploadToken(s.mac)
return token, nil
}
// GetFileURL 获取文件访问URL
func (s *QiNiuStorageService) GetFileURL(ctx context.Context, key string) string {
// 如果是私有空间需要生成带签名的URL
if s.isPrivateBucket() {
deadline := time.Now().Add(time.Hour).Unix() // 1小时过期
privateAccessURL := storage.MakePrivateURL(s.mac, s.domain, key, deadline)
return privateAccessURL
}
// 公开空间直接返回URL
return fmt.Sprintf("%s/%s", s.domain, key)
}
// GetPrivateFileURL 获取私有文件访问URL
func (s *QiNiuStorageService) GetPrivateFileURL(ctx context.Context, key string, expires int64) (string, error) {
baseURL := s.GetFileURL(ctx, key)
// TODO: 实际集成七牛云SDK生成私有URL
s.logger.Info("生成七牛云私有文件URL",
zap.String("key", key),
zap.Int64("expires", expires),
)
// 模拟返回私有URL
return fmt.Sprintf("%s?token=mock_private_token&expires=%d", baseURL, expires), nil
}
// DeleteFile 删除文件
func (s *QiNiuStorageService) DeleteFile(ctx context.Context, key string) error {
s.logger.Info("删除七牛云文件", zap.String("key", key))
err := s.bucketManager.Delete(s.bucket, key)
if err != nil {
s.logger.Error("删除文件失败",
zap.String("key", key),
zap.Error(err),
)
return fmt.Errorf("删除文件失败: %w", err)
}
s.logger.Info("文件删除成功", zap.String("key", key))
return nil
}
// FileExists 检查文件是否存在
func (s *QiNiuStorageService) FileExists(ctx context.Context, key string) (bool, error) {
// TODO: 实际集成七牛云SDK检查文件存在性
s.logger.Info("检查七牛云文件存在性", zap.String("key", key))
// 模拟文件存在
return true, nil
}
// GetFileInfo 获取文件信息
func (s *QiNiuStorageService) GetFileInfo(ctx context.Context, key string) (*sharedStorage.FileInfo, error) {
fileInfo, err := s.bucketManager.Stat(s.bucket, key)
if err != nil {
s.logger.Error("获取文件信息失败",
zap.String("key", key),
zap.Error(err),
)
return nil, fmt.Errorf("获取文件信息失败: %w", err)
}
return &sharedStorage.FileInfo{
Key: key,
Size: fileInfo.Fsize,
MimeType: fileInfo.MimeType,
Hash: fileInfo.Hash,
PutTime: fileInfo.PutTime,
}, nil
}
// ListFiles 列出文件
func (s *QiNiuStorageService) ListFiles(ctx context.Context, prefix string, limit int) ([]*sharedStorage.FileInfo, error) {
entries, _, _, hasMore, err := s.bucketManager.ListFiles(s.bucket, prefix, "", "", limit)
if err != nil {
s.logger.Error("列出文件失败",
zap.String("prefix", prefix),
zap.Error(err),
)
return nil, fmt.Errorf("列出文件失败: %w", err)
}
var fileInfos []*sharedStorage.FileInfo
for _, entry := range entries {
fileInfo := &sharedStorage.FileInfo{
Key: entry.Key,
Size: entry.Fsize,
MimeType: entry.MimeType,
Hash: entry.Hash,
PutTime: entry.PutTime,
}
fileInfos = append(fileInfos, fileInfo)
}
_ = hasMore // 暂时忽略hasMore
return fileInfos, nil
}
// generateFileKey 生成文件key
func (s *QiNiuStorageService) generateFileKey(fileName string) string {
// 生成时间戳
timestamp := time.Now().Format("20060102_150405")
// 生成随机字符串
randomStr := fmt.Sprintf("%d", time.Now().UnixNano()%1000000)
// 获取文件扩展名
ext := filepath.Ext(fileName)
// 构建key: 日期/时间戳_随机数.扩展名
key := fmt.Sprintf("certification/%s/%s_%s%s",
time.Now().Format("20060102"), timestamp, randomStr, ext)
return key
}
// getMimeType 根据文件名获取MIME类型
func (s *QiNiuStorageService) getMimeType(fileName string) string {
ext := strings.ToLower(filepath.Ext(fileName))
switch ext {
case ".jpg", ".jpeg":
return "image/jpeg"
case ".png":
return "image/png"
case ".pdf":
return "application/pdf"
case ".gif":
return "image/gif"
case ".bmp":
return "image/bmp"
case ".webp":
return "image/webp"
default:
return "application/octet-stream"
}
}
// isPrivateBucket 判断是否为私有空间
func (s *QiNiuStorageService) isPrivateBucket() bool {
// 这里可以根据配置或域名特征判断
// 私有空间的域名通常包含特定标识
return strings.Contains(s.domain, "private") ||
strings.Contains(s.domain, "auth") ||
strings.Contains(s.domain, "secure")
}
// generateSignature 生成签名(用于私有空间访问)
func (s *QiNiuStorageService) generateSignature(data string) string {
h := hmac.New(sha1.New, []byte(s.secretKey))
h.Write([]byte(data))
return base64.URLEncoding.EncodeToString(h.Sum(nil))
}
// UploadFromReader 从Reader上传文件
func (s *QiNiuStorageService) UploadFromReader(ctx context.Context, reader io.Reader, fileName string, fileSize int64) (*sharedStorage.UploadResult, error) {
// 读取文件内容
fileBytes, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("读取文件失败: %w", err)
}
return s.UploadFile(ctx, fileBytes, fileName)
}