♻️ refactored thumbnail storage strategy

This commit is contained in:
2025-02-28 01:42:44 +08:00
parent 693ed8755c
commit 58c58546d2
32 changed files with 405 additions and 425 deletions

View File

@@ -11,8 +11,7 @@ import (
"net/url"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
"sync"
"time"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
@@ -27,6 +26,16 @@ type QueryShareImageLogic struct {
svcCtx *svc.ServiceContext
}
var WeekdayMap = map[time.Weekday]string{
time.Sunday: "日",
time.Monday: "一",
time.Tuesday: "二",
time.Wednesday: "三",
time.Thursday: "四",
time.Friday: "五",
time.Saturday: "六",
}
func NewQueryShareImageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryShareImageLogic {
return &QueryShareImageLogic{
Logger: logx.WithContext(ctx),
@@ -139,24 +148,11 @@ func (l *QueryShareImageLogic) queryShareImageFromSource(storageShare *model.Sca
if err != nil {
return nil, err
}
// 加载用户oss配置信息
cacheOssConfigKey := constant.UserOssConfigPrefix + storageShare.UserID + ":" + storageShare.Provider
ossConfig, err := l.getOssConfigFromCacheOrDb(cacheOssConfigKey, storageShare.UserID, storageShare.Provider)
if err != nil {
return nil, err
}
service, err := l.svcCtx.StorageManager.GetStorage(storageShare.UserID, ossConfig)
if err != nil {
return nil, errors.New("get storage failed")
}
reqParams := make(url.Values)
// 使用 errgroup 和 semaphore 并发处理图片信息
var ResultList []types.ShareImageListMeta
g, ctx := errgroup.WithContext(l.ctx)
sem := semaphore.NewWeighted(10) // 限制并发数为 10
groupedImages := sync.Map{}
for _, imgInfo := range storageInfoList {
imgInfo := imgInfo // 创建局部变量,避免闭包问题
if err := sem.Acquire(ctx, 1); err != nil {
@@ -164,25 +160,32 @@ func (l *QueryShareImageLogic) queryShareImageFromSource(storageShare *model.Sca
}
g.Go(func() error {
defer sem.Release(1)
ossURL, err := service.PresignedURL(ctx, ossConfig.BucketName, imgInfo.Path, 30*time.Minute)
// 生成日期分类键
weekday := WeekdayMap[imgInfo.CreatedAt.Weekday()]
date := imgInfo.CreatedAt.Format("2006年1月2日 星期" + weekday)
ossUrl, err := l.svcCtx.MinioClient.PresignedGetObject(ctx, constant.ShareImagesBucketName, imgInfo.Path, 30*time.Minute, reqParams)
if err != nil {
return errors.New("get presigned url failed")
}
reqParams := make(url.Values)
presignedURL, err := l.svcCtx.MinioClient.PresignedGetObject(ctx, constant.ThumbnailBucketName, imgInfo.ThumbPath, 30*time.Minute, reqParams)
if err != nil {
return errors.New("get presigned thumbnail url failed")
}
ResultList = append(ResultList, types.ShareImageListMeta{
// 原子操作更新分组数据
value, _ := groupedImages.LoadOrStore(date, []types.ImageMeta{})
images := value.([]types.ImageMeta)
images = append(images, types.ImageMeta{
ID: imgInfo.ID,
FileName: imgInfo.FileName,
Height: imgInfo.ThumbH,
Width: imgInfo.ThumbW,
ThumbSize: imgInfo.ThumbSize,
CreatedAt: imgInfo.CreatedAt.Format(constant.TimeFormat),
URL: ossURL,
URL: ossUrl.String(),
Thumbnail: presignedURL.String(),
})
groupedImages.Store(date, images)
return nil
})
}
@@ -192,8 +195,17 @@ func (l *QueryShareImageLogic) queryShareImageFromSource(storageShare *model.Sca
return nil, err
}
// 转换为切片并排序
var imageList []types.AllImageDetail
groupedImages.Range(func(key, value interface{}) bool {
imageList = append(imageList, types.AllImageDetail{
Date: key.(string),
List: value.([]types.ImageMeta),
})
return true
})
return &types.QueryShareImageResponse{
Records: ResultList}, nil
Records: imageList}, nil
}
func (l *QueryShareImageLogic) recordUserVisit(shareID int64, userID string) error {
@@ -255,63 +267,3 @@ func (l *QueryShareImageLogic) incrementVisitCount(shareCode string, limit int64
return nil
}
// 提取解密操作为函数
func (l *QueryShareImageLogic) decryptConfig(config *model.ScaStorageConfig) (*storageConfig.StorageConfig, error) {
accessKey, err := encrypt.Decrypt(config.AccessKey, l.svcCtx.Config.Encrypt.Key)
if err != nil {
return nil, errors.New("decrypt access key failed")
}
secretKey, err := encrypt.Decrypt(config.SecretKey, l.svcCtx.Config.Encrypt.Key)
if err != nil {
return nil, errors.New("decrypt secret key failed")
}
return &storageConfig.StorageConfig{
Provider: config.Provider,
Endpoint: config.Endpoint,
AccessKey: accessKey,
SecretKey: secretKey,
BucketName: config.Bucket,
Region: config.Region,
}, nil
}
// 从缓存或数据库中获取 OSS 配置
func (l *QueryShareImageLogic) getOssConfigFromCacheOrDb(cacheKey, uid, provider string) (*storageConfig.StorageConfig, error) {
result, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err != nil && !errors.Is(err, redis.Nil) {
return nil, errors.New("get oss config failed")
}
var ossConfig *storageConfig.StorageConfig
if result != "" {
var redisOssConfig model.ScaStorageConfig
if err = json.Unmarshal([]byte(result), &redisOssConfig); err != nil {
return nil, errors.New("unmarshal oss config failed")
}
return l.decryptConfig(&redisOssConfig)
}
// 缓存未命中,从数据库中加载
scaOssConfig := l.svcCtx.DB.ScaStorageConfig
dbOssConfig, err := scaOssConfig.Where(scaOssConfig.UserID.Eq(uid), scaOssConfig.Provider.Eq(provider)).First()
if err != nil {
return nil, err
}
// 缓存数据库配置
ossConfig, err = l.decryptConfig(dbOssConfig)
if err != nil {
return nil, err
}
marshalData, err := json.Marshal(dbOssConfig)
if err != nil {
return nil, errors.New("marshal oss config failed")
}
err = l.svcCtx.RedisClient.Set(l.ctx, cacheKey, marshalData, 0).Err()
if err != nil {
return nil, errors.New("set oss config failed")
}
return ossConfig, nil
}

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"github.com/ccpwcn/kgo"
"github.com/minio/minio-go/v7"
"github.com/redis/go-redis/v9"
"github.com/zeromicro/go-zero/core/logx"
"golang.org/x/sync/errgroup"
"image"
@@ -20,8 +19,6 @@ import (
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/app/auth/model/mysql/query"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
"strconv"
"time"
)
@@ -76,7 +73,7 @@ func (l *UploadShareImageLogic) UploadShareImage(req *types.ShareImageRequest) (
for _, img := range req.Images {
img := img // 确保每个协程有独立的 img 参数副本
g.Go(func() error {
return l.uploadImageAndRecord(tx, uid, album, img, req)
return l.uploadImageAndRecord(tx, uid, album, img)
})
}
@@ -101,8 +98,6 @@ func (l *UploadShareImageLogic) UploadShareImage(req *types.ShareImageRequest) (
ValidityPeriod: int64(duration),
ExpireTime: expiryTime,
ImageCount: int64(len(req.Images)),
Provider: req.Provider,
Bucket: req.Bucket,
}
err = tx.ScaStorageShare.Create(&storageShare)
if err != nil {
@@ -130,33 +125,30 @@ func (l *UploadShareImageLogic) UploadShareImage(req *types.ShareImageRequest) (
return storageShare.InviteCode, nil
}
func (l *UploadShareImageLogic) uploadImageAndRecord(tx *query.QueryTx, uid string, album model.ScaStorageAlbum, img types.ShareImageMeta, req *types.ShareImageRequest) error {
func (l *UploadShareImageLogic) uploadImageAndRecord(tx *query.QueryTx, uid string, album model.ScaStorageAlbum, img types.ShareImageMeta) error {
// 上传原始图片到用户的存储桶
// 上传原始图片到存储桶
originImage, err := base64.StdEncoding.DecodeString(img.OriginImage)
if err != nil {
return fmt.Errorf("base64 decode failed: %v", err)
}
originObjectKey := path.Join(
"share_space",
uid,
time.Now().Format("2006/01"),
fmt.Sprintf("%s_%s%s", img.FileName, kgo.SimpleUuid(), filepath.Ext(img.FileName)),
)
// 获取存储服务
ossConfig, err := l.getOssConfigFromCacheOrDb(constant.UserOssConfigPrefix+uid+":"+req.Provider, uid, req.Provider)
_, err = l.svcCtx.MinioClient.PutObject(
l.ctx,
constant.ShareImagesBucketName,
originObjectKey,
bytes.NewReader(originImage),
int64(len(originImage)),
minio.PutObjectOptions{
ContentType: "image/jpeg",
},
)
if err != nil {
return err
}
service, err := l.svcCtx.StorageManager.GetStorage(uid, ossConfig)
if err != nil {
return errors.New("get storage failed")
}
_, err = service.UploadFileSimple(l.ctx, ossConfig.BucketName, originObjectKey, bytes.NewReader(originImage), map[string]string{
"Content-Type": img.FileType,
})
if err != nil {
logx.Errorf("Failed to upload object to storage: %v", err)
logx.Errorf("Failed to upload object to MinIO: %v", err)
return err
}
@@ -169,8 +161,6 @@ func (l *UploadShareImageLogic) uploadImageAndRecord(tx *query.QueryTx, uid stri
// 记录原始图片信息
imageRecord := model.ScaStorageInfo{
UserID: uid,
Provider: req.Provider,
Bucket: req.Bucket,
Path: originObjectKey,
FileName: img.FileName,
FileSize: strconv.Itoa(size),
@@ -197,14 +187,7 @@ func (l *UploadShareImageLogic) uploadImageAndRecord(tx *query.QueryTx, uid stri
l.classifyFile(img.FileType),
fmt.Sprintf("%s_%s.jpg", time.Now().Format("20060102150405"), kgo.SimpleUuid()),
)
exists, err := l.svcCtx.MinioClient.BucketExists(l.ctx, constant.ThumbnailBucketName)
if err != nil || !exists {
err = l.svcCtx.MinioClient.MakeBucket(l.ctx, constant.ThumbnailBucketName, minio.MakeBucketOptions{Region: "us-east-1", ObjectLocking: true})
if err != nil {
logx.Errorf("Failed to create MinIO bucket: %v", err)
return err
}
}
_, err = l.svcCtx.MinioClient.PutObject(
l.ctx,
constant.ThumbnailBucketName,
@@ -257,66 +240,6 @@ func (l *UploadShareImageLogic) GetImageInfo(base64Str string) (width, height in
return imgCfg.Width, imgCfg.Height, size, nil
}
// 提取解密操作为函数
func (l *UploadShareImageLogic) decryptConfig(config *model.ScaStorageConfig) (*storageConfig.StorageConfig, error) {
accessKey, err := encrypt.Decrypt(config.AccessKey, l.svcCtx.Config.Encrypt.Key)
if err != nil {
return nil, errors.New("decrypt access key failed")
}
secretKey, err := encrypt.Decrypt(config.SecretKey, l.svcCtx.Config.Encrypt.Key)
if err != nil {
return nil, errors.New("decrypt secret key failed")
}
return &storageConfig.StorageConfig{
Provider: config.Provider,
Endpoint: config.Endpoint,
AccessKey: accessKey,
SecretKey: secretKey,
BucketName: config.Bucket,
Region: config.Region,
}, nil
}
// 从缓存或数据库中获取 OSS 配置
func (l *UploadShareImageLogic) getOssConfigFromCacheOrDb(cacheKey, uid, provider string) (*storageConfig.StorageConfig, error) {
result, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err != nil && !errors.Is(err, redis.Nil) {
return nil, errors.New("get oss config failed")
}
var ossConfig *storageConfig.StorageConfig
if result != "" {
var redisOssConfig model.ScaStorageConfig
if err = json.Unmarshal([]byte(result), &redisOssConfig); err != nil {
return nil, errors.New("unmarshal oss config failed")
}
return l.decryptConfig(&redisOssConfig)
}
// 缓存未命中,从数据库中加载
scaOssConfig := l.svcCtx.DB.ScaStorageConfig
dbOssConfig, err := scaOssConfig.Where(scaOssConfig.UserID.Eq(uid), scaOssConfig.Provider.Eq(provider)).First()
if err != nil {
return nil, err
}
// 缓存数据库配置
ossConfig, err = l.decryptConfig(dbOssConfig)
if err != nil {
return nil, err
}
marshalData, err := json.Marshal(dbOssConfig)
if err != nil {
return nil, errors.New("marshal oss config failed")
}
err = l.svcCtx.RedisClient.Set(l.ctx, cacheKey, marshalData, 0).Err()
if err != nil {
return nil, errors.New("set oss config failed")
}
return ossConfig, nil
}
// GenerateExpiryTime 函数接受当前时间和有效期(天为单位),返回过期时间
func (l *UploadShareImageLogic) GenerateExpiryTime(currentTime time.Time, durationInDays int) time.Time {
// 创建一个持续时间对象