基础架构

This commit is contained in:
2025-07-13 16:36:20 +08:00
parent e3d64e7485
commit 807004f78d
128 changed files with 17232 additions and 11396 deletions

View File

@@ -1,332 +0,0 @@
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"
)
// QiNiuStorageService 七牛云存储服务
type QiNiuStorageService struct {
accessKey string
secretKey string
bucket string
domain string
region 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"`
Region string `yaml:"region"`
}
// NewQiNiuStorageService 创建七牛云存储服务
func NewQiNiuStorageService(accessKey, secretKey, bucket, domain, region string, logger *zap.Logger) *QiNiuStorageService {
mac := qbox.NewMac(accessKey, secretKey)
cfg := storage.Config{
Region: &storage.Zone{
RsHost: fmt.Sprintf("rs-%s.qiniu.com", region),
},
}
bucketManager := storage.NewBucketManager(mac, &cfg)
return &QiNiuStorageService{
accessKey: accessKey,
secretKey: secretKey,
bucket: bucket,
domain: domain,
region: region,
logger: logger,
mac: mac,
bucketManager: bucketManager,
}
}
// UploadFile 上传文件到七牛云
func (s *QiNiuStorageService) UploadFile(ctx context.Context, fileBytes []byte, fileName string) (*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{
Region: &storage.Zone{
RsHost: fmt.Sprintf("rs-%s.qiniu.com", s.region),
},
}
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 &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) (*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 &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) ([]*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 []*FileInfo
for _, entry := range entries {
fileInfo := &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) (*UploadResult, error) {
s.logger.Info("从Reader上传文件到七牛云",
zap.String("file_name", fileName),
zap.Int64("file_size", fileSize),
)
// 生成唯一的文件key
key := s.generateFileKey(fileName)
// 创建上传凭证
putPolicy := storage.PutPolicy{
Scope: s.bucket,
}
upToken := putPolicy.UploadToken(s.mac)
// 配置上传参数
cfg := storage.Config{
Region: &storage.Zone{
RsHost: fmt.Sprintf("rs-%s.qiniu.com", s.region),
},
}
formUploader := storage.NewFormUploader(&cfg)
ret := storage.PutRet{}
// 上传文件
err := formUploader.Put(ctx, &ret, upToken, key, reader, fileSize, &storage.PutExtra{})
if err != nil {
s.logger.Error("从Reader上传文件失败",
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("从Reader上传文件成功",
zap.String("file_name", fileName),
zap.String("key", key),
zap.String("url", fileURL),
)
return &UploadResult{
Key: key,
URL: fileURL,
MimeType: s.getMimeType(fileName),
Size: fileSize,
Hash: ret.Hash,
}, nil
}