♻️ refactored thumbnail storage strategy
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
// 创建一个持续时间对象
|
||||
|
Reference in New Issue
Block a user