added apis and optimized table structures

This commit is contained in:
2025-02-22 23:38:36 +08:00
parent db4c59f6f6
commit 737a367677
49 changed files with 1753 additions and 1377 deletions

View File

@@ -593,6 +593,11 @@ type (
SingleImageRequest {
ID int64 `json:"id"`
}
DeleteImageRequest {
IDS []int64 `json:"ids"`
Provider string `json:"provider"`
Bucket string `json:"bucket"`
}
StorageMeta {
Name string `json:"name"`
Value string `json:"value"`
@@ -605,6 +610,22 @@ type (
StorageListResponse {
Records []StroageNode `json:"records"`
}
QueryDeleteRecordRequest {
Provider string `json:"provider"`
Bucket string `json:"bucket"`
}
DeleteRecordListResponse {
Records []AllImageDetail `json:"records"`
}
BucketCapacityRequest {
Provider string `json:"provider"`
Bucket string `json:"bucket"`
}
BucketCapacityResponse {
Capacity string `json:"capacity"`
Used string `json:"used"`
Percentage float64 `json:"percentage"`
}
)
// 文件上传
@@ -695,6 +716,18 @@ service auth {
// 获取用户存储配置列表
@handler getUserStorageList
post /user/config/list returns (StorageListResponse)
// 删除图片
@handler deleteImage
post /image/delete (DeleteImageRequest) returns (string)
// 获取删除记录
@handler getDeleteRecord
post /delete/record (QueryDeleteRecordRequest) returns (DeleteRecordListResponse)
// 获取存储桶的容量信息
@handler getBucketCapacity
post /bucket/capacity (BucketCapacityRequest) returns (BucketCapacityResponse)
}
type (
@@ -717,7 +750,7 @@ type (
Images []ShareImageMeta `json:"images"`
}
QueryShareImageRequest {
ShareCode string `json:"share_code"`
InviteCode string `json:"invite_code"`
AccessPassword string `json:"access_password,omitempty"`
}
ShareImageListMeta {
@@ -725,13 +758,13 @@ type (
FileName string `json:"file_name"`
URL string `json:"url"`
Thumbnail string `json:"thumbnail"`
ThumbW float64 `json:"thumb_w"`
ThumbH float64 `json:"thumb_h"`
Width float64 `json:"width"`
Height float64 `json:"height"`
ThumbSize float64 `json:"thumb_size"`
CreatedAt string `json:"created_at"`
}
QueryShareImageResponse {
List []ShareImageListMeta `json:"list"`
Records []ShareImageListMeta `json:"records"`
}
ShareRecordListRequest {
DateRange []string `json:"date_range"`
@@ -741,7 +774,7 @@ type (
ID int64 `json:"id"`
CoverImage string `json:"cover_image"`
CreatedAt string `json:"created_at"`
ShareCode string `json:"share_code"`
InviteCode string `json:"invite_code"`
VisitLimit int64 `json:"visit_limit"`
AccessPassword string `json:"access_password"`
ValidityPeriod int64 `json:"validity_period"`
@@ -749,6 +782,31 @@ type (
ShareRecordListResponse {
records []ShareRecord `json:"records"`
}
QueryShareInfoRequest {
InviteCode string `json:"invite_code"`
}
ShareInfoResponse {
ID int64 `json:"id"`
CoverImage string `json:"cover_image"`
CreatedAt string `json:"created_at"`
VisitLimit int64 `json:"visit_limit"`
ExpireTime string `json:"expire_time"`
ImageCount int64 `json:"image_count"`
VisitCount int64 `json:"visit_count"`
ViewerCount int64 `json:"viewer_count"`
SharerAvatar string `json:"sharer_avatar"`
SharerName string `json:"sharer_name"`
AlbumName string `json:"album_name"`
}
// 分享数据概览响应参数
ShareOverviewResponse {
VisitCount int64 `json:"visit_count"`
VisitCountToday int64 `json:"visit_count_today"`
ViewerCount int64 `json:"viewer_count"`
ViewerCountToday int64 `json:"viewer_count_today"`
PublishCount int64 `json:"publish_count"`
PublishCountToday int64 `json:"publish_count_today"`
}
)
// 分享服务
@@ -774,5 +832,13 @@ service auth {
// 列出分享记录
@handler listShareRecord
post /record/list (ShareRecordListRequest) returns (ShareRecordListResponse)
// 查看分享信息
@handler queryShareInfo
post /info (QueryShareInfoRequest) returns (ShareInfoResponse)
// 查询浏览数据概览
@handler queryShareOverview
post /overview returns (ShareOverviewResponse)
}

View File

@@ -168,6 +168,16 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/image/list",
Handler: share.QueryShareImageHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/info",
Handler: share.QueryShareInfoHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/overview",
Handler: share.QueryShareOverviewHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/record/list",
@@ -241,11 +251,21 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/album/rename",
Handler: storage.RenameAlbumHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/bucket/capacity",
Handler: storage.GetBucketCapacityHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/config",
Handler: storage.SetStorageConfigHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/delete/record",
Handler: storage.GetDeleteRecordHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/face/detail/list",
@@ -271,6 +291,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/image/all/list",
Handler: storage.QueryAllImageListHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/image/delete",
Handler: storage.DeleteImageHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/image/location/detail/list",

View File

@@ -0,0 +1,29 @@
package share
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"schisandra-album-cloud-microservices/app/auth/api/internal/logic/share"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"schisandra-album-cloud-microservices/common/xhttp"
)
func QueryShareInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.QueryShareInfoRequest
if err := httpx.Parse(r, &req); err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
return
}
l := share.NewQueryShareInfoLogic(r.Context(), svcCtx)
resp, err := l.QueryShareInfo(&req)
if err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
} else {
xhttp.JsonBaseResponseCtx(r.Context(), w, resp)
}
}
}

View File

@@ -0,0 +1,21 @@
package share
import (
"net/http"
"schisandra-album-cloud-microservices/app/auth/api/internal/logic/share"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/common/xhttp"
)
func QueryShareOverviewHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := share.NewQueryShareOverviewLogic(r.Context(), svcCtx)
resp, err := l.QueryShareOverview()
if err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
} else {
xhttp.JsonBaseResponseCtx(r.Context(), w, resp)
}
}
}

View File

@@ -0,0 +1,29 @@
package storage
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"schisandra-album-cloud-microservices/app/auth/api/internal/logic/storage"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"schisandra-album-cloud-microservices/common/xhttp"
)
func DeleteImageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.DeleteImageRequest
if err := httpx.Parse(r, &req); err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
return
}
l := storage.NewDeleteImageLogic(r.Context(), svcCtx)
resp, err := l.DeleteImage(&req)
if err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
} else {
xhttp.JsonBaseResponseCtx(r.Context(), w, resp)
}
}
}

View File

@@ -0,0 +1,29 @@
package storage
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"schisandra-album-cloud-microservices/app/auth/api/internal/logic/storage"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"schisandra-album-cloud-microservices/common/xhttp"
)
func GetBucketCapacityHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.BucketCapacityRequest
if err := httpx.Parse(r, &req); err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
return
}
l := storage.NewGetBucketCapacityLogic(r.Context(), svcCtx)
resp, err := l.GetBucketCapacity(&req)
if err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
} else {
xhttp.JsonBaseResponseCtx(r.Context(), w, resp)
}
}
}

View File

@@ -0,0 +1,29 @@
package storage
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"schisandra-album-cloud-microservices/app/auth/api/internal/logic/storage"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"schisandra-album-cloud-microservices/common/xhttp"
)
func GetDeleteRecordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.QueryDeleteRecordRequest
if err := httpx.Parse(r, &req); err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
return
}
l := storage.NewGetDeleteRecordLogic(r.Context(), svcCtx)
resp, err := l.GetDeleteRecord(&req)
if err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
} else {
xhttp.JsonBaseResponseCtx(r.Context(), w, resp)
}
}
}

View File

