diff --git a/app/auth/api/auth.api b/app/auth/api/auth.api index 10974e9..9a87b20 100644 --- a/app/auth/api/auth.api +++ b/app/auth/api/auth.api @@ -659,6 +659,9 @@ type ( Provider string `json:"provider"` Bucket string `json:"bucket"` } + DownloadAlbumResponse { + Records []string `json:"records"` + } // 搜索图片请求参数 SearchImageRequest { Type string `json:"type"` @@ -671,6 +674,21 @@ type ( SearchImageResponse { Records []AllImageDetail `json:"records"` } + // 搜索相册请求参数 + SearchAlbumRequest { + Keyword string `json:"keyword"` + } + // 搜索相册相应参数 + SearchAlbumResponse { + Albums []Album `json:"albums"` + } + // 图片添加到相册请求参数 + AddImageToAlbumRequest { + IDS []int64 `json:"ids"` + AlbumID int64 `json:"album_id"` + Provider string `json:"provider"` + Bucket string `json:"bucket"` + } ) // 文件上传 @@ -780,11 +798,19 @@ service auth { // 下载相册 @handler downloadAlbum - post /album/download (DownloadAlbumRequest) returns (string) + post /album/download (DownloadAlbumRequest) returns (DownloadAlbumResponse) // 图片搜索 @handler searchImage post /image/search (SearchImageRequest) returns (SearchImageResponse) + + // 搜索相册 + @handler searchAlbum + post /album/search (SearchAlbumRequest) returns (SearchAlbumResponse) + + // 添加图片到相册 + @handler addImageToAlbum + post /album/add/image (AddImageToAlbumRequest) returns (string) } type ( diff --git a/app/auth/api/internal/handler/routes.go b/app/auth/api/internal/handler/routes.go index 1a74794..d68f8b4 100644 --- a/app/auth/api/internal/handler/routes.go +++ b/app/auth/api/internal/handler/routes.go @@ -257,6 +257,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { rest.WithMiddlewares( []rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.CasbinVerifyMiddleware, serverCtx.NonceMiddleware}, []rest.Route{ + { + Method: http.MethodPost, + Path: "/album/add/image", + Handler: storage.AddImageToAlbumHandler(serverCtx), + }, { Method: http.MethodPost, Path: "/album/create", @@ -287,6 +292,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/album/rename", Handler: storage.RenameAlbumHandler(serverCtx), }, + { + Method: http.MethodPost, + Path: "/album/search", + Handler: storage.SearchAlbumHandler(serverCtx), + }, { Method: http.MethodPost, Path: "/album/share", diff --git a/app/auth/api/internal/handler/storage/add_image_to_album_handler.go b/app/auth/api/internal/handler/storage/add_image_to_album_handler.go new file mode 100644 index 0000000..27e57d8 --- /dev/null +++ b/app/auth/api/internal/handler/storage/add_image_to_album_handler.go @@ -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 AddImageToAlbumHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AddImageToAlbumRequest + if err := httpx.Parse(r, &req); err != nil { + xhttp.JsonBaseResponseCtx(r.Context(), w, err) + return + } + + l := storage.NewAddImageToAlbumLogic(r.Context(), svcCtx) + resp, err := l.AddImageToAlbum(&req) + if err != nil { + xhttp.JsonBaseResponseCtx(r.Context(), w, err) + } else { + xhttp.JsonBaseResponseCtx(r.Context(), w, resp) + } + } +} diff --git a/app/auth/api/internal/handler/storage/search_album_handler.go b/app/auth/api/internal/handler/storage/search_album_handler.go new file mode 100644 index 0000000..ecaf643 --- /dev/null +++ b/app/auth/api/internal/handler/storage/search_album_handler.go @@ -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 SearchAlbumHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SearchAlbumRequest + if err := httpx.Parse(r, &req); err != nil { + xhttp.JsonBaseResponseCtx(r.Context(), w, err) + return + } + + l := storage.NewSearchAlbumLogic(r.Context(), svcCtx) + resp, err := l.SearchAlbum(&req) + if err != nil { + xhttp.JsonBaseResponseCtx(r.Context(), w, err) + } else { + xhttp.JsonBaseResponseCtx(r.Context(), w, resp) + } + } +} diff --git a/app/auth/api/internal/logic/share/delete_share_record_logic.go b/app/auth/api/internal/logic/share/delete_share_record_logic.go index 60bfe21..010a873 100644 --- a/app/auth/api/internal/logic/share/delete_share_record_logic.go +++ b/app/auth/api/internal/logic/share/delete_share_record_logic.go @@ -3,6 +3,7 @@ package share import ( "context" "errors" + "schisandra-album-cloud-microservices/common/constant" "schisandra-album-cloud-microservices/app/auth/api/internal/svc" "schisandra-album-cloud-microservices/app/auth/api/internal/types" @@ -65,6 +66,19 @@ func (l *DeleteShareRecordLogic) DeleteShareRecord(req *types.DeleteShareRecordR tx.Rollback() return "", errors.New("delete storage info record failed") } + // delete redis cache + cacheKey := constant.ImageSharePrefix + req.InviteCode + err = l.svcCtx.RedisClient.Del(l.ctx, cacheKey).Err() + if err != nil { + tx.Rollback() + return "", errors.New("delete cache failed") + } + cacheVisitKey := constant.ImageShareVisitPrefix + req.InviteCode + err = l.svcCtx.RedisClient.Del(l.ctx, cacheVisitKey).Err() + if err != nil { + tx.Rollback() + return "", errors.New("delete cache visit failed") + } err = tx.Commit() if err != nil { tx.Rollback() diff --git a/app/auth/api/internal/logic/storage/add_image_to_album_logic.go b/app/auth/api/internal/logic/storage/add_image_to_album_logic.go new file mode 100644 index 0000000..1bc6e3c --- /dev/null +++ b/app/auth/api/internal/logic/storage/add_image_to_album_logic.go @@ -0,0 +1,44 @@ +package storage + +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 AddImageToAlbumLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAddImageToAlbumLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddImageToAlbumLogic { + return &AddImageToAlbumLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AddImageToAlbumLogic) AddImageToAlbum(req *types.AddImageToAlbumRequest) (resp string, err error) { + uid, ok := l.ctx.Value("user_id").(string) + if !ok { + return "", errors.New("user_id not found") + } + storageInfo := l.svcCtx.DB.ScaStorageInfo + update, err := storageInfo.Where(storageInfo.UserID.Eq(uid), + storageInfo.ID.In(req.IDS...), + storageInfo.Provider.Eq(req.Provider), + storageInfo.Bucket.Eq(req.Bucket)).Update(storageInfo.AlbumID, req.AlbumID) + if err != nil { + return "", err + } + if update.RowsAffected == 0 { + return "", errors.New("no image found") + } + return "success", nil +} diff --git a/app/auth/api/internal/logic/storage/download_album_logic.go b/app/auth/api/internal/logic/storage/download_album_logic.go index 6358bc4..31491b7 100644 --- a/app/auth/api/internal/logic/storage/download_album_logic.go +++ b/app/auth/api/internal/logic/storage/download_album_logic.go @@ -2,6 +2,16 @@ package storage import ( "context" + "encoding/json" + "errors" + "fmt" + "github.com/redis/go-redis/v9" + "golang.org/x/sync/errgroup" + "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" "schisandra-album-cloud-microservices/app/auth/api/internal/svc" "schisandra-album-cloud-microservices/app/auth/api/internal/types" @@ -23,8 +33,115 @@ func NewDownloadAlbumLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Dow } } -func (l *DownloadAlbumLogic) DownloadAlbum(req *types.DownloadAlbumRequest) (resp string, err error) { - // todo: download album logic +func (l *DownloadAlbumLogic) DownloadAlbum(req *types.DownloadAlbumRequest) (resp *types.DownloadAlbumResponse, err error) { + uid, ok := l.ctx.Value("user_id").(string) + if !ok { + return nil, errors.New("user_id not found") + } + storageInfo := l.svcCtx.DB.ScaStorageInfo + storageInfos, err := storageInfo.Where(storageInfo.UserID.Eq(uid), storageInfo.AlbumID.Eq(req.ID), + storageInfo.Provider.Eq(req.Provider), storageInfo.Bucket.Eq(req.Bucket)).Find() + if err != nil { + return nil, err + } - return + // 加载用户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") + } + // 并发生成预签名URL + urls := make([]string, len(storageInfos)) + g, ctx := errgroup.WithContext(l.ctx) + for i := range storageInfos { + i := i + file := storageInfos[i] + g.Go(func() error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + url, err := service.PresignedURL(ctx, ossConfig.BucketName, file.Path, 5*time.Minute) + if err != nil { + return fmt.Errorf("failed to generate URL for %s: %w", file.Path, err) + } + urls[i] = url + return nil + } + }) + } + + if err = g.Wait(); err != nil { + return nil, fmt.Errorf("failed to generate URLs: %w", err) + } + + return &types.DownloadAlbumResponse{ + Records: urls, + }, nil +} + +// 提取解密操作为函数 +func (l *DownloadAlbumLogic) 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 *DownloadAlbumLogic) 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 } diff --git a/app/auth/api/internal/logic/storage/search_album_logic.go b/app/auth/api/internal/logic/storage/search_album_logic.go new file mode 100644 index 0000000..0643d66 --- /dev/null +++ b/app/auth/api/internal/logic/storage/search_album_logic.go @@ -0,0 +1,51 @@ +package storage + +import ( + "context" + "errors" + "github.com/zeromicro/go-zero/core/logx" + "schisandra-album-cloud-microservices/app/auth/api/internal/svc" + "schisandra-album-cloud-microservices/app/auth/api/internal/types" +) + +type SearchAlbumLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSearchAlbumLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SearchAlbumLogic { + return &SearchAlbumLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SearchAlbumLogic) SearchAlbum(req *types.SearchAlbumRequest) (resp *types.SearchAlbumResponse, err error) { + uid, ok := l.ctx.Value("user_id").(string) + if !ok { + return nil, errors.New("user_id not found") + } + storageAlbum := l.svcCtx.DB.ScaStorageAlbum + storageAlbums, err := storageAlbum.Where(storageAlbum.UserID.Eq(uid), storageAlbum.AlbumName.Like("%"+req.Keyword+"%")).Find() + if err != nil { + return nil, err + } + if len(storageAlbums) == 0 { + return nil, nil + } + var albums []types.Album + for _, album := range storageAlbums { + albums = append(albums, types.Album{ + ID: album.ID, + Name: album.AlbumName, + Type: album.AlbumType, + CoverImage: album.CoverImage, + CreatedAt: album.CreatedAt.Format("2006-01-02"), + }) + } + return &types.SearchAlbumResponse{ + Albums: albums, + }, nil +} diff --git a/app/auth/api/internal/types/types.go b/app/auth/api/internal/types/types.go index d7b1a9b..9abe775 100644 --- a/app/auth/api/internal/types/types.go +++ b/app/auth/api/internal/types/types.go @@ -11,6 +11,13 @@ type AccountLoginRequest struct { Key string `json:"key"` } +type AddImageToAlbumRequest struct { + IDS []int64 `json:"ids"` + AlbumID int64 `json:"album_id"` + Provider string `json:"provider"` + Bucket string `json:"bucket"` +} + type Album struct { ID int64 `json:"id"` Name string `json:"name"` @@ -181,6 +188,10 @@ type DownloadAlbumRequest struct { Bucket string `json:"bucket"` } +type DownloadAlbumResponse struct { + Records []string `json:"records"` +} + type FaceDetailListRequest struct { FaceID int64 `json:"face_id"` Provider string `json:"provider"` @@ -364,6 +375,14 @@ type RotateCaptchaResponse struct { Thumb string `json:"thumb"` } +type SearchAlbumRequest struct { + Keyword string `json:"keyword"` +} + +type SearchAlbumResponse struct { + Albums []Album `json:"albums"` +} + type SearchImageRequest struct { Type string `json:"type"` Keyword string `json:"keyword"`