336 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			336 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package storage
 | ||
| 
 | ||
| import (
 | ||
| 	"context"
 | ||
| 	"crypto/hmac"
 | ||
| 	"crypto/sha1"
 | ||
| 	"encoding/base64"
 | ||
| 	"fmt"
 | ||
| 	"io"
 | ||
| 	"net/http"
 | ||
| 	"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)
 | ||
| }
 | ||
| 
 | ||
| // DownloadFile 从七牛云下载文件
 | ||
| func (s *QiNiuStorageService) DownloadFile(ctx context.Context, fileURL string) ([]byte, error) {
 | ||
| 	s.logger.Info("开始从七牛云下载文件", zap.String("file_url", fileURL))
 | ||
| 
 | ||
| 	// 创建HTTP客户端
 | ||
| 	client := &http.Client{
 | ||
| 		Timeout: 30 * time.Second,
 | ||
| 	}
 | ||
| 
 | ||
| 	// 创建请求
 | ||
| 	req, err := http.NewRequestWithContext(ctx, "GET", fileURL, nil)
 | ||
| 	if err != nil {
 | ||
| 		return nil, fmt.Errorf("创建请求失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 发送请求
 | ||
| 	resp, err := client.Do(req)
 | ||
| 	if err != nil {
 | ||
| 		s.logger.Error("下载文件失败",
 | ||
| 			zap.String("file_url", fileURL),
 | ||
| 			zap.Error(err),
 | ||
| 		)
 | ||
| 		return nil, fmt.Errorf("下载文件失败: %w", err)
 | ||
| 	}
 | ||
| 	defer resp.Body.Close()
 | ||
| 
 | ||
| 	// 检查响应状态
 | ||
| 	if resp.StatusCode != http.StatusOK {
 | ||
| 		s.logger.Error("下载文件失败,状态码异常",
 | ||
| 			zap.String("file_url", fileURL),
 | ||
| 			zap.Int("status_code", resp.StatusCode),
 | ||
| 		)
 | ||
| 		return nil, fmt.Errorf("下载文件失败,状态码: %d", resp.StatusCode)
 | ||
| 	}
 | ||
| 
 | ||
| 	// 读取文件内容
 | ||
| 	fileContent, err := io.ReadAll(resp.Body)
 | ||
| 	if err != nil {
 | ||
| 		s.logger.Error("读取文件内容失败",
 | ||
| 			zap.String("file_url", fileURL),
 | ||
| 			zap.Error(err),
 | ||
| 		)
 | ||
| 		return nil, fmt.Errorf("读取文件内容失败: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	s.logger.Info("文件下载成功",
 | ||
| 		zap.String("file_url", fileURL),
 | ||
| 		zap.Int("file_size", len(fileContent)),
 | ||
| 	)
 | ||
| 
 | ||
| 	return fileContent, nil
 | ||
| }
 |