From 4d0f628586e3b74dd7dcd0fe6ae0bf7450225629 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Mon, 3 Mar 2025 00:59:52 +0800 Subject: [PATCH] :sparkles: add search function --- app/auth/api/auth.api | 26 +- app/auth/api/etc/auth.yaml | 4 + app/auth/api/internal/config/config.go | 5 + .../handler/phone/common_upload_handler.go | 17 + app/auth/api/internal/handler/routes.go | 12 +- .../handler/storage/search_image_handler.go | 29 ++ .../logic/phone/common_upload_logic.go | 28 ++ .../logic/share/query_share_info_logic.go | 4 +- .../logic/storage/get_album_detail_logic.go | 2 +- .../logic/storage/get_delete_record_logic.go | 2 +- .../storage/get_face_detail_list_logic.go | 2 +- .../storage/query_all_image_list_logic.go | 2 +- .../query_location_detail_list_logic.go | 2 +- .../query_location_image_list_logic.go | 9 +- .../storage/query_recent_image_list_logic.go | 2 +- .../storage/query_thing_detail_list_logic.go | 2 +- .../logic/storage/search_image_logic.go | 379 ++++++++++++++++++ .../logic/storage/upload_file_logic.go | 98 ++--- .../api/internal/mq/image_process_consumer.go | 70 +++- app/auth/api/internal/svc/service_context.go | 3 + app/auth/api/internal/types/file_types.go | 60 ++- app/auth/api/internal/types/types.go | 17 +- .../mysql/model/sca_storage_extra.gen.go | 1 + .../mysql/query/sca_storage_extra.gen.go | 6 +- common/constant/zinc_index_name.go | 5 + common/zincx/special.go | 78 ++++ common/zincx/zincx.go | 201 ++++++++++ go.mod | 1 + go.sum | 57 +-- 29 files changed, 982 insertions(+), 142 deletions(-) create mode 100644 app/auth/api/internal/handler/phone/common_upload_handler.go create mode 100644 app/auth/api/internal/handler/storage/search_image_handler.go create mode 100644 app/auth/api/internal/logic/phone/common_upload_logic.go create mode 100644 app/auth/api/internal/logic/storage/search_image_logic.go create mode 100644 common/constant/zinc_index_name.go create mode 100644 common/zincx/special.go create mode 100644 common/zincx/zincx.go diff --git a/app/auth/api/auth.api b/app/auth/api/auth.api index 85032f1..10974e9 100644 --- a/app/auth/api/auth.api +++ b/app/auth/api/auth.api @@ -363,7 +363,7 @@ type ( group: comment // 微服务分组 prefix: /api/auth/comment // 微服务前缀 timeout: 10s // 超时时间 - maxBytes: 1048576 // 最大请求大小 + maxBytes: 10485760 // 最大请求大小 signature: false // 是否开启签名验证 middleware: SecurityHeadersMiddleware,CasbinVerifyMiddleware,NonceMiddleware // 注册中间件 MaxConns: true // 是否开启最大连接数限制 @@ -426,6 +426,9 @@ service auth { @handler sharePhoneUpload post /share/upload (SharePhoneUploadRequest) + + @handler commonUpload + post /common/upload } // 文件上传配置请求参数 @@ -656,6 +659,18 @@ type ( Provider string `json:"provider"` Bucket string `json:"bucket"` } + // 搜索图片请求参数 + SearchImageRequest { + Type string `json:"type"` + Keyword string `json:"keyword"` + Provider string `json:"provider"` + Bucket string `json:"bucket"` + InputImage string `json:"input_image,omitempty"` + } + // 搜索图片相应参数 + SearchImageResponse { + Records []AllImageDetail `json:"records"` + } ) // 文件上传 @@ -766,6 +781,10 @@ service auth { // 下载相册 @handler downloadAlbum post /album/download (DownloadAlbumRequest) returns (string) + + // 图片搜索 + @handler searchImage + post /image/search (SearchImageRequest) returns (SearchImageResponse) } type ( @@ -810,7 +829,8 @@ type ( records []ShareRecord `json:"records"` } QueryShareInfoRequest { - InviteCode string `json:"invite_code"` + InviteCode string `json:"invite_code"` + AccessPassword string `json:"access_password,omitempty"` } ShareInfoResponse { ID int64 `json:"id"` @@ -824,6 +844,8 @@ type ( SharerAvatar string `json:"sharer_avatar"` SharerName string `json:"sharer_name"` AlbumName string `json:"album_name"` + InviteCode string `json:"invite_code"` + SharerUID string `json:"sharer_uid"` } // 分享数据概览响应参数 ShareOverviewResponse { diff --git a/app/auth/api/etc/auth.yaml b/app/auth/api/etc/auth.yaml index 38f6d78..dba0570 100644 --- a/app/auth/api/etc/auth.yaml +++ b/app/auth/api/etc/auth.yaml @@ -175,3 +175,7 @@ NSQ: # NSQD地址 NSQDHost: 1.95.0.111:4150 LookUpdHost: 1.95.0.111:4161 +Zinc: + URL: http://1.95.0.111:4080 + Username: landaiqing + Password: LDQ20020618xxx diff --git a/app/auth/api/internal/config/config.go b/app/auth/api/internal/config/config.go index e6f24dc..d91ae8d 100644 --- a/app/auth/api/internal/config/config.go +++ b/app/auth/api/internal/config/config.go @@ -77,4 +77,9 @@ type Config struct { NSQDHost string LookUpdHost string } + Zinc struct { + URL string + Username string + Password string + } } diff --git a/app/auth/api/internal/handler/phone/common_upload_handler.go b/app/auth/api/internal/handler/phone/common_upload_handler.go new file mode 100644 index 0000000..76af52e --- /dev/null +++ b/app/auth/api/internal/handler/phone/common_upload_handler.go @@ -0,0 +1,17 @@ +package phone + +import ( + "net/http" + + "schisandra-album-cloud-microservices/app/auth/api/internal/logic/phone" + "schisandra-album-cloud-microservices/app/auth/api/internal/svc" + "schisandra-album-cloud-microservices/common/xhttp" +) + +func CommonUploadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := phone.NewCommonUploadLogic(r.Context(), svcCtx) + err := l.CommonUpload() + xhttp.JsonBaseResponseCtx(r.Context(), w, err) + } +} diff --git a/app/auth/api/internal/handler/routes.go b/app/auth/api/internal/handler/routes.go index 0e854fc..1a74794 100644 --- a/app/auth/api/internal/handler/routes.go +++ b/app/auth/api/internal/handler/routes.go @@ -105,7 +105,7 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { rest.WithJwt(serverCtx.Config.Auth.AccessSecret), rest.WithPrefix("/api/auth/comment"), rest.WithTimeout(10000*time.Millisecond), - rest.WithMaxBytes(1048576), + rest.WithMaxBytes(10485760), ) server.AddRoutes( @@ -163,6 +163,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { rest.WithMiddlewares( []rest.Middleware{serverCtx.SecurityHeadersMiddleware, serverCtx.NonceMiddleware}, []rest.Route{ + { + Method: http.MethodPost, + Path: "/common/upload", + Handler: phone.CommonUploadHandler(serverCtx), + }, { Method: http.MethodPost, Path: "/share/upload", @@ -347,6 +352,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/image/recent/list", Handler: storage.QueryRecentImageListHandler(serverCtx), }, + { + Method: http.MethodPost, + Path: "/image/search", + Handler: storage.SearchImageHandler(serverCtx), + }, { Method: http.MethodPost, Path: "/image/thing/detail/list", diff --git a/app/auth/api/internal/handler/storage/search_image_handler.go b/app/auth/api/internal/handler/storage/search_image_handler.go new file mode 100644 index 0000000..6f20aec --- /dev/null +++ b/app/auth/api/internal/handler/storage/search_image_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 SearchImageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SearchImageRequest + if err := httpx.Parse(r, &req); err != nil { + xhttp.JsonBaseResponseCtx(r.Context(), w, err) + return + } + + l := storage.NewSearchImageLogic(r.Context(), svcCtx) + resp, err := l.SearchImage(&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/phone/common_upload_logic.go b/app/auth/api/internal/logic/phone/common_upload_logic.go new file mode 100644 index 0000000..52fba3d --- /dev/null +++ b/app/auth/api/internal/logic/phone/common_upload_logic.go @@ -0,0 +1,28 @@ +package phone + +import ( + "context" + + "github.com/zeromicro/go-zero/core/logx" + "schisandra-album-cloud-microservices/app/auth/api/internal/svc" +) + +type CommonUploadLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCommonUploadLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CommonUploadLogic { + return &CommonUploadLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CommonUploadLogic) CommonUpload() error { + // todo: add your logic here and delete this line + + return nil +} diff --git a/app/auth/api/internal/logic/share/query_share_info_logic.go b/app/auth/api/internal/logic/share/query_share_info_logic.go index 56f744d..96f2c7b 100644 --- a/app/auth/api/internal/logic/share/query_share_info_logic.go +++ b/app/auth/api/internal/logic/share/query_share_info_logic.go @@ -48,12 +48,14 @@ func (l *QueryShareInfoLogic) QueryShareInfo(req *types.QueryShareInfoRequest) ( shareVisit.Views.As("visit_count"), shareVisit.UserID.Count().As("viewer_count"), authUser.Avatar.As("sharer_avatar"), - authUser.Nickname.As("sharer_name")). + authUser.Nickname.As("sharer_name"), + authUser.UID.As("sharer_uid")). 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), + storageShare.AccessPassword.Eq(req.AccessPassword), shareVisit.UserID.Eq(uid)). Group( storageShare.ID, diff --git a/app/auth/api/internal/logic/storage/get_album_detail_logic.go b/app/auth/api/internal/logic/storage/get_album_detail_logic.go index 5a335c4..9b64b3a 100644 --- a/app/auth/api/internal/logic/storage/get_album_detail_logic.go +++ b/app/auth/api/internal/logic/storage/get_album_detail_logic.go @@ -182,7 +182,7 @@ func (l *GetAlbumDetailLogic) GetAlbumDetail(req *types.AlbumDetailListRequest) // 缓存结果 if data, err := json.Marshal(resp); err == nil { - expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second + expireTime := 1*time.Minute + time.Duration(rand.Intn(60))*time.Second if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil { logx.Error("Failed to cache image list:", err) } diff --git a/app/auth/api/internal/logic/storage/get_delete_record_logic.go b/app/auth/api/internal/logic/storage/get_delete_record_logic.go index 1e9d06a..5661af7 100644 --- a/app/auth/api/internal/logic/storage/get_delete_record_logic.go +++ b/app/auth/api/internal/logic/storage/get_delete_record_logic.go @@ -156,7 +156,7 @@ func (l *GetDeleteRecordLogic) GetDeleteRecord(req *types.QueryDeleteRecordReque // 缓存结果 if data, err := json.Marshal(resp); err == nil { - expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second + expireTime := 1*time.Minute + time.Duration(rand.Intn(60))*time.Second if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil { logx.Error("Failed to cache image list:", err) } diff --git a/app/auth/api/internal/logic/storage/get_face_detail_list_logic.go b/app/auth/api/internal/logic/storage/get_face_detail_list_logic.go index 348218a..e789d0f 100644 --- a/app/auth/api/internal/logic/storage/get_face_detail_list_logic.go +++ b/app/auth/api/internal/logic/storage/get_face_detail_list_logic.go @@ -156,7 +156,7 @@ func (l *GetFaceDetailListLogic) GetFaceDetailList(req *types.FaceDetailListRequ // 缓存结果 if data, err := json.Marshal(resp); err == nil { - expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second + expireTime := 1*time.Minute + time.Duration(rand.Intn(60))*time.Second if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil { logx.Error("Failed to cache image list:", err) } diff --git a/app/auth/api/internal/logic/storage/query_all_image_list_logic.go b/app/auth/api/internal/logic/storage/query_all_image_list_logic.go index e3fcb7f..86fce21 100644 --- a/app/auth/api/internal/logic/storage/query_all_image_list_logic.go +++ b/app/auth/api/internal/logic/storage/query_all_image_list_logic.go @@ -189,7 +189,7 @@ func (l *QueryAllImageListLogic) QueryAllImageList(req *types.AllImageListReques // 缓存结果 if data, err := json.Marshal(resp); err == nil { - expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second + expireTime := 1*time.Minute + time.Duration(rand.Intn(60))*time.Second if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil { logx.Error("Failed to cache image list:", err) } diff --git a/app/auth/api/internal/logic/storage/query_location_detail_list_logic.go b/app/auth/api/internal/logic/storage/query_location_detail_list_logic.go index b6d88ac..8f78d98 100644 --- a/app/auth/api/internal/logic/storage/query_location_detail_list_logic.go +++ b/app/auth/api/internal/logic/storage/query_location_detail_list_logic.go @@ -160,7 +160,7 @@ func (l *QueryLocationDetailListLogic) QueryLocationDetailList(req *types.Locati // 缓存结果 if data, err := json.Marshal(resp); err == nil { - expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second + expireTime := 1*time.Minute + time.Duration(rand.Intn(60))*time.Second if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil { logx.Error("Failed to cache image list:", err) } diff --git a/app/auth/api/internal/logic/storage/query_location_image_list_logic.go b/app/auth/api/internal/logic/storage/query_location_image_list_logic.go index 477eb57..485d870 100644 --- a/app/auth/api/internal/logic/storage/query_location_image_list_logic.go +++ b/app/auth/api/internal/logic/storage/query_location_image_list_logic.go @@ -40,17 +40,18 @@ func (l *QueryLocationImageListLogic) QueryLocationImageList(req *types.Location storageInfo := l.svcCtx.DB.ScaStorageInfo var locations []types.LocationInfo - err = storageLocation.Select( + err = storageInfo.Select( storageLocation.ID, storageLocation.Country, storageLocation.City, storageLocation.Province, storageLocation.CoverImage, storageInfo.ID.Count().As("total")). - LeftJoin(storageInfo, storageInfo.LocationID.EqCol(storageLocation.ID)). - Where(storageLocation.UserID.Eq(uid), + LeftJoin(storageLocation, storageLocation.ID.EqCol(storageInfo.LocationID)). + Where(storageInfo.UserID.Eq(uid), storageInfo.Provider.Eq(req.Provider), - storageInfo.Bucket.Eq(req.Bucket)). + storageInfo.Bucket.Eq(req.Bucket), + storageInfo.LocationID.Neq(0)). Order(storageLocation.CreatedAt.Desc()). Group(storageLocation.ID). Scan(&locations) diff --git a/app/auth/api/internal/logic/storage/query_recent_image_list_logic.go b/app/auth/api/internal/logic/storage/query_recent_image_list_logic.go index 281ab0c..82a3871 100644 --- a/app/auth/api/internal/logic/storage/query_recent_image_list_logic.go +++ b/app/auth/api/internal/logic/storage/query_recent_image_list_logic.go @@ -173,7 +173,7 @@ func (l *QueryRecentImageListLogic) QueryRecentImageList(req *types.RecentListRe // 缓存结果 if data, err := json.Marshal(resp); err == nil { - expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second + expireTime := 1*time.Minute + time.Duration(rand.Intn(60))*time.Second if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil { logx.Error("Failed to cache image list:", err) } diff --git a/app/auth/api/internal/logic/storage/query_thing_detail_list_logic.go b/app/auth/api/internal/logic/storage/query_thing_detail_list_logic.go index 6047b38..d5f27f7 100644 --- a/app/auth/api/internal/logic/storage/query_thing_detail_list_logic.go +++ b/app/auth/api/internal/logic/storage/query_thing_detail_list_logic.go @@ -160,7 +160,7 @@ func (l *QueryThingDetailListLogic) QueryThingDetailList(req *types.ThingDetailL // 缓存结果 if data, err := json.Marshal(resp); err == nil { - expireTime := 5*time.Minute + time.Duration(rand.Intn(300))*time.Second + expireTime := 1*time.Minute + time.Duration(rand.Intn(60))*time.Second if err := l.svcCtx.RedisClient.Set(l.ctx, cacheKey, data, expireTime).Err(); err != nil { logx.Error("Failed to cache image list:", err) } diff --git a/app/auth/api/internal/logic/storage/search_image_logic.go b/app/auth/api/internal/logic/storage/search_image_logic.go new file mode 100644 index 0000000..378f65e --- /dev/null +++ b/app/auth/api/internal/logic/storage/search_image_logic.go @@ -0,0 +1,379 @@ +package storage + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/redis/go-redis/v9" + "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" + "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" + "strconv" + "strings" + "sync" + "time" +) + +type SearchImageLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSearchImageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SearchImageLogic { + return &SearchImageLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SearchImageLogic) SearchImage(req *types.SearchImageRequest) (resp *types.SearchImageResponse, err error) { + uid, ok := l.ctx.Value("user_id").(string) + if !ok { + return nil, errors.New("user_id not found") + } + + baseQuery := map[string]interface{}{ + "query": map[string]interface{}{ + "bool": map[string]interface{}{ + "must": []map[string]interface{}{ + {"term": map[string]interface{}{"provider": req.Provider}}, + {"term": map[string]interface{}{"bucket": req.Bucket}}, + {"term": map[string]interface{}{"uid": uid}}, + }, + }, + }, + } + switch req.Type { + case "time": + // 时间范围查询(示例:"[2023-01-01,2023-12-31]") + start, end, err := parseTimeRange(req.Keyword) + if err != nil { + return nil, fmt.Errorf("时间解析失败: %w", err) + } + addTimeRangeQuery(baseQuery, start, end) + case "person": + // 人脸ID精确匹配 + faceID, err := strconv.ParseInt(req.Keyword, 10, 64) + if err != nil { + return nil, fmt.Errorf("人脸ID格式错误: %w", err) + } + addFaceIDQuery(baseQuery, faceID) + case "thing": + // 标签和分类匹配 + addThingQuery(baseQuery, req.Keyword) + case "picture": + // 图片属性匹配(示例:文件类型) + addPictureQuery(baseQuery, req.Keyword) + case "location": + addLocationQuery(baseQuery, req.Keyword) + default: + return nil, errors.New("不支持的查询类型") + } + // 执行查询 + var target types.ZincFileInfo + result, err := l.svcCtx.ZincClient.Search(constant.ZincIndexNameStorageInfo, baseQuery, target) + if err != nil { + return nil, fmt.Errorf("查询失败: %w", 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") + } + // 按日期分组处理 + groupedImages := sync.Map{} + var wg sync.WaitGroup + + for _, hit := range result.Hits.Hits { + wg.Add(1) + go func(hit struct { // 明确传递 hit 结构 + ID string `json:"_id"` + Source interface{} `json:"_source"` + Score float64 `json:"_score"` + }) { + defer wg.Done() + + // 类型断言转换 + source, err := convertToZincFileInfo(hit.Source) + if err != nil { + logx.Errorf("数据转换失败: %v | 原始数据: %+v", err, hit.Source) + return + } + + // 生成日期键(示例格式:2023年8月15日 星期二) + weekday := WeekdayMap[source.CreatedAt.Weekday()] + dateKey := source.CreatedAt.Format("2006年1月2日 星期" + weekday) + + // 生成访问链接 + thumbnailUrl, err := service.PresignedURL(l.ctx, ossConfig.BucketName, source.ThumbPath, 15*time.Minute) + if err != nil { + logx.Errorf("生成缩略图链接失败: %v", err) + return + } + + fileUrl, err := service.PresignedURL(l.ctx, ossConfig.BucketName, source.FilePath, 15*time.Minute) + if err != nil { + logx.Errorf("生成文件链接失败: %v", err) + return + } + + // 构建元数据 + meta := types.ImageMeta{ + ID: source.StorageId, + FileName: source.FileName, + URL: fileUrl, + Width: source.ThumbW, + Height: source.ThumbH, + CreatedAt: source.CreatedAt.Format("2006-01-02 15:04:05"), + Thumbnail: thumbnailUrl, + } + + // 线程安全写入 Map + value, _ := groupedImages.LoadOrStore(dateKey, []types.ImageMeta{}) + images := value.([]types.ImageMeta) + images = append(images, meta) + groupedImages.Store(dateKey, images) + }(struct { + ID string `json:"_id"` + Source interface{} `json:"_source"` + Score float64 `json:"_score"` + }(hit)) // 将 hit 作为参数传递 + } + wg.Wait() + + // 转换分组结果 + var records []types.AllImageDetail + groupedImages.Range(func(key, value interface{}) bool { + records = append(records, types.AllImageDetail{ + Date: key.(string), + List: value.([]types.ImageMeta), + }) + return true + }) + // 按日期降序排序 + sort.Slice(records, func(i, j int) bool { + ti, _ := time.Parse("2006年1月2日 星期一", records[i].Date) + tj, _ := time.Parse("2006年1月2日 星期一", records[j].Date) + return ti.After(tj) + }) + return &types.SearchImageResponse{ + Records: records, + }, nil + +} + +// 时间范围解析(支持日期和时间戳) +func parseTimeRange(input string) (int64, int64, error) { + input = strings.Trim(input, "[]") + parts := strings.Split(input, ",") + if len(parts) != 2 { + return 0, 0, errors.New("时间格式错误") + } + + parseTime := func(s string) (int64, error) { + // 尝试解析为日期格式 + if t, err := time.Parse("2006-01-02", strings.TrimSpace(s)); err == nil { + return t.Unix(), nil + } + // 尝试解析为时间戳 + if ts, err := strconv.ParseInt(s, 10, 64); err == nil { + return ts, nil + } + return 0, errors.New("无效时间格式") + } + + start, err := parseTime(parts[0]) + if err != nil { + return 0, 0, err + } + end, err := parseTime(parts[1]) + if err != nil { + return 0, 0, err + } + return start, end, nil +} + +func addTimeRangeQuery(query map[string]interface{}, start, end int64) { + must := query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] + timeQuery := map[string]interface{}{ + "range": map[string]interface{}{ + "created_at": map[string]interface{}{ // 改为使用 created_at 字段 + "gte": start * 1000, // 转换为毫秒(根据格式决定) + "lte": end * 1000, + }, + }, + } + query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] = append(must.([]map[string]interface{}), timeQuery) +} + +// 修改后的标签查询 +func addThingQuery(query map[string]interface{}, keyword string) { + must := query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] + tagQuery := map[string]interface{}{ + "multi_match": map[string]interface{}{ + "query": keyword, + "fields": []string{"tag_name", "top_category"}, // 使用新字段名 + }, + } + query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] = append(must.([]map[string]interface{}), tagQuery) +} + +// 修改后的文件类型查询 +// 修改后的 picture 类型查询 (同时搜索文件名和文件类型) +func addPictureQuery(query map[string]interface{}, keyword string) { + must := query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] + + pictureQuery := map[string]interface{}{ + "bool": map[string]interface{}{ + "should": []map[string]interface{}{ + { + "wildcard": map[string]interface{}{ + "file_name": "*" + strings.ToLower(keyword) + "*", + }, + }, + { + "term": map[string]interface{}{ + "file_type": strings.ToLower(keyword), + }, + }, + }, + "minimum_should_match": 1, + }, + } + + query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] = append( + must.([]map[string]interface{}), + pictureQuery, + ) +} + +// 添加人脸ID查询 +func addFaceIDQuery(query map[string]interface{}, faceID int64) { + must := query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] + idQuery := map[string]interface{}{ + "term": map[string]interface{}{ + "face_id": faceID, + }, + } + query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] = append(must.([]map[string]interface{}), idQuery) +} + +func addLocationQuery(query map[string]interface{}, keyword string) { + must := query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] + + locationQuery := map[string]interface{}{ + "multi_match": map[string]interface{}{ + "query": keyword, + "fields": []string{"country", "province", "city"}, + "type": "best_fields", // 优先匹配最多字段 + }, + } + + query["query"].(map[string]interface{})["bool"].(map[string]interface{})["must"] = append( + must.([]map[string]interface{}), + locationQuery, + ) +} + +// ZincSearch 响应结构 +type ZincSearchResult struct { + Hits struct { + Total struct { + Value int `json:"value"` + } `json:"total"` + Hits []struct { + ID string `json:"_id"` + Source types.FileUploadMessage `json:"_source"` + Score float64 `json:"_score"` + } `json:"hits"` + } `json:"hits"` +} + +// 提取解密操作为函数 +func (l *SearchImageLogic) 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 *SearchImageLogic) 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 +} + +// 新的数据转换函数 +func convertToZincFileInfo(source interface{}) (types.ZincFileInfo, error) { + data, err := json.Marshal(source) + if err != nil { + return types.ZincFileInfo{}, fmt.Errorf("序列化失败: %w", err) + } + + var info types.ZincFileInfo + if err := json.Unmarshal(data, &info); err != nil { + return types.ZincFileInfo{}, fmt.Errorf("反序列化失败: %w", err) + } + return info, nil +} diff --git a/app/auth/api/internal/logic/storage/upload_file_logic.go b/app/auth/api/internal/logic/storage/upload_file_logic.go index c8fea8a..b124349 100644 --- a/app/auth/api/internal/logic/storage/upload_file_logic.go +++ b/app/auth/api/internal/logic/storage/upload_file_logic.go @@ -76,6 +76,12 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) { return "", err } + // 解析上传配置信息 + settingResult, err := l.parseUploadSettingResult(r) + if err != nil { + return "", err + } + // 使用 `errgroup.Group` 处理并发任务 var ( faceId int64 @@ -86,22 +92,24 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) { // 创建信号量,限制最大并发上传数(比如最多同时 5 个任务) sem := semaphore.NewWeighted(5) - // 进行人脸识别 - g.Go(func() error { - if result.FileType == "image/png" || result.FileType == "image/jpeg" { - face, err := l.svcCtx.AiSvcRpc.FaceRecognition(l.ctx, &pb.FaceRecognitionRequest{ - Face: data, - UserId: uid, - }) - if err != nil { - return err + if settingResult.FaceDetection { + // 进行人脸识别 + g.Go(func() error { + if result.FileType == "image/png" || result.FileType == "image/jpeg" { + face, err := l.svcCtx.AiSvcRpc.FaceRecognition(l.ctx, &pb.FaceRecognitionRequest{ + Face: data, + UserId: uid, + }) + if err != nil { + return err + } + if face != nil { + faceId = face.GetFaceId() + } } - if face != nil { - faceId = face.GetFaceId() - } - } - return nil - }) + return nil + }) + } // 上传文件到 OSS g.Go(func() error { if err := sem.Acquire(ctx, 1); err != nil { @@ -133,12 +141,13 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) { } fileUploadMessage := &types.FileUploadMessage{ - UID: uid, - Result: result, - FaceID: faceId, - FileHeader: header, - FilePath: filePath, - ThumbPath: thumbPath, + UID: uid, + Result: result, + FaceID: faceId, + FileName: header.Filename, + FileSize: header.Size, + FilePath: filePath, + ThumbPath: thumbPath, } // 转换为 JSON messageData, err := json.Marshal(fileUploadMessage) @@ -189,6 +198,16 @@ func (l *UploadFileLogic) parseImageInfoResult(r *http.Request) (types.File, err return result, nil } +// 解析设置结果 +func (l *UploadFileLogic) parseUploadSettingResult(r *http.Request) (types.UploadSetting, error) { + formValue := r.PostFormValue("setting") + var result types.UploadSetting + if err := json.Unmarshal([]byte(formValue), &result); err != nil { + return result, errors.New("invalid result") + } + return result, nil +} + // 上传文件到 OSS func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHeader, file multipart.File, thumbnail multipart.File, result types.File) (string, string, error) { cacheKey := constant.UserOssConfigPrefix + uid + ":" + result.Provider @@ -232,43 +251,6 @@ func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHead return objectKey, thumbObjectKey, nil } -//func (l *UploadFileLogic) uploadFileToMinio(uid string, header *multipart.FileHeader, file multipart.File, result types.File) (string, error) { -// objectKey := path.Join( -// uid, -// time.Now().Format("2006/01"), // 按年/月划分目录 -// l.classifyFile(result.FileType, result.IsScreenshot), -// fmt.Sprintf("%s_%s%s", strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename)), kgo.SimpleUuid(), filepath.Ext(header.Filename)), -// ) -// 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 -// } -// } -// // 上传到MinIO -// _, err = l.svcCtx.MinioClient.PutObject( -// l.ctx, -// constant.ThumbnailBucketName, -// objectKey, -// file, -// int64(result.ThumbSize), -// minio.PutObjectOptions{ -// ContentType: result.FileType, -// }, -// ) -// if err != nil { -// return "", err -// } -// //reqParams := make(url.Values) -// //presignedURL, err := l.svcCtx.MinioClient.PresignedGetObject(l.ctx, constant.ThumbnailBucketName, objectKey, time.Hour*24*7, reqParams) -// //if err != nil { -// // return "", "", err -// //} -// return objectKey, nil -//} - // 提取解密操作为函数 func (l *UploadFileLogic) decryptConfig(dbConfig *model.ScaStorageConfig) (*config.StorageConfig, error) { accessKey, err := encrypt.Decrypt(dbConfig.AccessKey, l.svcCtx.Config.Encrypt.Key) diff --git a/app/auth/api/internal/mq/image_process_consumer.go b/app/auth/api/internal/mq/image_process_consumer.go index be28ff5..8196851 100644 --- a/app/auth/api/internal/mq/image_process_consumer.go +++ b/app/auth/api/internal/mq/image_process_consumer.go @@ -9,7 +9,7 @@ import ( "github.com/redis/go-redis/v9" "github.com/zeromicro/go-zero/core/logx" "gorm.io/gorm" - "mime/multipart" + "net/http" "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" @@ -19,6 +19,7 @@ import ( "schisandra-album-cloud-microservices/common/nsqx" "schisandra-album-cloud-microservices/common/storage/config" "strconv" + "time" ) type NsqImageProcessConsumer struct { @@ -60,7 +61,7 @@ func (c *NsqImageProcessConsumer) HandleMessage(msg *nsq.Message) error { } // 将文件信息存入数据库 - storageId, err := c.saveFileInfoToDB(message.UID, message.Result.Bucket, message.Result.Provider, message.FileHeader, message.Result, message.FaceID, message.FilePath, locationId, message.Result.AlbumId) + storageId, err := c.saveFileInfoToDB(message.UID, message.Result.Bucket, message.Result.Provider, message.FileName, message.FileSize, message.Result, message.FaceID, message.FilePath, locationId, message.Result.AlbumId) if err != nil { return err } @@ -71,6 +72,46 @@ func (c *NsqImageProcessConsumer) HandleMessage(msg *nsq.Message) error { // 删除缓存 c.afterImageUpload(message.UID) + zincFileInfo := types.ZincFileInfo{ + FaceID: message.FaceID, + FileName: message.FileName, + FileSize: message.FileSize, + UID: message.UID, + FilePath: message.FilePath, + ThumbPath: message.ThumbPath, + CreatedAt: time.Now().UTC(), + StorageId: storageId, + Provider: message.Result.Provider, + Bucket: message.Result.Bucket, + FileType: message.Result.FileType, + IsAnime: message.Result.IsAnime, + TagName: message.Result.TagName, + Landscape: message.Result.Landscape, + TopCategory: message.Result.TopCategory, + IsScreenshot: message.Result.IsScreenshot, + Width: message.Result.Width, + Height: message.Result.Height, + Longitude: message.Result.Longitude, + Latitude: message.Result.Latitude, + ThumbW: message.Result.ThumbW, + ThumbH: message.Result.ThumbH, + ThumbSize: message.Result.ThumbSize, + AlbumId: message.Result.AlbumId, + HasQrcode: message.Result.HasQrcode, + Country: country, + Province: province, + City: city, + } + + err = c.svcCtx.ZincClient.CreateFileUploadIndex(constant.ZincIndexNameStorageInfo) + if err != nil { + return err + } + _, err = c.insertFileInfoTOZinc(constant.ZincIndexNameStorageInfo, storageId, zincFileInfo) + if err != nil { + return err + } + return nil } @@ -225,7 +266,7 @@ func (c *NsqImageProcessConsumer) saveFileThumbnailInfoToDB(uid string, filePath } // 将 EXIF 和文件信息存入数据库 -func (c *NsqImageProcessConsumer) saveFileInfoToDB(uid, bucket, provider string, header *multipart.FileHeader, result types.File, faceId int64, filePath string, locationID, albumId int64) (int64, error) { +func (c *NsqImageProcessConsumer) saveFileInfoToDB(uid, bucket, provider string, fileName string, fileSize int64, result types.File, faceId int64, filePath string, locationID, albumId int64) (int64, error) { tx := c.svcCtx.DB.Begin() defer func() { if r := recover(); r != nil { @@ -238,8 +279,8 @@ func (c *NsqImageProcessConsumer) saveFileInfoToDB(uid, bucket, provider string, UserID: uid, Provider: provider, Bucket: bucket, - FileName: header.Filename, - FileSize: strconv.FormatInt(header.Size, 10), + FileName: fileName, + FileSize: strconv.FormatInt(fileSize, 10), FileType: result.FileType, Path: filePath, FaceID: faceId, @@ -261,6 +302,7 @@ func (c *NsqImageProcessConsumer) saveFileInfoToDB(uid, bucket, provider string, Tag: result.TagName, IsAnime: strconv.FormatBool(result.IsAnime), Category: result.TopCategory, + HasQrcode: strconv.FormatBool(result.HasQrcode), } err = tx.ScaStorageExtra.Create(scaStorageExtra) if err != nil { @@ -292,6 +334,22 @@ func (c *NsqImageProcessConsumer) afterImageUpload(uid string) { // 删除所有匹配的键 if err := c.svcCtx.RedisClient.Del(c.ctx, keys...).Err(); err != nil { logx.Errorf("删除缓存键 %s 失败: %v", keyPattern, err) - } } + +func (c *NsqImageProcessConsumer) insertFileInfoTOZinc(indexName string, docID int64, message types.ZincFileInfo) (int64, error) { + url := fmt.Sprintf("%s/api/%s/_doc/%v", c.svcCtx.ZincClient.BaseURL, indexName, docID) + resp, err := c.svcCtx.ZincClient.Client.R(). + SetBasicAuth(c.svcCtx.ZincClient.Username, c.svcCtx.ZincClient.Password). + SetHeader("Content-Type", "application/json"). + SetBody(message). + Put(url) + if err != nil { + return 0, fmt.Errorf("请求失败: %w", err) + } + + if resp.StatusCode() != http.StatusOK { + return 0, fmt.Errorf("插入失败 (状态码 %d): %s", resp.StatusCode(), resp.String()) + } + return docID, nil +} diff --git a/app/auth/api/internal/svc/service_context.go b/app/auth/api/internal/svc/service_context.go index b9401a7..24ca0f7 100644 --- a/app/auth/api/internal/svc/service_context.go +++ b/app/auth/api/internal/svc/service_context.go @@ -28,6 +28,7 @@ import ( "schisandra-album-cloud-microservices/common/storage" "schisandra-album-cloud-microservices/common/storage/manager" "schisandra-album-cloud-microservices/common/wechat_official" + "schisandra-album-cloud-microservices/common/zincx" ) type ServiceContext struct { @@ -48,6 +49,7 @@ type ServiceContext struct { MinioClient *minio.Client GeoRegionData *geo_json.RegionData NSQProducer *nsq.Producer + ZincClient *zincx.ZincClient } func NewServiceContext(c config.Config) *ServiceContext { @@ -72,6 +74,7 @@ func NewServiceContext(c config.Config) *ServiceContext { MinioClient: miniox.NewMinio(c.Minio.Endpoint, c.Minio.AccessKeyID, c.Minio.SecretAccessKey, c.Minio.UseSSL), GeoRegionData: geo_json.NewGeoJSON(), NSQProducer: nsqx.NewNsqProducer(c.NSQ.NSQDHost), + ZincClient: zincx.NewZincClient(c.Zinc.URL, c.Zinc.Username, c.Zinc.Password), } return serviceContext } diff --git a/app/auth/api/internal/types/file_types.go b/app/auth/api/internal/types/file_types.go index eae8359..33720d3 100644 --- a/app/auth/api/internal/types/file_types.go +++ b/app/auth/api/internal/types/file_types.go @@ -1,7 +1,6 @@ package types import ( - "mime/multipart" "time" ) @@ -23,18 +22,28 @@ type File struct { ThumbH float64 `json:"thumb_h"` ThumbSize float64 `json:"thumb_size"` AlbumId int64 `json:"albumId"` + HasQrcode bool `json:"hasQrcode"` +} + +type UploadSetting struct { + NsfwDetection bool `json:"nsfw_detection"` + AnimeDetection bool `json:"anime_detection"` + LandscapeDetection bool `json:"landscape_detection"` + ScreenshotDetection bool `json:"screenshot_detection"` + GpsDetection bool `json:"gps_detection"` + TargetDetection bool `json:"target_detection"` + QrcodeDetection bool `json:"qrcode_detection"` + FaceDetection bool `json:"face_detection"` } -// FileUploadMessage represents a message sent to the user after a file upload. type FileUploadMessage struct { - FaceID int64 `json:"face_id"` - FileHeader *multipart.FileHeader `json:"fileHeader"` - Result File `json:"result"` - UID string `json:"uid"` - FilePath string `json:"filePath"` - URL string `json:"url"` - ThumbPath string `json:"thumbPath"` - Thumbnail string `json:"thumbnail"` + FaceID int64 `json:"face_id"` + FileName string `json:"file_name"` + FileSize int64 `json:"file_size"` + Result File `json:"result"` + UID string `json:"uid"` + FilePath string `json:"file_path"` + ThumbPath string `json:"thumb_path"` } type FileInfoResult struct { @@ -74,3 +83,34 @@ type LocationInfo struct { CoverImage string `json:"cover_image"` Total int64 `json:"total"` } + +type ZincFileInfo struct { + FaceID int64 `json:"face_id"` + FileName string `json:"file_name"` + FileSize int64 `json:"file_size"` + UID string `json:"uid"` + FilePath string `json:"file_path"` + ThumbPath string `json:"thumb_path"` + CreatedAt time.Time `json:"created_at"` + StorageId int64 `json:"storage_id"` + Provider string `json:"provider"` + Bucket string `json:"bucket"` + FileType string `json:"file_type"` + IsAnime bool `json:"is_anime"` + TagName string `json:"tag_name"` + Landscape string `json:"landscape"` + TopCategory string `json:"top_category"` + IsScreenshot bool `json:"is_screenshot"` + Width float64 `json:"width"` + Height float64 `json:"height"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + ThumbW float64 `json:"thumb_w"` + ThumbH float64 `json:"thumb_h"` + ThumbSize float64 `json:"thumb_size"` + AlbumId int64 `json:"album_id"` + HasQrcode bool `json:"has_qrcode"` + Country string `json:"country"` + Province string `json:"province"` + City string `json:"city"` +} diff --git a/app/auth/api/internal/types/types.go b/app/auth/api/internal/types/types.go index 7659414..d7b1a9b 100644 --- a/app/auth/api/internal/types/types.go +++ b/app/auth/api/internal/types/types.go @@ -303,7 +303,8 @@ type QueryShareImageResponse struct { } type QueryShareInfoRequest struct { - InviteCode string `json:"invite_code"` + InviteCode string `json:"invite_code"` + AccessPassword string `json:"access_password,omitempty"` } type RecentListRequest struct { @@ -363,6 +364,18 @@ type RotateCaptchaResponse struct { Thumb string `json:"thumb"` } +type SearchImageRequest struct { + Type string `json:"type"` + Keyword string `json:"keyword"` + Provider string `json:"provider"` + Bucket string `json:"bucket"` + InputImage string `json:"input_image,omitempty"` +} + +type SearchImageResponse struct { + Records []AllImageDetail `json:"records"` +} + type ShareAlbumRequest struct { ID int64 `json:"id"` ExpireDate string `json:"expire_date"` @@ -402,6 +415,8 @@ type ShareInfoResponse struct { SharerAvatar string `json:"sharer_avatar"` SharerName string `json:"sharer_name"` AlbumName string `json:"album_name"` + InviteCode string `json:"invite_code"` + SharerUID string `json:"sharer_uid"` } type ShareOverviewResponse struct { diff --git a/app/auth/model/mysql/model/sca_storage_extra.gen.go b/app/auth/model/mysql/model/sca_storage_extra.gen.go index 2f37e31..6d706f6 100644 --- a/app/auth/model/mysql/model/sca_storage_extra.gen.go +++ b/app/auth/model/mysql/model/sca_storage_extra.gen.go @@ -22,6 +22,7 @@ type ScaStorageExtra struct { IsAnime string `gorm:"column:is_anime;type:varchar(50);comment:是否是动漫图片" json:"is_anime"` // 是否是动漫图片 Landscape string `gorm:"column:landscape;type:varchar(50);comment:风景类型" json:"landscape"` // 风景类型 Hash string `gorm:"column:hash;type:varchar(255);comment:哈希值" json:"hash"` // 哈希值 + HasQrcode string `gorm:"column:has_qrcode;type:varchar(50);comment:是否有二维码" json:"has_qrcode"` // 是否有二维码 CreatedAt time.Time `gorm:"column:created_at;type:timestamp;autoCreateTime;comment:创建时间" json:"created_at"` // 创建时间 UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;autoUpdateTime;comment:更新时间" json:"updated_at"` // 更新时间 DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp;comment:删除时间" json:"deleted_at"` // 删除时间 diff --git a/app/auth/model/mysql/query/sca_storage_extra.gen.go b/app/auth/model/mysql/query/sca_storage_extra.gen.go index 5c6271e..24cdafe 100644 --- a/app/auth/model/mysql/query/sca_storage_extra.gen.go +++ b/app/auth/model/mysql/query/sca_storage_extra.gen.go @@ -35,6 +35,7 @@ func newScaStorageExtra(db *gorm.DB, opts ...gen.DOOption) scaStorageExtra { _scaStorageExtra.IsAnime = field.NewString(tableName, "is_anime") _scaStorageExtra.Landscape = field.NewString(tableName, "landscape") _scaStorageExtra.Hash = field.NewString(tableName, "hash") + _scaStorageExtra.HasQrcode = field.NewString(tableName, "has_qrcode") _scaStorageExtra.CreatedAt = field.NewTime(tableName, "created_at") _scaStorageExtra.UpdatedAt = field.NewTime(tableName, "updated_at") _scaStorageExtra.DeletedAt = field.NewField(tableName, "deleted_at") @@ -57,6 +58,7 @@ type scaStorageExtra struct { IsAnime field.String // 是否是动漫图片 Landscape field.String // 风景类型 Hash field.String // 哈希值 + HasQrcode field.String // 是否有二维码 CreatedAt field.Time // 创建时间 UpdatedAt field.Time // 更新时间 DeletedAt field.Field // 删除时间 @@ -84,6 +86,7 @@ func (s *scaStorageExtra) updateTableName(table string) *scaStorageExtra { s.IsAnime = field.NewString(table, "is_anime") s.Landscape = field.NewString(table, "landscape") s.Hash = field.NewString(table, "hash") + s.HasQrcode = field.NewString(table, "has_qrcode") s.CreatedAt = field.NewTime(table, "created_at") s.UpdatedAt = field.NewTime(table, "updated_at") s.DeletedAt = field.NewField(table, "deleted_at") @@ -103,7 +106,7 @@ func (s *scaStorageExtra) GetFieldByName(fieldName string) (field.OrderExpr, boo } func (s *scaStorageExtra) fillFieldMap() { - s.fieldMap = make(map[string]field.Expr, 11) + s.fieldMap = make(map[string]field.Expr, 12) s.fieldMap["id"] = s.ID s.fieldMap["user_id"] = s.UserID s.fieldMap["info_id"] = s.InfoID @@ -112,6 +115,7 @@ func (s *scaStorageExtra) fillFieldMap() { s.fieldMap["is_anime"] = s.IsAnime s.fieldMap["landscape"] = s.Landscape s.fieldMap["hash"] = s.Hash + s.fieldMap["has_qrcode"] = s.HasQrcode s.fieldMap["created_at"] = s.CreatedAt s.fieldMap["updated_at"] = s.UpdatedAt s.fieldMap["deleted_at"] = s.DeletedAt diff --git a/common/constant/zinc_index_name.go b/common/constant/zinc_index_name.go new file mode 100644 index 0000000..f4e4b31 --- /dev/null +++ b/common/constant/zinc_index_name.go @@ -0,0 +1,5 @@ +package constant + +const ( + ZincIndexNameStorageInfo = "storage_info" +) diff --git a/common/zincx/special.go b/common/zincx/special.go new file mode 100644 index 0000000..4928557 --- /dev/null +++ b/common/zincx/special.go @@ -0,0 +1,78 @@ +package zincx + +import ( + "fmt" + "net/http" +) + +// 创建索引(幂等操作,若存在则跳过) +func (zc *ZincClient) CreateFileUploadIndex(indexName string) error { + exists, err := zc.IndexExists(indexName) + if err != nil { + return fmt.Errorf("检查索引失败: %w", err) + } + if exists { + return nil // 索引已存在则跳过 + } + + // 定义完整的索引映射 + mapping := map[string]interface{}{ + "mappings": map[string]interface{}{ + "properties": map[string]interface{}{ + // 基础信息 + "storage_id": map[string]string{"type": "numeric"}, + "face_id": map[string]string{"type": "numeric"}, + "file_name": map[string]string{"type": "keyword"}, + "file_size": map[string]string{"type": "numeric"}, + "uid": map[string]string{"type": "keyword"}, + "file_path": map[string]string{"type": "text"}, + "thumb_path": map[string]string{"type": "text"}, + "created_at": map[string]string{ + "type": "date"}, + + // 文件元数据 + "provider": map[string]string{"type": "keyword"}, + "bucket": map[string]string{"type": "keyword"}, + "file_type": map[string]string{"type": "keyword"}, + "is_anime": map[string]string{"type": "boolean"}, + "tag_name": map[string]string{"type": "keyword"}, + "landscape": map[string]string{"type": "keyword"}, + "top_category": map[string]string{"type": "keyword"}, + "is_screenshot": map[string]string{"type": "boolean"}, + + // 媒体属性 + "width": map[string]string{"type": "numeric"}, + "height": map[string]string{"type": "numeric"}, + "thumb_w": map[string]string{"type": "numeric"}, + "thumb_h": map[string]string{"type": "numeric"}, + "thumb_size": map[string]string{"type": "numeric"}, + + // 地理信息 + "longitude": map[string]string{"type": "numeric"}, + "latitude": map[string]string{"type": "numeric"}, + "country": map[string]string{"type": "keyword"}, + "province": map[string]string{"type": "keyword"}, + "city": map[string]string{"type": "keyword"}, + + // 其他 + "album_id": map[string]string{"type": "numeric"}, + "has_qrcode": map[string]string{"type": "boolean"}, + }, + }, + } + + resp, err := zc.Client.R(). + SetBasicAuth(zc.Username, zc.Password). + SetHeader("Content-Type", "application/json"). + SetBody(mapping). + Put(zc.BaseURL + "/api/index/" + indexName) + + if err != nil { + return fmt.Errorf("创建索引请求失败: %w", err) + } + + if resp.StatusCode() != http.StatusOK { + return fmt.Errorf("创建索引失败 (状态码 %d): %s", resp.StatusCode(), resp.String()) + } + return nil +} diff --git a/common/zincx/zincx.go b/common/zincx/zincx.go new file mode 100644 index 0000000..56dfce7 --- /dev/null +++ b/common/zincx/zincx.go @@ -0,0 +1,201 @@ +package zincx + +import ( + "encoding/json" + "fmt" + "github.com/go-resty/resty/v2" + "net/http" +) + +type ZincClient struct { + Client *resty.Client + BaseURL string + Username string + Password string +} + +func NewZincClient(BaseURL, Username, Password string) *ZincClient { + Client := resty.New(). + SetDebug(false). + SetDisableWarn(true) + return &ZincClient{ + Client: Client, + BaseURL: BaseURL, + Username: Username, + Password: Password, + } +} + +// 检查索引是否存在 (内部方法) +func (zc *ZincClient) IndexExists(indexName string) (bool, error) { + resp, err := zc.Client.R(). + SetBasicAuth(zc.Username, zc.Password). + Head(zc.BaseURL + "/api/index/" + indexName) + + if err != nil { + return false, err + } + + return resp.StatusCode() == http.StatusOK, nil +} + +type IndexMapping struct { + Mappings struct { + Properties map[string]interface{} `json:"properties"` + } `json:"mappings"` +} + +func (zc *ZincClient) CreateIndex(indexName string, mapping *IndexMapping) error { + url := fmt.Sprintf("%s/api/index/%s", zc.BaseURL, indexName) + resp, err := zc.Client.R(). + SetBasicAuth(zc.Username, zc.Password). + SetHeader("Content-Type", "application/json"). + SetBody(mapping). + Put(url) + + if err != nil { + return err + } + + if resp.StatusCode() != 200 { + return fmt.Errorf("创建索引失败: %s", resp.String()) + } + return nil +} + +func (zc *ZincClient) IndexDocument(indexName, documentID string, doc interface{}) error { + url := fmt.Sprintf("%s/api/%s/_doc/%s", zc.BaseURL, indexName, documentID) + resp, err := zc.Client.R(). + SetBasicAuth(zc.Username, zc.Password). + SetHeader("Content-Type", "application/json"). + SetBody(doc). + Put(url) + + if err != nil { + return err + } + + if resp.StatusCode() != 200 { + return fmt.Errorf("插入文档失败: %s", resp.String()) + } + return nil +} + +type SearchRequest struct { + Query struct { + Match map[string]interface{} `json:"match"` + } `json:"query"` +} + +// 泛型响应结构 +type ZincSearchResponse[T any] struct { + Hits struct { + Total struct { + Value int `json:"value"` + } `json:"total"` + Hits []struct { + ID string `json:"_id"` + Source T `json:"_source"` + Score float64 `json:"_score"` + } `json:"hits"` + } `json:"hits"` +} + +// 修改 Search 方法签名 +func (zc *ZincClient) Search(indexName string, query interface{}, resultType interface{}) (*ZincSearchResponse[interface{}], error) { + url := fmt.Sprintf("%s/api/%s/_search", zc.BaseURL, indexName) + resp, err := zc.Client.R(). + SetBasicAuth(zc.Username, zc.Password). + SetHeader("Content-Type", "application/json"). + SetBody(query). + SetResult(&ZincSearchResponse[interface{}]{}). + Post(url) + + if err != nil { + return nil, err + } + + if resp.StatusCode() != 200 { + return nil, fmt.Errorf("搜索失败: %s", resp.String()) + } + + // 手动反序列化以支持动态类型 + var rawResponse ZincSearchResponse[interface{}] + if err := json.Unmarshal(resp.Body(), &rawResponse); err != nil { + return nil, err + } + + // 将 Source 转换为目标类型 + for i := range rawResponse.Hits.Hits { + data, _ := json.Marshal(rawResponse.Hits.Hits[i].Source) + _ = json.Unmarshal(data, &resultType) + rawResponse.Hits.Hits[i].Source = resultType + } + + return &rawResponse, nil +} + +type BulkRequest struct { + Index string `json:"index"` + ID string `json:"id"` + Source interface{} `json:"source"` +} + +func (zc *ZincClient) BulkIndex(indexName string, docs []BulkRequest) error { + url := fmt.Sprintf("%s/api/_bulk", zc.BaseURL) + body := "" + for _, doc := range docs { + action := fmt.Sprintf(`{ "index": { "_index": "%s", "_id": "%s" } }`, indexName, doc.ID) + source, _ := json.Marshal(doc.Source) + body += action + "\n" + string(source) + "\n" + } + + resp, err := zc.Client.R(). + SetBasicAuth(zc.Username, zc.Password). + SetHeader("Content-Type", "application/json"). + SetBody(body). + Post(url) + + if err != nil { + return err + } + + if resp.StatusCode() != 200 { + return fmt.Errorf("批量操作失败: %s", resp.String()) + } + return nil +} + +// 删除文档 +func (zc *ZincClient) DeleteDocument(indexName, documentID string) error { + url := fmt.Sprintf("%s/api/%s/_doc/%s", zc.BaseURL, indexName, documentID) + resp, err := zc.Client.R(). + SetBasicAuth(zc.Username, zc.Password). + Delete(url) + + if err != nil { + return err + } + + if resp.StatusCode() != 200 { + return fmt.Errorf("删除文档失败: %s", resp.String()) + } + return nil +} + +// 删除索引 +func (zc *ZincClient) DeleteIndex(indexName string) error { + url := fmt.Sprintf("%s/api/index/%s", zc.BaseURL, indexName) + resp, err := zc.Client.R(). + SetBasicAuth(zc.Username, zc.Password). + Delete(url) + + if err != nil { + return err + } + + if resp.StatusCode() != 200 { + return fmt.Errorf("删除索引失败: %s", resp.String()) + } + return nil +} diff --git a/go.mod b/go.mod index 4edf80e..8c8251f 100644 --- a/go.mod +++ b/go.mod @@ -77,6 +77,7 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/assert/v2 v2.2.0 // indirect + github.com/go-resty/resty/v2 v2.16.5 // indirect github.com/go-sql-driver/mysql v1.9.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/goccy/go-json v0.10.5 // indirect diff --git a/go.sum b/go.sum index ed3bbb8..b6c18a6 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,10 @@ github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvY github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= -github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/elastic/elastic-transport-go/v8 v8.6.1 h1:h2jQRqH6eLGiBSN4eZbQnJLtL4bC5b4lfVFRjw2R4e4= +github.com/elastic/elastic-transport-go/v8 v8.6.1/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk= +github.com/elastic/go-elasticsearch/v8 v8.17.1 h1:bOXChDoCMB4TIwwGqKd031U8OXssmWLT3UrAr9EGs3Q= +github.com/elastic/go-elasticsearch/v8 v8.17.1/go.mod h1:MVJCtL+gJJ7x5jFeUmA20O7rvipX8GcQmo5iBcmaJn4= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -105,7 +107,6 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= -github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -121,12 +122,11 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= @@ -160,8 +160,6 @@ github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcb github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -181,7 +179,6 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= @@ -222,13 +219,9 @@ github.com/karlseguin/ccache/v3 v3.0.6/go.mod h1:b0qfdUOHl4vJgKFQN41paXIdBb3acAt github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -264,10 +257,6 @@ github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.85 h1:9psTLS/NTvC3MWoyjhjXpwcKoNbkongaCSF3PNpSuXo= -github.com/minio/minio-go/v7 v7.0.85/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY= -github.com/minio/minio-go/v7 v7.0.86 h1:DcgQ0AUjLJzRH6y/HrxiZ8CXarA70PAIufXHodP4s+k= -github.com/minio/minio-go/v7 v7.0.86/go.mod h1:VbfO4hYwUu3Of9WqGLBZ8vl3Hxnxo4ngxK4hzQDf4x4= github.com/minio/minio-go/v7 v7.0.87 h1:nkr9x0u53PespfxfUqxP3UYWiE2a41gaofgNnC4Y8WQ= github.com/minio/minio-go/v7 v7.0.87/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -325,8 +314,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= @@ -337,12 +324,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94= -github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s= github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc= github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= @@ -374,17 +357,11 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= -github.com/tencentyun/cos-go-sdk-v5 v0.7.61 h1:tKNIjvsezkdtajqE887XAw1VL8Pq1HNtpc7rfgz25lA= -github.com/tencentyun/cos-go-sdk-v5 v0.7.61/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= github.com/tencentyun/cos-go-sdk-v5 v0.7.62 h1:7SZVCc31rkvMxod8nwvG1Ko0N5npT39/s3NhpHBvs70= github.com/tencentyun/cos-go-sdk-v5 v0.7.62/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/wenlng/go-captcha-assets v1.0.1 h1:AdjRFMKmadPRWRTv0XEYfjDvcaayZ2yExITDvlK/7bk= -github.com/wenlng/go-captcha-assets v1.0.1/go.mod h1:yQqc7rRbxgLCg+tWtVp+7Y317D1wIZDan/yIwt8wSac= github.com/wenlng/go-captcha-assets v1.0.5 h1:TL+31Qe/kJwcuYyU+jHedjSTZnMu1XKgktKL++lH9Js= github.com/wenlng/go-captcha-assets v1.0.5/go.mod h1:zinRACsdYcL/S6pHgI9Iv7FKTU41d00+43pNX+b9+MM= -github.com/wenlng/go-captcha/v2 v2.0.2 h1:8twz6pI6xZwPvEGFezoFX395oFso1MuOlJt/tLiv7pk= -github.com/wenlng/go-captcha/v2 v2.0.2/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34= github.com/wenlng/go-captcha/v2 v2.0.3 h1:QTZ39/gVDisPSgvL9O2X2HbTuj5P/z8QsdGB/aayg9c= github.com/wenlng/go-captcha/v2 v2.0.3/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -414,8 +391,6 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.18/go.mod h1:BxVf2o5wXG9ZJV+/Cu7QNUiJYk4A29sA go.etcd.io/etcd/client/v3 v3.5.18 h1:nvvYmNHGumkDjZhTHgVU36A9pykGa2K4lAJ0yY7hcXA= go.etcd.io/etcd/client/v3 v3.5.18/go.mod h1:kmemwOsPU9broExyhYsBxX4spCTDX3yLgPMWtpBXG6E= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= -go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= -go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -468,16 +443,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA= -golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= -golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg= -golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= @@ -511,8 +478,6 @@ golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= -golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -587,16 +552,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20250212204824-5a70512c5d8b h1:i+d0RZa8Hs2L/MuaOQYI+krthcxdEbEM2N+Tf3kJ4zk= -google.golang.org/genproto/googleapis/api v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 h1:35ZFtrCgaAjF7AFAK0+lRSf+4AyYnWRbH7og13p7rZ4= -google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:W9ynFDP/shebLB1Hl/ESTOap2jHd6pmLXPNZC7SVDbA= google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99 h1:ilJhrCga0AptpJZXmUYG4MCrx/zf3l1okuYz7YK9PPw= google.golang.org/genproto/googleapis/api v0.0.0-20250224174004-546df14abb99/go.mod h1:Xsh8gBVxGCcbV8ZeTB9wI5XPyZ5RvC6V3CTeeplHbiA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99 h1:ZSlhAUqC4r8TPzqLXQ0m3upBNZeF+Y8jQ3c4CR3Ujms= google.golang.org/genproto/googleapis/rpc v0.0.0-20250224174004-546df14abb99/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= @@ -677,8 +634,6 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.35.0 h1:yQps4fegMnZFdphtzlfQTCNBWtS0CZv48pRpW3RFHRw= -modernc.org/sqlite v1.35.0/go.mod h1:9cr2sicr7jIaWTBKQmAxQLfBv9LL0su4ZTEV+utt3ic= modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8= modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=