add search function

This commit is contained in:
2025-03-03 00:59:52 +08:00
parent 58c58546d2
commit 4d0f628586
29 changed files with 982 additions and 142 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -77,4 +77,9 @@ type Config struct {
NSQDHost string
LookUpdHost string
}
Zinc struct {
URL string
Username string
Password string
}
}

View File

@@ -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)
}
}

View File

@@ -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",

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 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)
}
}
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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 {

View File

@@ -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"` // 删除时间

View File

@@ -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

View File

@@ -0,0 +1,5 @@
package constant
const (
ZincIndexNameStorageInfo = "storage_info"
)

78
common/zincx/special.go Normal file
View File

@@ -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
}

201
common/zincx/zincx.go Normal file
View File

@@ -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
}

1
go.mod
View File

@@ -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

57
go.sum
View File

@@ -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=