@@ -35,7 +35,7 @@ func (l *ListShareRecordLogic) ListShareRecord(req *types.ShareRecordListRequest
var recordList []types.ShareRecord
query := storageShare.
Select(storageShare.ID,
storageShare.ShareCode,
storageShare.InviteCode,
storageShare.VisitLimit,
storageShare.AccessPassword,
storageShare.ValidityPeriod,

View File

@@ -41,7 +41,7 @@ func (l *QueryShareImageLogic) QueryShareImage(req *types.QueryShareImageRequest
return nil, errors.New("user_id not found")
}
// 获取分享记录
cacheKey := constant.ImageSharePrefix + req.ShareCode
cacheKey := constant.ImageSharePrefix + req.InviteCode
shareData, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
@@ -66,7 +66,7 @@ func (l *QueryShareImageLogic) QueryShareImage(req *types.QueryShareImageRequest
// 检查访问限制
if storageShare.VisitLimit > 0 {
err = l.incrementVisitCount(req.ShareCode, storageShare.VisitLimit)
err = l.incrementVisitCount(req.InviteCode, storageShare.VisitLimit)
if err != nil {
return nil, err
}
@@ -79,7 +79,7 @@ func (l *QueryShareImageLogic) QueryShareImage(req *types.QueryShareImageRequest
}
// 生成缓存键(在验证通过后)
resultCacheKey := constant.ImageListPrefix + req.ShareCode + ":" + req.AccessPassword
resultCacheKey := constant.ImageCachePrefix + storageShare.UserID + ":share:" + req.InviteCode
// 尝试从缓存中获取结果
cachedResult, err := l.svcCtx.RedisClient.Get(l.ctx, resultCacheKey).Result()
@@ -131,7 +131,7 @@ func (l *QueryShareImageLogic) queryShareImageFromSource(storageShare *model.Sca
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).
LeftJoin(storageThumb, storageInfo.ID.EqCol(storageThumb.InfoID)).
Where(
storageInfo.Type.Eq(constant.ImageTypeShared),
storageInfo.AlbumID.Eq(storageShare.AlbumID)).
@@ -175,8 +175,8 @@ func (l *QueryShareImageLogic) queryShareImageFromSource(storageShare *model.Sca
ResultList = append(ResultList, types.ShareImageListMeta{
ID: imgInfo.ID,
FileName: imgInfo.FileName,
ThumbH: imgInfo.ThumbH,
ThumbW: imgInfo.ThumbW,
Height: imgInfo.ThumbH,
Width: imgInfo.ThumbW,
ThumbSize: imgInfo.ThumbSize,
CreatedAt: imgInfo.CreatedAt.Format(constant.TimeFormat),
URL: ossURL,
@@ -192,7 +192,7 @@ func (l *QueryShareImageLogic) queryShareImageFromSource(storageShare *model.Sca
}
return &types.QueryShareImageResponse{
List: ResultList}, nil
Records: ResultList}, nil
}
func (l *QueryShareImageLogic) recordUserVisit(shareID int64, userID string) error {

View File

@@ -0,0 +1,75 @@
package share
import (
"context"
"errors"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type QueryShareInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewQueryShareInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryShareInfoLogic {
return &QueryShareInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryShareInfoLogic) QueryShareInfo(req *types.QueryShareInfoRequest) (resp *types.ShareInfoResponse, err error) {
uid, ok := l.ctx.Value("user_id").(string)
if !ok {
return nil, errors.New("user_id not found")
}
storageShare := l.svcCtx.DB.ScaStorageShare
storageAlbum := l.svcCtx.DB.ScaStorageAlbum
shareVisit := l.svcCtx.DB.ScaStorageShareVisit
authUser := l.svcCtx.DB.ScaAuthUser
var shareInfo types.ShareInfoResponse
err = storageShare.Select(
storageShare.ID,
storageShare.VisitLimit,
storageShare.InviteCode,
storageShare.ExpireTime,
storageShare.CreatedAt,
storageAlbum.CoverImage,
storageAlbum.AlbumName,
storageShare.ImageCount,
shareVisit.Views.As("visit_count"),
shareVisit.UserID.Count().As("viewer_count"),
authUser.Avatar.As("sharer_avatar"),
authUser.Nickname.As("sharer_name")).
LeftJoin(storageAlbum, storageShare.AlbumID.EqCol(storageAlbum.ID)).
Join(shareVisit, storageShare.ID.EqCol(shareVisit.ShareID)).
LeftJoin(authUser, storageShare.UserID.EqCol(authUser.UID)).
Where(
storageShare.InviteCode.Eq(req.InviteCode),
shareVisit.UserID.Eq(uid)).
Group(
storageShare.ID,
storageShare.VisitLimit,
storageShare.InviteCode,
storageShare.ExpireTime,
storageShare.CreatedAt,
storageAlbum.CoverImage,
storageShare.ImageCount,
storageAlbum.AlbumName,
shareVisit.Views,
authUser.Avatar,
authUser.Nickname).
Scan(&shareInfo)
if err != nil {
return nil, err
}
return &shareInfo, nil
}

View File

@@ -0,0 +1,80 @@
package share
import (
"context"
"errors"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"time"
"github.com/zeromicro/go-zero/core/logx"
)
type QueryShareOverviewLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewQueryShareOverviewLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryShareOverviewLogic {
return &QueryShareOverviewLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryShareOverviewLogic) QueryShareOverview() (resp *types.ShareOverviewResponse, err error) {
uid, ok := l.ctx.Value("user_id").(string)
if !ok {
return nil, errors.New("user_id not found")
}
storageShare := l.svcCtx.DB.ScaStorageShare
shareVisit := l.svcCtx.DB.ScaStorageShareVisit
// 统计所有数据
var totalResult struct {
TotalCount int64
TotalViews int64
TotalUsers int64
}
err = storageShare.Select(
storageShare.ID.Count().As("total_count"),
shareVisit.Views.Sum().As("total_views"),
shareVisit.UserID.Distinct().Count().As("total_users"),
).
Join(shareVisit, storageShare.ID.EqCol(shareVisit.ShareID)).
Where(storageShare.UserID.Eq(uid)).
Scan(&totalResult)
if err != nil {
return nil, err
}
// 统计当天数据
var dailyResult struct {
DailyCount int64
DailyViews int64
DailyUsers int64
}
err = storageShare.Select(
storageShare.ID.Count().As("daily_count"),
shareVisit.Views.Sum().As("daily_views"),
shareVisit.UserID.Distinct().Count().As("daily_users"),
).
Join(shareVisit, storageShare.ID.EqCol(shareVisit.ShareID)).
Where(storageShare.UserID.Eq(uid),
shareVisit.CreatedAt.Gte(time.Now().Truncate(24*time.Hour))).
Scan(&dailyResult)
if err != nil {
return nil, err
}
// 合并结果到 ShareOverviewResponse
response := types.ShareOverviewResponse{
VisitCount: totalResult.TotalViews, // 总访问量
VisitCountToday: dailyResult.DailyViews, // 当天访问量
ViewerCount: totalResult.TotalUsers, // 总独立用户数
ViewerCountToday: dailyResult.DailyUsers, // 当天独立用户数
PublishCount: totalResult.TotalCount, // 总发布量
PublishCountToday: dailyResult.DailyCount, // 当天发布量
}
return &response, nil
}

View File

@@ -94,12 +94,13 @@ func (l *UploadShareImageLogic) UploadShareImage(req *types.ShareImageRequest) (
storageShare := model.ScaStorageShare{
UserID: uid,
AlbumID: album.ID,
ShareCode: kgo.SimpleUuid(),
InviteCode: kgo.SimpleUuid(),
Status: 0,
AccessPassword: req.AccessPassword,
VisitLimit: req.AccessLimit,
ValidityPeriod: int64(duration),
ExpireTime: expiryTime,
ImageCount: int64(len(req.Images)),
}
err = tx.ScaStorageShare.Create(&storageShare)
if err != nil {
@@ -112,7 +113,7 @@ func (l *UploadShareImageLogic) UploadShareImage(req *types.ShareImageRequest) (
tx.Rollback()
return "", err
}
cacheKey := constant.ImageSharePrefix + storageShare.ShareCode
cacheKey := constant.ImageSharePrefix + storageShare.InviteCode
err = l.svcCtx.RedisClient.Set(l.ctx, cacheKey, marshal, time.Duration(duration)*time.Hour*24).Err()
if err != nil {
tx.Rollback()
@@ -124,56 +125,10 @@ func (l *UploadShareImageLogic) UploadShareImage(req *types.ShareImageRequest) (
logx.Errorf("Transaction commit failed: %v", err)
return "", err
}
return storageShare.ShareCode, nil
return storageShare.InviteCode, nil
}
func (l *UploadShareImageLogic) uploadImageAndRecord(tx *query.QueryTx, uid string, album model.ScaStorageAlbum, img types.ShareImageMeta, req *types.ShareImageRequest) error {
// 上传缩略图到 Minio
thumbnail, err := base64.StdEncoding.DecodeString(img.Thumbnail)
if err != nil {
return fmt.Errorf("base64 decode failed: %v", err)
}
thumbObjectKey := path.Join(
uid,
time.Now().Format("2006/01"),
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,
thumbObjectKey,
bytes.NewReader(thumbnail),
int64(len(thumbnail)),
minio.PutObjectOptions{
ContentType: "image/jpeg",
},
)
if err != nil {
logx.Errorf("Failed to upload MinIO object: %v", err)
return err
}
// 记录缩略图
thumbRecord := model.ScaStorageThumb{
UserID: uid,
ThumbPath: thumbObjectKey,
ThumbW: img.ThumbW,
ThumbH: img.ThumbH,
ThumbSize: float64(len(thumbnail)),
}
err = tx.ScaStorageThumb.Create(&thumbRecord)
if err != nil {
return err
}
// 上传原始图片到用户的存储桶
originImage, err := base64.StdEncoding.DecodeString(img.OriginImage)
@@ -211,24 +166,72 @@ 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),
FileType: img.FileType,
Width: float64(width),
Height: float64(height),
Type: constant.ImageTypeShared,
AlbumID: album.ID,
ThumbID: thumbRecord.ID,
UserID: uid,
Provider: req.Provider,
Bucket: req.Bucket,
Path: originObjectKey,
FileName: img.FileName,
FileSize: strconv.Itoa(size),
FileType: img.FileType,
Width: float64(width),
Height: float64(height),
Type: constant.ImageTypeShared,
AlbumID: album.ID,
IsDisplayed: 1,
}
err = tx.ScaStorageInfo.Create(&imageRecord)
if err != nil {
return err
}
// 上传缩略图到 Minio
thumbnail, err := base64.StdEncoding.DecodeString(img.Thumbnail)
if err != nil {
return fmt.Errorf("base64 decode failed: %v", err)
}
thumbObjectKey := path.Join(
uid,
time.Now().Format("2006/01"),
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,
thumbObjectKey,
bytes.NewReader(thumbnail),
int64(len(thumbnail)),
minio.PutObjectOptions{
ContentType: "image/jpeg",
},
)
if err != nil {
logx.Errorf("Failed to upload MinIO object: %v", err)
return err
}
// 记录缩略图
thumbRecord := model.ScaStorageThumb{
InfoID: imageRecord.ID,
UserID: uid,
ThumbPath: thumbObjectKey,
ThumbW: img.ThumbW,
ThumbH: img.ThumbH,
ThumbSize: float64(len(thumbnail)),
}
err = tx.ScaStorageThumb.Create(&thumbRecord)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,97 @@
package storage
import (
"context"
"errors"
"fmt"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type DeleteImageLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewDeleteImageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteImageLogic {
return &DeleteImageLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeleteImageLogic) DeleteImage(req *types.DeleteImageRequest) (resp string, err error) {
uid, ok := l.ctx.Value("user_id").(string)
if !ok {
return "", errors.New("user_id not found")
}
tx := l.svcCtx.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
storageInfo := tx.ScaStorageInfo
info, err := storageInfo.Where(storageInfo.UserID.Eq(uid),
storageInfo.ID.In(req.IDS...),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket)).Delete()
if err != nil {
tx.Rollback()
return "", err
}
if info.RowsAffected == 0 {
tx.Rollback()
return "", errors.New("no image found")
}
storageThumb := tx.ScaStorageThumb
resultInfo, err := storageThumb.Where(storageThumb.UserID.Eq(uid), storageThumb.InfoID.In(req.IDS...)).Delete()
if err != nil {
tx.Rollback()
return "", err
}
if resultInfo.RowsAffected == 0 {
tx.Rollback()
return "", errors.New("no thumb found")
}
storageExtra := tx.ScaStorageExtra
resultExtra, err := storageExtra.Where(storageExtra.UserID.Eq(uid), storageExtra.InfoID.In(req.IDS...)).Delete()
if err != nil {
tx.Rollback()
return "", err
}
if resultExtra.RowsAffected == 0 {
tx.Rollback()
return "", errors.New("no extra found")
}
err = tx.Commit()
if err != nil {
tx.Rollback()
return "", err
}
// 删除缓存
keyPattern := fmt.Sprintf("%s%s:%s", constant.ImageCachePrefix, uid, "*")
// 获取所有匹配的键
keys, err := l.svcCtx.RedisClient.Keys(l.ctx, keyPattern).Result()
if err != nil {
logx.Errorf("获取缓存键 %s 失败: %v", keyPattern, err)
return "", err
}
// 如果没有匹配的键,直接返回
if len(keys) == 0 {
logx.Infof("没有找到匹配的缓存键: %s", keyPattern)
return "", nil
}
// 删除所有匹配的键
if err := l.svcCtx.RedisClient.Del(l.ctx, keys...).Err(); err != nil {
logx.Errorf("删除缓存键 %s 失败: %v", keyPattern, err)
return "", err
}
return "success", nil
}

View File

@@ -13,6 +13,7 @@ import (
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
"sort"
"sync"
"time"
@@ -42,7 +43,7 @@ func (l *GetAlbumDetailLogic) GetAlbumDetail(req *types.AlbumDetailListRequest)
return nil, errors.New("user_id not found")
}
// 缓存获取数据 v1.0.0
cacheKey := fmt.Sprintf("%s%s:%s:%s:%v", constant.ImageListPrefix, uid, req.Provider, req.Bucket, req.ID)
cacheKey := fmt.Sprintf("%s%s:%s:%s:%s:%v", constant.ImageCachePrefix, uid, "album", req.Provider, req.Bucket, req.ID)
// 尝试从缓存获取
cachedResult, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err == nil {
@@ -72,7 +73,7 @@ func (l *GetAlbumDetailLogic) GetAlbumDetail(req *types.AlbumDetailListRequest)
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).
LeftJoin(storageThumb, storageInfo.ID.EqCol(storageThumb.InfoID)).
Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
@@ -147,13 +148,19 @@ func (l *GetAlbumDetailLogic) GetAlbumDetail(req *types.AlbumDetailListRequest)
})
return true
})
// 按日期排序,最新的在最上面
sort.Slice(imageList, func(i, j int) bool {
dateI, _ := time.Parse("2006年1月2日 星期一", imageList[i].Date)
dateJ, _ := time.Parse("2006年1月2日 星期一", imageList[j].Date)
return dateI.After(dateJ)
})
resp = &types.AlbumDetailListResponse{
Records: imageList,
}
// 缓存结果
if data, err := json.Marshal(resp); err == nil {
expireTime := 7*24*time.Hour - time.Duration(rand.Intn(60))*time.Minute
expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second
if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil {
logx.Error("Failed to cache image list:", err)
}

View File

@@ -0,0 +1,211 @@
package storage
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/redis/go-redis/v9"
"math"
"math/rand"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"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"
"time"
"github.com/zeromicro/go-zero/core/logx"
)
type GetBucketCapacityLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetBucketCapacityLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetBucketCapacityLogic {
return &GetBucketCapacityLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetBucketCapacityLogic) GetBucketCapacity(req *types.BucketCapacityRequest) (resp *types.BucketCapacityResponse, err error) {
uid, ok := l.ctx.Value("user_id").(string)
if !ok {
return nil, errors.New("user_id not found")
}
// 设计缓存键
cacheKey := fmt.Sprintf("%s%s:%s:%s", constant.BucketCapacityCachePrefix, uid, req.Provider, req.Bucket)
// 尝试从缓存中获取容量信息
cachedResult, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err != nil && !errors.Is(err, redis.Nil) {
logx.Errorf("get bucket capacity from cache failed: %v", err)
return nil, err
}
// 如果缓存存在,直接返回缓存结果
if cachedResult != "" {
// 如果是空值缓存(防缓存穿透),返回空结果
if cachedResult == "{}" {
return &types.BucketCapacityResponse{}, nil
}
err = json.Unmarshal([]byte(cachedResult), &resp)
if err != nil {
return nil, errors.New("unmarshal cached result failed")
}
return resp, nil
}
// 加载用户oss配置信息
cacheOssConfigKey := constant.UserOssConfigPrefix + uid + ":" + req.Provider
ossConfig, err := l.getOssConfigFromCacheOrDb(cacheOssConfigKey, uid, req.Provider)
if err != nil {
return nil, err
}
service, err := l.svcCtx.StorageManager.GetStorage(uid, ossConfig)
if err != nil {
return nil, errors.New("get storage failed")
}
bucketStat, err := service.GetBucketStat(l.ctx, ossConfig.BucketName)
if err != nil {
// 如果 OSS 接口调用失败,设置空值缓存(防缓存穿透)
emptyData := "{}"
emptyCacheExpire := 5 * time.Minute // 空值缓存过期时间
if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, emptyData, emptyCacheExpire).Err(); err != nil {
logx.Errorf("set empty cache failed: %v", err)
}
return nil, errors.New("get bucket stat failed")
}
scaStorageConfig := l.svcCtx.DB.ScaStorageConfig
capacity, err := scaStorageConfig.Select(scaStorageConfig.Capacity).
Where(scaStorageConfig.UserID.Eq(uid), scaStorageConfig.Provider.Eq(req.Provider), scaStorageConfig.Bucket.Eq(req.Bucket)).First()
if err != nil {
return nil, errors.New("get storage config failed")
}
// 总容量单位GB
totalCapacityGB := capacity.Capacity
// 已用容量(单位:字节转换为 GB
const bytesToGB = 1024 * 1024 * 1024
usedCapacityGB := float64(bucketStat.StandardStorage) / bytesToGB
// 计算百分比
percentage := calculatePercentage(usedCapacityGB, float64(totalCapacityGB))
// 格式化容量信息
capacityStr := fmt.Sprintf("%.2v GB", totalCapacityGB) // 总容量GB
resp = &types.BucketCapacityResponse{
Capacity: capacityStr,
Used: formatBytes(bucketStat.StandardStorage),
Percentage: percentage,
}
// 缓存容量信息
marshalData, err := json.Marshal(resp)
if err != nil {
return nil, errors.New("marshal bucket capacity failed")
}
// 添加随机值(防缓存雪崩)
// 计算缓存过期时间:距离第二天凌晨 12 点的剩余时间
cacheExpire := timeUntilNextMidnight() + time.Duration(rand.Intn(300))*time.Second
err = l.svcCtx.RedisClient.Set(l.ctx, cacheKey, marshalData, cacheExpire).Err()
if err != nil {
return nil, errors.New("set bucket capacity failed")
}
return resp, nil
}
// 提取解密操作为函数
func (l *GetBucketCapacityLogic) 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 *GetBucketCapacityLogic) 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
}
// 格式化字节大小为更友好的单位KB、MB、GB 等)
func formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.2f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
// 计算使用量百分比(基于 GB
func calculatePercentage(usedGB, totalGB float64) float64 {
if totalGB == 0 {
return 0
}
return math.Round(usedGB/totalGB*100*100) / 100
}
// 计算距离第二天凌晨 12 点的剩余时间
func timeUntilNextMidnight() time.Duration {
now := time.Now()
nextMidnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
return nextMidnight.Sub(now)
}

View File

@@ -0,0 +1,231 @@
package storage
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/redis/go-redis/v9"
"math/rand"
"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"
"sort"
"sync"
"time"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetDeleteRecordLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetDeleteRecordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetDeleteRecordLogic {
return &GetDeleteRecordLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetDeleteRecordLogic) GetDeleteRecord(req *types.QueryDeleteRecordRequest) (resp *types.DeleteRecordListResponse, err error) {
uid, ok := l.ctx.Value("user_id").(string)
if !ok {
return nil, errors.New("user_id not found")
}
// 缓存获取数据 v1.0.0
cacheKey := fmt.Sprintf("%s%s:%s:%s:%s", constant.ImageCachePrefix, uid, "deleted", req.Provider, req.Bucket)
// 尝试从缓存获取
cachedResult, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err == nil {
var cachedResponse types.DeleteRecordListResponse
if err := json.Unmarshal([]byte(cachedResult), &cachedResponse); err == nil {
return &cachedResponse, nil
}
logx.Error("Failed to unmarshal cached image list:", err)
return nil, errors.New("get cached image list failed")
} else if !errors.Is(err, redis.Nil) {
logx.Error("Redis error:", err)
return nil, errors.New("get cached image list failed")
}
// 缓存未命中,从数据库中查询
storageInfo := l.svcCtx.DB.ScaStorageInfo
storageThumb := l.svcCtx.DB.ScaStorageThumb
var storageInfoList []types.FileInfoResult
err = storageInfo.Select(
storageInfo.ID,
storageInfo.FileName,
storageInfo.CreatedAt,
storageThumb.ThumbPath,
storageInfo.Path,
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ID.EqCol(storageThumb.InfoID)).
Unscoped().
Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.DeletedAt.IsNotNull(),
storageInfo.Type.Neq(constant.ImageTypeShared)).
Order(storageInfo.CreatedAt.Desc()).
Scan(&storageInfoList)
if err != nil {
return nil, err
}
if len(storageInfoList) == 0 {
return &types.DeleteRecordListResponse{}, nil
}
// 加载用户oss配置信息
cacheOssConfigKey := constant.UserOssConfigPrefix + uid + ":" + req.Provider
ossConfig, err := l.getOssConfigFromCacheOrDb(cacheOssConfigKey, uid, req.Provider)
if err != nil {
return nil, err
}
service, err := l.svcCtx.StorageManager.GetStorage(uid, ossConfig)
if err != nil {
return nil, errors.New("get storage failed")
}
// 按日期进行分组
var wg sync.WaitGroup
groupedImages := sync.Map{}
for _, dbFileInfo := range storageInfoList {
wg.Add(1)
go func(dbFileInfo *types.FileInfoResult) {
defer wg.Done()
weekday := WeekdayMap[dbFileInfo.CreatedAt.Weekday()]
date := dbFileInfo.CreatedAt.Format("2006年1月2日 星期" + weekday)
reqParams := make(url.Values)
presignedUrl, err := l.svcCtx.MinioClient.PresignedGetObject(l.ctx, constant.ThumbnailBucketName, dbFileInfo.ThumbPath, time.Hour*24*7, reqParams)
if err != nil {
logx.Error(err)
return
}
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, dbFileInfo.Path, time.Hour*24*7)
if err != nil {
logx.Error(err)
return
}
// 使用 Load 或 Store 确保原子操作
value, _ := groupedImages.LoadOrStore(date, []types.ImageMeta{})
images := value.([]types.ImageMeta)
images = append(images, types.ImageMeta{
ID: dbFileInfo.ID,
FileName: dbFileInfo.FileName,
Thumbnail: presignedUrl.String(),
URL: url,
Width: dbFileInfo.ThumbW,
Height: dbFileInfo.ThumbH,
CreatedAt: dbFileInfo.CreatedAt.Format("2006-01-02 15:04:05"),
})
// 重新存储更新后的图像列表
groupedImages.Store(date, images)
}(&dbFileInfo)
}
wg.Wait()
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
})
// 按日期排序,最新的在最上面
sort.Slice(imageList, func(i, j int) bool {
dateI, _ := time.Parse("2006年1月2日 星期一", imageList[i].Date)
dateJ, _ := time.Parse("2006年1月2日 星期一", imageList[j].Date)
return dateI.After(dateJ)
})
resp = &types.DeleteRecordListResponse{
Records: imageList,
}
// 缓存结果
if data, err := json.Marshal(resp); err == nil {
expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second
if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil {
logx.Error("Failed to cache image list:", err)
}
} else {
logx.Error("Failed to marshal image list for caching:", err)
}
return resp, nil
}
// 提取解密操作为函数
func (l *GetDeleteRecordLogic) 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 *GetDeleteRecordLogic) 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

