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 }