@@ -12,6 +12,7 @@ import (
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
"sort"
"sync"
"time"
@@ -41,7 +42,7 @@ func (l *GetFaceDetailListLogic) GetFaceDetailList(req *types.FaceDetailListRequ
return nil, errors.New("user_id not found")
}
// 缓存获取数据 v1.0.0
cacheKey := fmt.Sprintf("%s%s:%s:%s:%v", constant.ImageFaceListPrefix, uid, req.Provider, req.Bucket, req.FaceID)
cacheKey := fmt.Sprintf("%s%s:%s:%s:%s:%v", constant.ImageCachePrefix, uid, "faces", req.Provider, req.Bucket, req.FaceID)
// 尝试从缓存获取
cachedResult, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err == nil {
@@ -70,7 +71,7 @@ func (l *GetFaceDetailListLogic) GetFaceDetailList(req *types.FaceDetailListRequ
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).Where(
LeftJoin(storageThumb, storageInfo.ID.EqCol(storageThumb.InfoID)).Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
@@ -144,13 +145,19 @@ func (l *GetFaceDetailListLogic) GetFaceDetailList(req *types.FaceDetailListRequ
})
return true
})
// 按日期排序,最新的在最上面
sort.Slice(imageList, func(i, j int) bool {
dateI, _ := time.Parse("2006年1月2日 星期一", imageList[i].Date)
dateJ, _ := time.Parse("2006年1月2日 星期一", imageList[j].Date)
return dateI.After(dateJ)
})
resp = &types.FaceDetailListResponse{
Records: imageList,
}
// 缓存结果
if data, err := json.Marshal(resp); err == nil {
expireTime := 7*24*time.Hour - time.Duration(rand.Intn(60))*time.Minute
expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second
if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil {
logx.Error("Failed to cache image list:", err)
}

View File

@@ -75,12 +75,12 @@ func (l *GetImageUrlLogic) GetImageUrl(req *types.SingleImageRequest) (resp stri
if err != nil {
return "", errors.New("get storage failed")
}
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, result.Path, 7*24*time.Hour)
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, result.Path, 15*time.Minute)
if err != nil {
return "", errors.New("get presigned url failed")
}
// 缓存url
err = l.svcCtx.RedisClient.Set(l.ctx, cacheKey, url, 7*24*time.Hour).Err()
err = l.svcCtx.RedisClient.Set(l.ctx, cacheKey, url, 15*time.Minute).Err()
if err != nil {
logx.Info(err)
}

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/redis/go-redis/v9"
"github.com/zeromicro/go-zero/core/logx"
"gorm.io/gen"
"math/rand"
"net/url"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
@@ -15,6 +16,7 @@ import (
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
"sort"
"sync"
"time"
)
@@ -49,7 +51,7 @@ func (l *QueryAllImageListLogic) QueryAllImageList(req *types.AllImageListReques
return nil, errors.New("user_id not found")
}
// 缓存获取数据 v1.0.0
cacheKey := fmt.Sprintf("%s%s:%s:%s:%t", constant.ImageListPrefix, uid, req.Provider, req.Bucket, req.Sort)
cacheKey := fmt.Sprintf("%s%s:%s:%s:%s:%s:%t", constant.ImageCachePrefix, uid, "list", req.Provider, req.Bucket, req.Type, req.Sort)
// 尝试从缓存获取
cachedResult, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err == nil {
@@ -63,47 +65,36 @@ func (l *QueryAllImageListLogic) QueryAllImageList(req *types.AllImageListReques
logx.Error("Redis error:", err)
return nil, errors.New("get cached image list failed")
}
// 缓存未命中,从数据库中查询
storageInfo := l.svcCtx.DB.ScaStorageInfo
storageThumb := l.svcCtx.DB.ScaStorageThumb
var storageInfoList []types.FileInfoResult
if req.Sort {
err = storageInfo.Select(
storageInfo.ID,
storageInfo.FileName,
storageInfo.CreatedAt,
storageInfo.Path,
storageThumb.ThumbPath,
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).
Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.Type.Eq(req.Type),
storageInfo.AlbumID.IsNull()).
Order(storageInfo.CreatedAt.Desc()).Scan(&storageInfoList)
} else {
err = storageInfo.Select(
storageInfo.ID,
storageInfo.FileName,
storageInfo.CreatedAt,
storageThumb.ThumbPath,
storageInfo.Path,
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).
Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.Type.Eq(req.Type)).
Order(storageInfo.CreatedAt.Desc()).Scan(&storageInfoList)
var queryCondition []gen.Condition
conditions := []gen.Condition{
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.Type.Neq(constant.ImageTypeShared),
}
queryCondition = append(queryCondition, conditions...)
if req.Type != "all" {
queryCondition = append(queryCondition, storageInfo.Type.Eq(req.Type))
}
if req.Sort {
queryCondition = append(queryCondition, storageInfo.AlbumID.Eq(0))
}
var storageInfoList []types.FileInfoResult
err = storageInfo.Select(
storageInfo.ID,
storageInfo.FileName,
storageInfo.CreatedAt,
storageThumb.ThumbPath,
storageInfo.Path,
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ID.EqCol(storageThumb.InfoID)).
Where(queryCondition...).
Order(storageInfo.CreatedAt.Desc()).Scan(&storageInfoList)
if err != nil {
return nil, err
}
@@ -171,13 +162,19 @@ func (l *QueryAllImageListLogic) QueryAllImageList(req *types.AllImageListReques
})
return true
})
// 按日期排序,最新的在最上面
sort.Slice(imageList, func(i, j int) bool {
dateI, _ := time.Parse("2006年1月2日 星期一", imageList[i].Date)
dateJ, _ := time.Parse("2006年1月2日 星期一", imageList[j].Date)
return dateI.After(dateJ)
})
resp = &types.AllImageListResponse{
Records: imageList,
}
// 缓存结果
if data, err := json.Marshal(resp); err == nil {
expireTime := 7*24*time.Hour - time.Duration(rand.Intn(60))*time.Minute
expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second
if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil {
logx.Error("Failed to cache image list:", err)
}

View File

@@ -13,6 +13,7 @@ import (
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
"sort"
"sync"
"time"
@@ -42,7 +43,7 @@ func (l *QueryLocationDetailListLogic) QueryLocationDetailList(req *types.Locati
return nil, errors.New("user_id not found")
}
// 缓存获取数据 v1.0.0
cacheKey := fmt.Sprintf("%s%s:%s:%s:%v", constant.ImageListPrefix, uid, req.Provider, req.Bucket, req.ID)
cacheKey := fmt.Sprintf("%s%s:%s:%s:%s:%v", constant.ImageCachePrefix, uid, "location", req.Provider, req.Bucket, req.ID)
// 尝试从缓存获取
cachedResult, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err == nil {
@@ -59,6 +60,7 @@ func (l *QueryLocationDetailListLogic) QueryLocationDetailList(req *types.Locati
storageInfo := l.svcCtx.DB.ScaStorageInfo
storageThumb := l.svcCtx.DB.ScaStorageThumb
storageLocation := l.svcCtx.DB.ScaStorageLocation
// 数据库查询文件信息列表
var storageInfoQuery query.IScaStorageInfoDo
var storageInfoList []types.FileInfoResult
@@ -72,11 +74,13 @@ func (l *QueryLocationDetailListLogic) QueryLocationDetailList(req *types.Locati
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.LocationID.Eq(req.ID)).
LeftJoin(storageThumb, storageInfo.ID.EqCol(storageThumb.InfoID)).
LeftJoin(storageLocation, storageInfo.LocationID.EqCol(storageLocation.ID)).
Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageLocation.ID.Eq(req.ID)).
Order(storageInfo.CreatedAt.Desc())
err = storageInfoQuery.Scan(&storageInfoList)
if err != nil {
@@ -146,13 +150,19 @@ func (l *QueryLocationDetailListLogic) QueryLocationDetailList(req *types.Locati
})
return true
})
// 按日期排序,最新的在最上面
sort.Slice(imageList, func(i, j int) bool {
dateI, _ := time.Parse("2006年1月2日 星期一", imageList[i].Date)
dateJ, _ := time.Parse("2006年1月2日 星期一", imageList[j].Date)
return dateI.After(dateJ)
})
resp = &types.LocationDetailListResponse{
Records: imageList,
}
// 缓存结果
if data, err := json.Marshal(resp); err == nil {
expireTime := 7*24*time.Hour - time.Duration(rand.Intn(60))*time.Minute
expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second
if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil {
logx.Error("Failed to cache image list:", err)
}

View File

@@ -2,17 +2,12 @@ package storage
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/redis/go-redis/v9"
"net/url"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"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"
"time"
"github.com/zeromicro/go-zero/core/logx"
@@ -38,32 +33,27 @@ func (l *QueryLocationImageListLogic) QueryLocationImageList(req *types.Location
return nil, errors.New("user_id not found")
}
storageLocation := l.svcCtx.DB.ScaStorageLocation
storageInfo := l.svcCtx.DB.ScaStorageInfo
locations, err := storageLocation.Select(
var locations []types.LocationInfo
err = storageLocation.Select(
storageLocation.ID,
storageLocation.Country,
storageLocation.City,
storageLocation.Province,
storageLocation.CoverImage,
storageLocation.Total).Where(storageLocation.UserID.Eq(uid),
storageLocation.Provider.Eq(req.Provider),
storageLocation.Bucket.Eq(req.Bucket)).
Order(storageLocation.CreatedAt.Desc()).Find()
storageInfo.ID.Count().As("total")).
LeftJoin(storageInfo, storageInfo.LocationID.EqCol(storageLocation.ID)).
Where(storageLocation.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket)).
Order(storageLocation.CreatedAt.Desc()).
Group(storageLocation.ID).
Scan(&locations)
if err != nil {
return nil, err
}
// 加载用户oss配置信息
//cacheOssConfigKey := constant.UserOssConfigPrefix + uid + ":" + req.Provider
//ossConfig, err := l.getOssConfigFromCacheOrDb(cacheOssConfigKey, uid, req.Provider)
//if err != nil {
// return nil, err
//}
//
//service, err := l.svcCtx.StorageManager.GetStorage(uid, ossConfig)
//if err != nil {
// return nil, errors.New("get storage failed")
//}
locationMap := make(map[string][]types.LocationMeta)
for _, loc := range locations {
@@ -79,7 +69,7 @@ func (l *QueryLocationImageListLogic) QueryLocationImageList(req *types.Location
city = loc.Country
}
reqParams := make(url.Values)
presignedUrl, err := l.svcCtx.MinioClient.PresignedGetObject(l.ctx, constant.ThumbnailBucketName, loc.CoverImage, 7*24*time.Hour, reqParams)
presignedUrl, err := l.svcCtx.MinioClient.PresignedGetObject(l.ctx, constant.ThumbnailBucketName, loc.CoverImage, 15*time.Minute, reqParams)
if err != nil {
return nil, errors.New("get presigned url failed")
}
@@ -103,63 +93,3 @@ func (l *QueryLocationImageListLogic) QueryLocationImageList(req *types.Location
return &types.LocationListResponse{Records: locationListData}, nil
}
// 提取解密操作为函数
func (l *QueryLocationImageListLogic) 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 *QueryLocationImageListLogic) 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

@@ -13,6 +13,7 @@ import (
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
"sort"
"sync"
"time"
@@ -42,7 +43,7 @@ func (l *QueryThingDetailListLogic) QueryThingDetailList(req *types.ThingDetailL
return nil, errors.New("user_id not found")
}
// 缓存获取数据 v1.0.0
cacheKey := fmt.Sprintf("%s%s:%s:%s:%v", constant.ImageListPrefix, uid, req.Provider, req.Bucket, req.TagName)
cacheKey := fmt.Sprintf("%s%s:%s:%s:%s:%v", constant.ImageCachePrefix, uid, "thing", req.Provider, req.Bucket, req.TagName)
// 尝试从缓存获取
cachedResult, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err == nil {
@@ -59,6 +60,7 @@ func (l *QueryThingDetailListLogic) QueryThingDetailList(req *types.ThingDetailL
storageInfo := l.svcCtx.DB.ScaStorageInfo
storageThumb := l.svcCtx.DB.ScaStorageThumb
storageExtra := l.svcCtx.DB.ScaStorageExtra
// 数据库查询文件信息列表
var storageInfoQuery query.IScaStorageInfoDo
var storageInfoList []types.FileInfoResult
@@ -72,12 +74,13 @@ func (l *QueryThingDetailListLogic) QueryThingDetailList(req *types.ThingDetailL
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).
LeftJoin(storageThumb, storageInfo.ID.EqCol(storageThumb.InfoID)).
LeftJoin(storageExtra, storageInfo.ID.EqCol(storageExtra.InfoID)).
Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.Tag.Eq(req.TagName)).
storageExtra.Tag.Eq(req.TagName)).
Order(storageInfo.CreatedAt.Desc())
err = storageInfoQuery.Scan(&storageInfoList)
if err != nil {
@@ -147,13 +150,19 @@ func (l *QueryThingDetailListLogic) QueryThingDetailList(req *types.ThingDetailL
})
return true
})
// 按日期排序,最新的在最上面
sort.Slice(imageList, func(i, j int) bool {
dateI, _ := time.Parse("2006年1月2日 星期一", imageList[i].Date)
dateJ, _ := time.Parse("2006年1月2日 星期一", imageList[j].Date)
return dateI.After(dateJ)
})
resp = &types.ThingDetailListResponse{
Records: imageList,
}
// 缓存结果
if data, err := json.Marshal(resp); err == nil {
expireTime := 7*24*time.Hour - time.Duration(rand.Intn(60))*time.Minute
expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second
if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil {
logx.Error("Failed to cache image list:", err)
}

View File

@@ -40,21 +40,24 @@ func (l *QueryThingImageListLogic) QueryThingImageList(req *types.ThingListReque
}
storageInfo := l.svcCtx.DB.ScaStorageInfo
storageThumb := l.svcCtx.DB.ScaStorageThumb
storageExtra := l.svcCtx.DB.ScaStorageExtra
var thingList []types.ThingImageList
err = storageInfo.Select(
storageInfo.ID,
storageInfo.Category,
storageInfo.Tag,
storageExtra.Category,
storageExtra.Tag,
storageThumb.ThumbPath,
storageInfo.CreatedAt).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).
LeftJoin(storageThumb, storageInfo.ID.EqCol(storageThumb.InfoID)).
LeftJoin(storageExtra, storageInfo.ID.EqCol(storageExtra.InfoID)).
Where(storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.Category.IsNotNull(),
storageInfo.Tag.IsNotNull(),
storageInfo.Category.Length().Gt(0),
storageInfo.Tag.Length().Gte(0)).
storageExtra.Category.IsNotNull(),
storageExtra.Tag.IsNotNull(),
storageExtra.Category.Length().Gt(0),
storageExtra.Tag.Length().Gte(0)).
Order(storageInfo.CreatedAt.Desc()).
Scan(&thingList)
if err != nil {

View File

@@ -3,7 +3,6 @@ package storage
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -13,7 +12,6 @@ import (
"github.com/zeromicro/go-zero/core/logx"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
"gorm.io/gorm"
"io"
"mime/multipart"
"net/http"
@@ -26,9 +24,7 @@ import (
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
"schisandra-album-cloud-microservices/common/geo_json"
"schisandra-album-cloud-microservices/common/storage/config"
"strconv"
"strings"
"sync"
"time"
@@ -172,49 +168,9 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) {
if err != nil {
return "", errors.New("publish message failed")
}
// ------------------------------------------------------------------------
//// 根据 GPS 信息获取地理位置信息
//country, province, city, err := l.getGeoLocation(result.Latitude, result.Longitude)
//if err != nil {
// return "", err
//}
//// 将地址信息保存到数据库
//locationId, err := l.saveFileLocationInfoToDB(uid, result.Provider, result.Bucket, result.Latitude, result.Longitude, country, province, city, filePath)
//if err != nil {
// return "", err
//}
//
//// 将 EXIF 和文件信息存入数据库
//id, err := l.saveFileInfoToDB(uid, bucket, provider, header, result, locationId, faceId, filePath)
//if err != nil {
// return "", err
//}
//// 删除缓存
//l.afterImageUpload(uid, provider, bucket)
//
//// redis 保存最近7天上传的文件列表
//err = l.saveRecentFileList(uid, url, id, result, header.Filename)
//if err != nil {
// return "", err
//}
return "success", nil
}
// 将 multipart.File 转为 Base64 字符串
func (l *UploadFileLogic) fileToBase64(file multipart.File) (string, error) {
// 读取文件内容
fileBytes, err := io.ReadAll(file)
if err != nil {
return "", err
}
// 将文件内容转为 Base64 编码
return base64.StdEncoding.EncodeToString(fileBytes), nil
}
// 获取用户 ID
func (l *UploadFileLogic) getUserID() (string, error) {
uid, ok := l.ctx.Value("user_id").(string)
@@ -224,16 +180,6 @@ func (l *UploadFileLogic) getUserID() (string, error) {
return uid, nil
}
// 在UploadImageLogic或其他需要使缓存失效的逻辑中添加
func (l *UploadFileLogic) afterImageUpload(uid, provider, bucket string) {
for _, sort := range []bool{true, false} {
key := fmt.Sprintf("%s%s:%s:%s:%v", constant.ImageListPrefix, uid, provider, bucket, sort)
if err := l.svcCtx.RedisClient.Del(l.ctx, key).Err(); err != nil {
logx.Errorf("删除缓存键 %s 失败: %v", key, err)
}
}
}
// 解析上传的文件
func (l *UploadFileLogic) getUploadedFile(r *http.Request) (multipart.File, *multipart.FileHeader, error) {
file, header, err := r.FormFile("file")
@@ -262,19 +208,6 @@ func (l *UploadFileLogic) parseImageInfoResult(r *http.Request) (types.File, err
return result, nil
}
// 根据 GPS 信息获取地理位置信息
func (l *UploadFileLogic) getGeoLocation(latitude, longitude float64) (string, string, string, error) {
if latitude == 0.000000 || longitude == 0.000000 {
return "", "", "", nil
}
country, province, city, err := geo_json.GetAddress(latitude, longitude, l.svcCtx.GeoRegionData)
if err != nil {
return "", "", "", errors.New("get geo location failed")
}
return country, province, city, nil
}
// 上传文件到 OSS
func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHeader, file multipart.File, result types.File) (string, error) {
cacheKey := constant.UserOssConfigPrefix + uid + ":" + result.Provider
@@ -345,75 +278,6 @@ func (l *UploadFileLogic) uploadFileToMinio(uid string, header *multipart.FileHe
return objectKey, presignedURL.String(), nil
}
func (l *UploadFileLogic) saveFileLocationInfoToDB(uid string, provider string, bucket string, latitude float64, longitude float64, country string, province string, city string, filePath string) (int64, error) {
if latitude == 0.000000 || longitude == 0.000000 {
return 0, nil
}
locationDB := l.svcCtx.DB.ScaStorageLocation
storageLocations, err := locationDB.Where(locationDB.UserID.Eq(uid), locationDB.Province.Eq(province), locationDB.City.Eq(city)).First()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return 0, err
}
if storageLocations == nil {
locationInfo := model.ScaStorageLocation{
Provider: provider,
Bucket: bucket,
UserID: uid,
Country: country,
City: city,
Province: province,
Latitude: fmt.Sprintf("%f", latitude),
Longitude: fmt.Sprintf("%f", longitude),
Total: 1,
CoverImage: filePath,
}
err = locationDB.Create(&locationInfo)
if err != nil {
return 0, err
}
return locationInfo.ID, nil
} else {
info, err := locationDB.Where(locationDB.ID.Eq(storageLocations.ID), locationDB.UserID.Eq(uid)).UpdateColumnSimple(locationDB.Total.Add(1), locationDB.CoverImage.Value(filePath))
if err != nil {
return 0, err
}
if info.RowsAffected == 0 {
return 0, errors.New("update location failed")
}
return storageLocations.ID, nil
}
}
// 将 EXIF 和文件信息存入数据库
func (l *UploadFileLogic) saveFileInfoToDB(uid, bucket, provider string, header *multipart.FileHeader, result types.File, locationId, faceId int64, filePath string) (int64, error) {
typeName := l.classifyFile(result.FileType, result.IsScreenshot)
scaStorageInfo := &model.ScaStorageInfo{
UserID: uid,
Provider: provider,
Bucket: bucket,
FileName: header.Filename,
FileSize: strconv.FormatInt(header.Size, 10),
FileType: result.FileType,
Path: filePath,
Landscape: result.Landscape,
Tag: result.TagName,
IsAnime: strconv.FormatBool(result.IsAnime),
Category: result.TopCategory,
LocationID: locationId,
FaceID: faceId,
Type: typeName,
Width: result.Width,
Height: result.Height,
}
err := l.svcCtx.DB.ScaStorageInfo.Create(scaStorageInfo)
if err != nil {
return 0, errors.New("create storage info failed")
}
return scaStorageInfo.ID, nil
}
// 提取解密操作为函数
func (l *UploadFileLogic) decryptConfig(dbConfig *model.ScaStorageConfig) (*config.StorageConfig, error) {
accessKey, err := encrypt.Decrypt(dbConfig.AccessKey, l.svcCtx.Config.Encrypt.Key)
@@ -503,28 +367,3 @@ func (l *UploadFileLogic) classifyFile(mimeType string, isScreenshot bool) strin
}
return "unknown"
}
// 保存最近7天上传的文件列表
func (l *UploadFileLogic) saveRecentFileList(uid, url string, id int64, result types.File, filename string) error {
redisKey := constant.ImageRecentPrefix + uid + ":" + strconv.FormatInt(id, 10)
imageMeta := types.ImageMeta{
ID: id,
URL: url,
FileName: filename,
Width: result.Width,
Height: result.Height,
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
}
marshal, err := json.Marshal(imageMeta)
if err != nil {
logx.Error(err)
return errors.New("marshal image meta failed")
}
err = l.svcCtx.RedisClient.Set(l.ctx, redisKey, marshal, time.Hour*24*7).Err()
if err != nil {
logx.Error(err)
return errors.New("save recent file list failed")
}
return nil
}

View File

@@ -55,23 +55,27 @@ func (c *NsqImageProcessConsumer) HandleMessage(msg *nsq.Message) error {
return err
}
// 将地址信息保存到数据库
locationId, err := c.saveFileLocationInfoToDB(message.UID, message.Data.Provider, message.Data.Bucket, message.Data.Latitude, message.Data.Longitude, country, province, city, message.ThumbPath)
locationId, err := c.saveFileLocationInfoToDB(message.UID, message.Data.Latitude, message.Data.Longitude, country, province, city, message.ThumbPath)
if err != nil {
return err
}
thumbnailId, err := c.saveFileThumbnailInfoToDB(message.UID, message.ThumbPath, message.Data.ThumbW, message.Data.ThumbH, message.Data.ThumbSize)
// 将文件信息存入数据库
id, err := c.saveFileInfoToDB(message.UID, message.Data.Bucket, message.Data.Provider, message.FileHeader, message.Data, locationId, message.FaceID, message.FilePath, thumbnailId)
storageId, err := c.saveFileInfoToDB(message.UID, message.Data.Bucket, message.Data.Provider, message.FileHeader, message.Data, message.FaceID, message.FilePath, locationId)
if err != nil {
return err
}
err = c.saveFileThumbnailInfoToDB(message.UID, message.ThumbPath, message.Data.ThumbW, message.Data.ThumbH, message.Data.ThumbSize, storageId)
if err != nil {
return err
}
// 删除缓存
c.afterImageUpload(message.UID, message.Data.Provider, message.Data.Bucket)
c.afterImageUpload(message.UID)
// redis 保存最近7天上传的文件列表
err = c.saveRecentFileList(message.UID, message.PresignedURL, id, message.Data, message.FileHeader.Filename)
err = c.saveRecentFileList(message.UID, message.PresignedURL, storageId, message.Data, message.FileHeader.Filename)
if err != nil {
return err
}
@@ -150,15 +154,16 @@ func (c *NsqImageProcessConsumer) classifyFile(mimeType string, isScreenshot boo
"video/x-matroska": "video",
}
// 如果isScreenshot为true则返回"screenshot"
if isScreenshot {
return "screenshot"
}
// 根据MIME类型从map中获取分类
if classification, exists := typeMap[mimeType]; exists {
return classification
}
// 如果isScreenshot为true则返回"screenshot"
if isScreenshot {
return "screenshot"
}
return "unknown"
}
@@ -207,7 +212,7 @@ func (c *NsqImageProcessConsumer) decryptConfig(dbConfig *model.ScaStorageConfig
}, nil
}
func (c *NsqImageProcessConsumer) saveFileLocationInfoToDB(uid string, provider string, bucket string, latitude float64, longitude float64, country string, province string, city string, filePath string) (int64, error) {
func (c *NsqImageProcessConsumer) saveFileLocationInfoToDB(uid string, latitude float64, longitude float64, country string, province string, city string, filePath string) (int64, error) {
if latitude == 0.000000 || longitude == 0.000000 {
return 0, nil
}
@@ -218,35 +223,24 @@ func (c *NsqImageProcessConsumer) saveFileLocationInfoToDB(uid string, provider
}
if storageLocations == nil {
locationInfo := model.ScaStorageLocation{
Provider: provider,
Bucket: bucket,
UserID: uid,
Country: country,
City: city,
Province: province,
Latitude: fmt.Sprintf("%f", latitude),
Longitude: fmt.Sprintf("%f", longitude),
Total: 1,
CoverImage: filePath,
}
err = locationDB.Create(&locationInfo)
if err != nil {
return 0, err
}
return locationInfo.ID, nil
} else {
info, err := locationDB.Where(locationDB.ID.Eq(storageLocations.ID), locationDB.UserID.Eq(uid)).UpdateColumnSimple(locationDB.Total.Add(1), locationDB.CoverImage.Value(filePath))
if err != nil {
return 0, err
}
if info.RowsAffected == 0 {
return 0, errors.New("update location failed")
}
return storageLocations.ID, nil
return 0, nil
}
return storageLocations.ID, nil
}
func (c *NsqImageProcessConsumer) saveFileThumbnailInfoToDB(uid string, filePath string, width, height float64, size float64) (int64, error) {
func (c *NsqImageProcessConsumer) saveFileThumbnailInfoToDB(uid string, filePath string, width, height float64, size float64, storageId int64) error {
storageThumb := c.svcCtx.DB.ScaStorageThumb
storageThumbInfo := &model.ScaStorageThumb{
UserID: uid,
@@ -254,18 +248,25 @@ func (c *NsqImageProcessConsumer) saveFileThumbnailInfoToDB(uid string, filePath
ThumbW: width,
ThumbH: height,
ThumbSize: size,
InfoID: storageId,
}
err := storageThumb.Create(storageThumbInfo)
if err != nil {
logx.Error(err)
return 0, errors.New("create storage thumb failed")
return errors.New("create storage thumb failed")
}
return storageThumbInfo.ID, nil
return nil
}
// 将 EXIF 和文件信息存入数据库
func (c *NsqImageProcessConsumer) saveFileInfoToDB(uid, bucket, provider string, header *multipart.FileHeader, result types.File, locationId, faceId int64, filePath string, thumbnailId int64) (int64, error) {
func (c *NsqImageProcessConsumer) saveFileInfoToDB(uid, bucket, provider string, header *multipart.FileHeader, result types.File, faceId int64, filePath string, locationID int64) (int64, error) {
tx := c.svcCtx.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback() // 如果有panic发生回滚事务
logx.Errorf("transaction rollback: %v", r)
}
}()
typeName := c.classifyFile(result.FileType, result.IsScreenshot)
scaStorageInfo := &model.ScaStorageInfo{
UserID: uid,
@@ -275,31 +276,55 @@ func (c *NsqImageProcessConsumer) saveFileInfoToDB(uid, bucket, provider string,
FileSize: strconv.FormatInt(header.Size, 10),
FileType: result.FileType,
Path: filePath,
Landscape: result.Landscape,
Tag: result.TagName,
IsAnime: strconv.FormatBool(result.IsAnime),
Category: result.TopCategory,
LocationID: locationId,
FaceID: faceId,
Type: typeName,
Width: result.Width,
Height: result.Height,
ThumbID: thumbnailId,
LocationID: locationID,
}
err := c.svcCtx.DB.ScaStorageInfo.Create(scaStorageInfo)
err := tx.ScaStorageInfo.Create(scaStorageInfo)
if err != nil {
tx.Rollback()
return 0, errors.New("create storage info failed")
}
scaStorageExtra := &model.ScaStorageExtra{
UserID: uid,
InfoID: scaStorageInfo.ID,
Landscape: result.Landscape,
Tag: result.TagName,
IsAnime: strconv.FormatBool(result.IsAnime),
Category: result.TopCategory,
}
err = tx.ScaStorageExtra.Create(scaStorageExtra)
if err != nil {
tx.Rollback()
return 0, errors.New("create storage extra failed")
}
err = tx.Commit()
if err != nil {
tx.Rollback()
return 0, errors.New("commit failed")
}
return scaStorageInfo.ID, nil
}
// 在UploadImageLogic或其他需要使缓存失效的逻辑中添加
func (c *NsqImageProcessConsumer) afterImageUpload(uid, provider, bucket string) {
for _, sort := range []bool{true, false} {
key := fmt.Sprintf("%s%s:%s:%s:%v", constant.ImageListPrefix, uid, provider, bucket, sort)
if err := c.svcCtx.RedisClient.Del(c.ctx, key).Err(); err != nil {
logx.Errorf("删除缓存键 %s 失败: %v", key, err)
}
func (c *NsqImageProcessConsumer) afterImageUpload(uid string) {
// 删除缓存
keyPattern := fmt.Sprintf("%s%s:%s", constant.ImageCachePrefix, uid, "*")
// 获取所有匹配的键
keys, err := c.svcCtx.RedisClient.Keys(c.ctx, keyPattern).Result()
if err != nil {
logx.Errorf("获取缓存键 %s 失败: %v", keyPattern, err)
}
// 如果没有匹配的键,直接返回
if len(keys) == 0 {
logx.Infof("没有找到匹配的缓存键: %s", keyPattern)
return
}
// 删除所有匹配的键
if err := c.svcCtx.RedisClient.Del(c.ctx, keys...).Err(); err != nil {
logx.Errorf("删除缓存键 %s 失败: %v", keyPattern, err)
}
}

View File

@@ -63,3 +63,12 @@ type ShareImageInfo struct {
Provider string `json:"provider"`
Bucket string `json:"bucket"`
}
type LocationInfo struct {
ID int64 `json:"id"`
Country string `json:"country"`
City string `json:"city"`
Province string `json:"province"`
CoverImage string `json:"cover_image"`
Total int64 `json:"total"`
}

View File

@@ -76,6 +76,17 @@ type AllImageListResponse struct {
Records []AllImageDetail `json:"records"`
}
type BucketCapacityRequest struct {
Provider string `json:"provider"`
Bucket string `json:"bucket"`
}
type BucketCapacityResponse struct {
Capacity string `json:"capacity"`
Used string `json:"used"`
Percentage float64 `json:"percentage"`
}
type CommentContent struct {
NickName string `json:"nickname"`
Avatar string `json:"avatar"`
@@ -147,6 +158,16 @@ type CommentResponse struct {
ReplyTo int64 `json:"reply_to,omitempty"`
}
type DeleteImageRequest struct {
IDS []int64 `json:"ids"`
Provider string `json:"provider"`
Bucket string `json:"bucket"`
}
type DeleteRecordListResponse struct {
Records []AllImageDetail `json:"records"`
}
type FaceDetailListRequest struct {
FaceID int64 `json:"face_id"`
Provider string `json:"provider"`
@@ -254,13 +275,22 @@ type PhoneLoginRequest struct {
AutoLogin bool `json:"auto_login"`
}
type QueryDeleteRecordRequest struct {
Provider string `json:"provider"`
Bucket string `json:"bucket"`
}
type QueryShareImageRequest struct {
ShareCode string `json:"share_code"`
InviteCode string `json:"invite_code"`
AccessPassword string `json:"access_password,omitempty"`
}
type QueryShareImageResponse struct {
List []ShareImageListMeta `json:"list"`
Records []ShareImageListMeta `json:"records"`
}
type QueryShareInfoRequest struct {
InviteCode string `json:"invite_code"`
}
type RecentListResponse struct {
@@ -320,8 +350,8 @@ type ShareImageListMeta struct {
FileName string `json:"file_name"`
URL string `json:"url"`
Thumbnail string `json:"thumbnail"`
ThumbW float64 `json:"thumb_w"`
ThumbH float64 `json:"thumb_h"`
Width float64 `json:"width"`
Height float64 `json:"height"`
ThumbSize float64 `json:"thumb_size"`
CreatedAt string `json:"created_at"`
}
@@ -346,11 +376,34 @@ type ShareImageRequest struct {
Images []ShareImageMeta `json:"images"`
}
type ShareInfoResponse struct {
ID int64 `json:"id"`
CoverImage string `json:"cover_image"`
CreatedAt string `json:"created_at"`
VisitLimit int64 `json:"visit_limit"`
ExpireTime string `json:"expire_time"`
ImageCount int64 `json:"image_count"`
VisitCount int64 `json:"visit_count"`
ViewerCount int64 `json:"viewer_count"`
SharerAvatar string `json:"sharer_avatar"`
SharerName string `json:"sharer_name"`
AlbumName string `json:"album_name"`
}
type ShareOverviewResponse struct {
VisitCount int64 `json:"visit_count"`
VisitCountToday int64 `json:"visit_count_today"`
ViewerCount int64 `json:"viewer_count"`
ViewerCountToday int64 `json:"viewer_count_today"`
PublishCount int64 `json:"publish_count"`
PublishCountToday int64 `json:"publish_count_today"`
}
type ShareRecord struct {
ID int64 `json:"id"`
CoverImage string `json:"cover_image"`
CreatedAt string `json:"created_at"`
ShareCode string `json:"share_code"`
InviteCode string `json:"invite_code"`
VisitLimit int64 `json:"visit_limit"`
AccessPassword string `json:"access_password"`
ValidityPeriod int64 `json:"validity_period"`