optimized image list interface

This commit is contained in:
2025-02-17 11:21:38 +08:00
parent ab4e9c4d59
commit b196e50aee
72 changed files with 1676 additions and 343 deletions

View File

@@ -53,12 +53,9 @@ func (l *FaceRecognitionLogic) FaceRecognition(in *pb.FaceRecognitionRequest) (*
}
// 提取人脸特征
faceFeatures, err := l.svcCtx.FaceRecognizer.RecognizeSingle(toJPEG)
if err != nil {
if err != nil || faceFeatures == nil {
return nil, err
}
if faceFeatures == nil {
return nil, nil
}
hashKey := constant.FaceVectorPrefix + in.GetUserId()
// 从 Redis 加载人脸数据

View File

@@ -520,6 +520,7 @@ type (
Width float64 `json:"width"`
Height float64 `json:"height"`
CreatedAt string `json:"created_at"`
Thumbnail string `json:"thumbnail"`
}
AllImageDetail {
Date string `json:"date"`
@@ -588,6 +589,10 @@ type (
ThingDetailListResponse {
records []AllImageDetail `json:"records"`
}
// 单张图片请求参数
SingleImageRequest {
ID int64 `json:"id"`
}
)
// 文件上传
@@ -670,5 +675,9 @@ service auth {
// 获取事物详情列表
@handler queryThingDetailList
post /image/thing/detail/list (ThingDetailListRequest) returns (ThingDetailListResponse)
// 获取单张图片连接
@handler getImageUrl
post /image/url/single (SingleImageRequest) returns (string)
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/rest"
"schisandra-album-cloud-microservices/app/auth/api/internal/mq"
"schisandra-album-cloud-microservices/common/idgenerator"
"schisandra-album-cloud-microservices/common/middleware"
@@ -31,6 +32,8 @@ func main() {
server.Use(middleware.I18nMiddleware)
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
// start image process consumer
go mq.NewImageProcessConsumer(ctx)
// initialize id generator
idgenerator.NewIDGenerator(0)
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)

View File

@@ -171,4 +171,9 @@ Minio:
# Minio 访问密钥
SecretAccessKey: XEHkwExqQdAlEPfpRk36xpc0Sie8hZkcmlhXQJXw
# Minio 使用SSL
UseSSL: false
UseSSL: false
#NSQ配置
NSQ:
# NSQD地址
NSQDHost: 1.95.0.111:4150
LookUpdHost: 1.95.0.111:4161

View File

@@ -73,4 +73,8 @@ type Config struct {
SecretAccessKey string
UseSSL bool
}
NSQ struct {
NSQDHost string
LookUpdHost string
}
}

View File

@@ -268,6 +268,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/image/thing/list",
Handler: storage.QueryThingImageListHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/image/url/single",
Handler: storage.GetImageUrlHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/uploads",

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 GetImageUrlHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SingleImageRequest
if err := httpx.Parse(r, &req); err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
return
}
l := storage.NewGetImageUrlLogic(r.Context(), svcCtx)
resp, err := l.GetImageUrl(&req)
if err != nil {
xhttp.JsonBaseResponseCtx(r.Context(), w, err)
} else {
xhttp.JsonBaseResponseCtx(r.Context(), w, resp)
}
}
}

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/redis/go-redis/v9"
"math/rand"
"net/url"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/app/auth/model/mysql/query"
"schisandra-album-cloud-microservices/common/constant"
@@ -56,18 +57,28 @@ func (l *GetAlbumDetailLogic) GetAlbumDetail(req *types.AlbumDetailListRequest)
return nil, errors.New("get cached image list failed")
}
// 缓存未命中,从数据库中查询
storageInfo := l.svcCtx.DB.ScaStorageInfo
storageThumb := l.svcCtx.DB.ScaStorageThumb
// 数据库查询文件信息列表
var storageInfoQuery query.IScaStorageInfoDo
var storageInfoList []types.FileInfoResult
storageInfoQuery = storageInfo.Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.AlbumID.Eq(req.ID)).
storageInfoQuery = storageInfo.Select(
storageInfo.ID,
storageInfo.FileName,
storageInfo.CreatedAt,
storageThumb.ThumbPath,
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).
Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.AlbumID.Eq(req.ID)).
Order(storageInfo.CreatedAt.Desc())
storageInfoList, err := storageInfoQuery.Find()
err = storageInfoQuery.Scan(&storageInfoList)
if err != nil {
return nil, err
}
@@ -75,17 +86,17 @@ func (l *GetAlbumDetailLogic) GetAlbumDetail(req *types.AlbumDetailListRequest)
return &types.AlbumDetailListResponse{}, nil
}
// 加载用户oss配置信息
cacheOssConfigKey := constant.UserOssConfigPrefix + uid + ":" + req.Provider
ossConfig, err := l.getOssConfigFromCacheOrDb(cacheOssConfigKey, uid, req.Provider)
if err != nil {
return nil, err
}
service, err := l.svcCtx.StorageManager.GetStorage(uid, ossConfig)
if err != nil {
return nil, errors.New("get storage failed")
}
//// 加载用户oss配置信息
//cacheOssConfigKey := constant.UserOssConfigPrefix + uid + ":" + req.Provider
//ossConfig, err := l.getOssConfigFromCacheOrDb(cacheOssConfigKey, uid, req.Provider)
//if err != nil {
// return nil, err
//}
//
//service, err := l.svcCtx.StorageManager.GetStorage(uid, ossConfig)
//if err != nil {
// return nil, errors.New("get storage failed")
//}
// 按日期进行分组
var wg sync.WaitGroup
@@ -93,11 +104,12 @@ func (l *GetAlbumDetailLogic) GetAlbumDetail(req *types.AlbumDetailListRequest)
for _, dbFileInfo := range storageInfoList {
wg.Add(1)
go func(dbFileInfo *model.ScaStorageInfo) {
go func(dbFileInfo *types.FileInfoResult) {
defer wg.Done()
weekday := WeekdayMap[dbFileInfo.CreatedAt.Weekday()]
date := dbFileInfo.CreatedAt.Format("2006年1月2日 星期" + weekday)
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, dbFileInfo.Path, time.Hour*24*7)
reqParams := make(url.Values)
presignedUrl, err := l.svcCtx.MinioClient.PresignedGetObject(l.ctx, constant.ThumbnailBucketName, dbFileInfo.ThumbPath, time.Hour*24*7, reqParams)
if err != nil {
logx.Error(err)
return
@@ -109,15 +121,15 @@ func (l *GetAlbumDetailLogic) GetAlbumDetail(req *types.AlbumDetailListRequest)
images = append(images, types.ImageMeta{
ID: dbFileInfo.ID,
FileName: dbFileInfo.FileName,
URL: url,
Width: dbFileInfo.Width,
Height: dbFileInfo.Height,
URL: presignedUrl.String(),
Width: dbFileInfo.ThumbW,
Height: dbFileInfo.ThumbH,
CreatedAt: dbFileInfo.CreatedAt.Format("2006-01-02 15:04:05"),
})
// 重新存储更新后的图像列表
groupedImages.Store(date, images)
}(dbFileInfo)
}(&dbFileInfo)
}
wg.Wait()
var imageList []types.AllImageDetail

View File

@@ -7,8 +7,8 @@ import (
"fmt"
"github.com/redis/go-redis/v9"
"math/rand"
"net/url"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/app/auth/model/mysql/query"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
@@ -58,16 +58,24 @@ func (l *GetFaceDetailListLogic) GetFaceDetailList(req *types.FaceDetailListRequ
// 缓存未命中,从数据库中查询
storageInfo := l.svcCtx.DB.ScaStorageInfo
// 数据库查询文件信息列表
var storageInfoQuery query.IScaStorageInfoDo
storageThumb := l.svcCtx.DB.ScaStorageThumb
var storageInfoList []types.FileInfoResult
storageInfoQuery = storageInfo.Where(
err = storageInfo.Select(
storageInfo.ID,
storageInfo.FileName,
storageInfo.CreatedAt,
storageInfo.Path,
storageThumb.ThumbPath,
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.FaceID.Eq(req.FaceID)).
Order(storageInfo.CreatedAt.Desc())
storageInfoList, err := storageInfoQuery.Find()
Order(storageInfo.CreatedAt.Desc()).Scan(&storageInfoList)
if err != nil {
return nil, err
}
@@ -93,15 +101,22 @@ func (l *GetFaceDetailListLogic) GetFaceDetailList(req *types.FaceDetailListRequ
for _, dbFileInfo := range storageInfoList {
wg.Add(1)
go func(dbFileInfo *model.ScaStorageInfo) {
go func(dbFileInfo *types.FileInfoResult) {
defer wg.Done()
weekday := WeekdayMap[dbFileInfo.CreatedAt.Weekday()]
date := dbFileInfo.CreatedAt.Format("2006年1月2日 星期" + weekday)
reqParams := make(url.Values)
presignedUrl, err := l.svcCtx.MinioClient.PresignedGetObject(l.ctx, constant.ThumbnailBucketName, dbFileInfo.ThumbPath, time.Hour*24*7, reqParams)
if err != nil {
logx.Error(err)
return
}
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, dbFileInfo.Path, time.Hour*24*7)
if err != nil {
logx.Error(err)
return
}
// 使用 Load 或 Store 确保原子操作
value, _ := groupedImages.LoadOrStore(date, []types.ImageMeta{})
images := value.([]types.ImageMeta)
@@ -109,15 +124,16 @@ func (l *GetFaceDetailListLogic) GetFaceDetailList(req *types.FaceDetailListRequ
images = append(images, types.ImageMeta{
ID: dbFileInfo.ID,
FileName: dbFileInfo.FileName,
Thumbnail: presignedUrl.String(),
URL: url,
Width: dbFileInfo.Width,
Height: dbFileInfo.Height,
Width: dbFileInfo.ThumbW,
Height: dbFileInfo.ThumbH,
CreatedAt: dbFileInfo.CreatedAt.Format("2006-01-02 15:04:05"),
})
// 重新存储更新后的图像列表
groupedImages.Store(date, images)
}(dbFileInfo)
}(&dbFileInfo)
}
wg.Wait()
var imageList []types.AllImageDetail

View File

@@ -0,0 +1,148 @@
package storage
import (
"context"
"encoding/json"
"errors"
"github.com/redis/go-redis/v9"
"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"
"strconv"
"time"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetImageUrlLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetImageUrlLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetImageUrlLogic {
return &GetImageUrlLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetImageUrlLogic) GetImageUrl(req *types.SingleImageRequest) (resp string, err error) {
uid, ok := l.ctx.Value("user_id").(string)
if !ok {
return "", errors.New("user_id not found")
}
// 从redis 获取url
cacheKey := constant.ImageSinglePrefix + uid + ":" + strconv.FormatInt(req.ID, 10)
cacheUrl, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err != nil && !errors.Is(err, redis.Nil) {
logx.Info(err)
return "", errors.New("get image url failed")
}
if cacheUrl != "" {
return cacheUrl, nil
}
storageInfo := l.svcCtx.DB.ScaStorageInfo
result, err := storageInfo.Select(
storageInfo.ID,
storageInfo.Provider,
storageInfo.Bucket,
storageInfo.Path).
Where(storageInfo.ID.Eq(req.ID), storageInfo.UserID.Eq(uid)).
First()
if err != nil {
logx.Info(err)
return "", errors.New("get storage info failed")
}
if result == nil {
return "", errors.New("get storage info failed")
}
// 加载用户oss配置信息
cacheOssConfigKey := constant.UserOssConfigPrefix + uid + ":" + result.Provider
ossConfig, err := l.getOssConfigFromCacheOrDb(cacheOssConfigKey, uid, result.Provider)
if err != nil {
return "", err
}
service, err := l.svcCtx.StorageManager.GetStorage(uid, ossConfig)
if err != nil {
return "", errors.New("get storage failed")
}
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, result.Path, 7*24*time.Hour)
if err != nil {
return "", errors.New("get presigned url failed")
}
// 缓存url
err = l.svcCtx.RedisClient.Set(l.ctx, cacheKey, url, 7*24*time.Hour).Err()
if err != nil {
logx.Info(err)
}
return url, nil
}
// 提取解密操作为函数
func (l *GetImageUrlLogic) 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 *GetImageUrlLogic) getOssConfigFromCacheOrDb(cacheKey, uid, provider string) (*storageConfig.StorageConfig, error) {
result, err := l.svcCtx.RedisClient.Get(l.ctx, cacheKey).Result()
if err != nil && !errors.Is(err, redis.Nil) {
return nil, errors.New("get oss config failed")
}
var ossConfig *storageConfig.StorageConfig
if result != "" {
var redisOssConfig model.ScaStorageConfig
if err = json.Unmarshal([]byte(result), &redisOssConfig); err != nil {
return nil, errors.New("unmarshal oss config failed")
}
return l.decryptConfig(&redisOssConfig)
}
// 缓存未命中,从数据库中加载
scaOssConfig := l.svcCtx.DB.ScaStorageConfig
dbOssConfig, err := scaOssConfig.Where(scaOssConfig.UserID.Eq(uid), scaOssConfig.Provider.Eq(provider)).First()
if err != nil {
return nil, err
}
// 缓存数据库配置
ossConfig, err = l.decryptConfig(dbOssConfig)
if err != nil {
return nil, err
}
marshalData, err := json.Marshal(dbOssConfig)
if err != nil {
return nil, errors.New("marshal oss config failed")
}
err = l.svcCtx.RedisClient.Set(l.ctx, cacheKey, marshalData, 0).Err()
if err != nil {
return nil, errors.New("set oss config failed")
}
return ossConfig, nil
}

View File

@@ -8,10 +8,10 @@ import (
"github.com/redis/go-redis/v9"
"github.com/zeromicro/go-zero/core/logx"
"math/rand"
"net/url"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/app/auth/model/mysql/query"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
storageConfig "schisandra-album-cloud-microservices/common/storage/config"
@@ -66,25 +66,44 @@ func (l *QueryAllImageListLogic) QueryAllImageList(req *types.AllImageListReques
// 缓存未命中,从数据库中查询
storageInfo := l.svcCtx.DB.ScaStorageInfo
// 数据库查询文件信息列表
var storageInfoQuery query.IScaStorageInfoDo
storageThumb := l.svcCtx.DB.ScaStorageThumb
var storageInfoList []types.FileInfoResult
if req.Sort {
storageInfoQuery = storageInfo.Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.Type.Eq(req.Type),
storageInfo.AlbumID.IsNull()).
Order(storageInfo.CreatedAt.Desc())
err = storageInfo.Select(
storageInfo.ID,
storageInfo.FileName,
storageInfo.CreatedAt,
storageInfo.Path,
storageThumb.ThumbPath,
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).
Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.Type.Eq(req.Type),
storageInfo.AlbumID.IsNull()).
Order(storageInfo.CreatedAt.Desc()).Scan(&storageInfoList)
} else {
storageInfoQuery = storageInfo.Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.Type.Eq(req.Type)).
Order(storageInfo.CreatedAt.Desc())
err = storageInfo.Select(
storageInfo.ID,
storageInfo.FileName,
storageInfo.CreatedAt,
storageThumb.ThumbPath,
storageInfo.Path,
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).
Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.Type.Eq(req.Type)).
Order(storageInfo.CreatedAt.Desc()).Scan(&storageInfoList)
}
storageInfoList, err := storageInfoQuery.Find()
if err != nil {
return nil, err
}
@@ -110,10 +129,16 @@ func (l *QueryAllImageListLogic) QueryAllImageList(req *types.AllImageListReques
for _, dbFileInfo := range storageInfoList {
wg.Add(1)
go func(dbFileInfo *model.ScaStorageInfo) {
go func(dbFileInfo *types.FileInfoResult) {
defer wg.Done()
weekday := WeekdayMap[dbFileInfo.CreatedAt.Weekday()]
date := dbFileInfo.CreatedAt.Format("2006年1月2日 星期" + weekday)
reqParams := make(url.Values)
presignedUrl, err := l.svcCtx.MinioClient.PresignedGetObject(l.ctx, constant.ThumbnailBucketName, dbFileInfo.ThumbPath, time.Hour*24*7, reqParams)
if err != nil {
logx.Error(err)
return
}
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, dbFileInfo.Path, time.Hour*24*7)
if err != nil {
logx.Error(err)
@@ -126,15 +151,16 @@ func (l *QueryAllImageListLogic) QueryAllImageList(req *types.AllImageListReques
images = append(images, types.ImageMeta{
ID: dbFileInfo.ID,
FileName: dbFileInfo.FileName,
Thumbnail: presignedUrl.String(),
URL: url,
Width: dbFileInfo.Width,
Height: dbFileInfo.Height,
Width: dbFileInfo.ThumbW,
Height: dbFileInfo.ThumbH,
CreatedAt: dbFileInfo.CreatedAt.Format("2006-01-02 15:04:05"),
})
// 重新存储更新后的图像列表
groupedImages.Store(date, images)
}(dbFileInfo)
}(&dbFileInfo)
}
wg.Wait()
var imageList []types.AllImageDetail

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/redis/go-redis/v9"
"math/rand"
"net/url"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/app/auth/model/mysql/query"
"schisandra-album-cloud-microservices/common/constant"
@@ -56,18 +57,28 @@ func (l *QueryLocationDetailListLogic) QueryLocationDetailList(req *types.Locati
return nil, errors.New("get cached image list failed")
}
// 缓存未命中,从数据库中查询
storageInfo := l.svcCtx.DB.ScaStorageInfo
storageThumb := l.svcCtx.DB.ScaStorageThumb
// 数据库查询文件信息列表
var storageInfoQuery query.IScaStorageInfoDo
var storageInfoList []types.FileInfoResult
storageInfoQuery = storageInfo.Where(
storageInfoQuery = storageInfo.Select(
storageInfo.ID,
storageInfo.FileName,
storageInfo.CreatedAt,
storageThumb.ThumbPath,
storageInfo.Path,
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.LocationID.Eq(req.ID)).
Order(storageInfo.CreatedAt.Desc())
storageInfoList, err := storageInfoQuery.Find()
err = storageInfoQuery.Scan(&storageInfoList)
if err != nil {
return nil, err
}
@@ -93,10 +104,16 @@ func (l *QueryLocationDetailListLogic) QueryLocationDetailList(req *types.Locati
for _, dbFileInfo := range storageInfoList {
wg.Add(1)
go func(dbFileInfo *model.ScaStorageInfo) {
go func(dbFileInfo *types.FileInfoResult) {
defer wg.Done()
weekday := WeekdayMap[dbFileInfo.CreatedAt.Weekday()]
date := dbFileInfo.CreatedAt.Format("2006年1月2日 星期" + weekday)
reqParams := make(url.Values)
presignedUrl, err := l.svcCtx.MinioClient.PresignedGetObject(l.ctx, constant.ThumbnailBucketName, dbFileInfo.ThumbPath, time.Hour*24*7, reqParams)
if err != nil {
logx.Error(err)
return
}
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, dbFileInfo.Path, time.Hour*24*7)
if err != nil {
logx.Error(err)
@@ -109,15 +126,16 @@ func (l *QueryLocationDetailListLogic) QueryLocationDetailList(req *types.Locati
images = append(images, types.ImageMeta{
ID: dbFileInfo.ID,
FileName: dbFileInfo.FileName,
Thumbnail: presignedUrl.String(),
URL: url,
Width: dbFileInfo.Width,
Height: dbFileInfo.Height,
Width: dbFileInfo.ThumbW,
Height: dbFileInfo.ThumbH,
CreatedAt: dbFileInfo.CreatedAt.Format("2006-01-02 15:04:05"),
})
// 重新存储更新后的图像列表
groupedImages.Store(date, images)
}(dbFileInfo)
}(&dbFileInfo)
}
wg.Wait()
var imageList []types.AllImageDetail

View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"github.com/redis/go-redis/v9"
"net/url"
"schisandra-album-cloud-microservices/app/auth/api/internal/svc"
"schisandra-album-cloud-microservices/app/auth/api/internal/types"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
@@ -53,16 +54,16 @@ func (l *QueryLocationImageListLogic) QueryLocationImageList(req *types.Location
}
// 加载用户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")
}
//cacheOssConfigKey := constant.UserOssConfigPrefix + uid + ":" + req.Provider
//ossConfig, err := l.getOssConfigFromCacheOrDb(cacheOssConfigKey, uid, req.Provider)
//if err != nil {
// return nil, err
//}
//
//service, err := l.svcCtx.StorageManager.GetStorage(uid, ossConfig)
//if err != nil {
// return nil, errors.New("get storage failed")
//}
locationMap := make(map[string][]types.LocationMeta)
for _, loc := range locations {
@@ -77,7 +78,8 @@ func (l *QueryLocationImageListLogic) QueryLocationImageList(req *types.Location
if city == "" {
city = loc.Country
}
url, err := service.PresignedURL(l.ctx, req.Bucket, loc.CoverImage, 7*24*time.Hour)
reqParams := make(url.Values)
presignedUrl, err := l.svcCtx.MinioClient.PresignedGetObject(l.ctx, constant.ThumbnailBucketName, loc.CoverImage, 7*24*time.Hour, reqParams)
if err != nil {
return nil, errors.New("get presigned url failed")
}
@@ -85,7 +87,7 @@ func (l *QueryLocationImageListLogic) QueryLocationImageList(req *types.Location
ID: loc.ID,
City: city,
Total: loc.Total,
CoverImage: url,
CoverImage: presignedUrl.String(),
}
locationMap[locationKey] = append(locationMap[locationKey], locationMeta)
}

View File

@@ -62,37 +62,34 @@ func (l *QueryRecentImageListLogic) QueryRecentImageList() (resp *types.RecentLi
if cmd == nil {
continue
}
val, ok := cmd.(string)
if !ok {
logx.Error("invalid value type")
return nil, errors.New("invalid value type")
}
var imageMeta types.ImageMeta
err = json.Unmarshal([]byte(val), &imageMeta)
if err != nil {
logx.Error(err)
return nil, errors.New("unmarshal recent file list failed")
}
parse, err := time.Parse("2006-01-02 15:04:05", imageMeta.CreatedAt)
if err != nil {
logx.Error(err)
return nil, errors.New("parse recent file list failed")
}
date := parse.Format("2006年1月2日 星期" + WeekdayMap[parse.Weekday()])
// 使用LoadOrStore来检查并存储或者追加
wg.Add(1)
go func(cmd interface{}) {
go func(date string, imageMeta types.ImageMeta) {
defer wg.Done()
val, ok := cmd.(string)
if !ok {
logx.Error("invalid value type")
return
value, loaded := groupedImages.LoadOrStore(date, []types.ImageMeta{imageMeta})
if loaded {
images := value.([]types.ImageMeta)
images = append(images, imageMeta)
groupedImages.Store(date, images)
}
var imageMeta types.ImageMeta
err = json.Unmarshal([]byte(val), &imageMeta)
if err != nil {
logx.Error(err)
return
}
parse, err := time.Parse("2006-01-02 15:04:05", imageMeta.CreatedAt)
if err != nil {
logx.Error(err)
return
}
date := parse.Format("2006年1月2日 星期" + WeekdayMap[parse.Weekday()])
groupedImages.Range(func(key, value interface{}) bool {
if key == date {
images := value.([]types.ImageMeta)
images = append(images, imageMeta)
groupedImages.Store(date, images)
return false
}
return true
})
groupedImages.Store(date, []types.ImageMeta{imageMeta})
}(cmd)
}(date, imageMeta)
}
wg.Wait()

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/redis/go-redis/v9"
"math/rand"
"net/url"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/app/auth/model/mysql/query"
"schisandra-album-cloud-microservices/common/constant"
@@ -56,18 +57,29 @@ func (l *QueryThingDetailListLogic) QueryThingDetailList(req *types.ThingDetailL
return nil, errors.New("get cached image list failed")
}
// 缓存未命中,从数据库中查询
storageInfo := l.svcCtx.DB.ScaStorageInfo
storageThumb := l.svcCtx.DB.ScaStorageThumb
// 数据库查询文件信息列表
var storageInfoQuery query.IScaStorageInfoDo
var storageInfoList []types.FileInfoResult
storageInfoQuery = storageInfo.Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.Tag.Eq(req.TagName)).
storageInfoQuery = storageInfo.Select(
storageInfo.ID,
storageInfo.FileName,
storageInfo.CreatedAt,
storageThumb.ThumbPath,
storageInfo.Path,
storageThumb.ThumbW,
storageThumb.ThumbH,
storageThumb.ThumbSize).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).
Where(
storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
storageInfo.Tag.Eq(req.TagName)).
Order(storageInfo.CreatedAt.Desc())
storageInfoList, err := storageInfoQuery.Find()
err = storageInfoQuery.Scan(&storageInfoList)
if err != nil {
return nil, err
}
@@ -93,10 +105,16 @@ func (l *QueryThingDetailListLogic) QueryThingDetailList(req *types.ThingDetailL
for _, dbFileInfo := range storageInfoList {
wg.Add(1)
go func(dbFileInfo *model.ScaStorageInfo) {
go func(dbFileInfo *types.FileInfoResult) {
defer wg.Done()
weekday := WeekdayMap[dbFileInfo.CreatedAt.Weekday()]
date := dbFileInfo.CreatedAt.Format("2006年1月2日 星期" + weekday)
reqParams := make(url.Values)
presignedUrl, err := l.svcCtx.MinioClient.PresignedGetObject(l.ctx, constant.ThumbnailBucketName, dbFileInfo.ThumbPath, time.Hour*24*7, reqParams)
if err != nil {
logx.Error(err)
return
}
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, dbFileInfo.Path, time.Hour*24*7)
if err != nil {
logx.Error(err)
@@ -109,15 +127,16 @@ func (l *QueryThingDetailListLogic) QueryThingDetailList(req *types.ThingDetailL
images = append(images, types.ImageMeta{
ID: dbFileInfo.ID,
FileName: dbFileInfo.FileName,
Thumbnail: presignedUrl.String(),
URL: url,
Width: dbFileInfo.Width,
Height: dbFileInfo.Height,
Width: dbFileInfo.ThumbW,
Height: dbFileInfo.ThumbH,
CreatedAt: dbFileInfo.CreatedAt.Format("2006-01-02 15:04:05"),
})
// 重新存储更新后的图像列表
groupedImages.Store(date, images)
}(dbFileInfo)
}(&dbFileInfo)
}
wg.Wait()
var imageList []types.AllImageDetail

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"github.com/redis/go-redis/v9"
"net/url"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
"schisandra-album-cloud-microservices/common/constant"
"schisandra-album-cloud-microservices/common/encrypt"
@@ -38,12 +39,15 @@ func (l *QueryThingImageListLogic) QueryThingImageList(req *types.ThingListReque
return nil, errors.New("user_id not found")
}
storageInfo := l.svcCtx.DB.ScaStorageInfo
storageInfos, err := storageInfo.Select(
storageThumb := l.svcCtx.DB.ScaStorageThumb
var thingList []types.ThingImageList
err = storageInfo.Select(
storageInfo.ID,
storageInfo.Category,
storageInfo.Tag,
storageInfo.Path,
storageThumb.ThumbPath,
storageInfo.CreatedAt).
LeftJoin(storageThumb, storageInfo.ThumbID.EqCol(storageThumb.ID)).
Where(storageInfo.UserID.Eq(uid),
storageInfo.Provider.Eq(req.Provider),
storageInfo.Bucket.Eq(req.Bucket),
@@ -52,28 +56,28 @@ func (l *QueryThingImageListLogic) QueryThingImageList(req *types.ThingListReque
storageInfo.Category.Length().Gt(0),
storageInfo.Tag.Length().Gte(0)).
Order(storageInfo.CreatedAt.Desc()).
Find()
Scan(&thingList)
if err != nil {
return nil, err
}
// 加载用户oss配置信息
cacheOssConfigKey := constant.UserOssConfigPrefix + uid + ":" + req.Provider
ossConfig, err := l.getOssConfigFromCacheOrDb(cacheOssConfigKey, uid, req.Provider)
if err != nil {
return nil, err
}
service, err := l.svcCtx.StorageManager.GetStorage(uid, ossConfig)
if err != nil {
return nil, errors.New("get storage failed")
}
//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")
//}
categoryMap := sync.Map{}
tagCountMap := sync.Map{}
tagCoverMap := sync.Map{} // 用于存储每个 Tag 的封面图片路径
for _, info := range storageInfos {
for _, info := range thingList {
tagKey := info.Category + "::" + info.Tag
if _, exists := tagCountMap.Load(tagKey); !exists {
tagCountMap.Store(tagKey, int64(0))
@@ -90,14 +94,15 @@ func (l *QueryThingImageListLogic) QueryThingImageList(req *types.ThingListReque
// 为每个 Tag 存储封面图片路径
if _, exists := tagCoverMap.Load(tagKey); !exists {
// 使用服务生成预签名 URL
coverImageURL, err := service.PresignedURL(l.ctx, req.Bucket, info.Path, 7*24*time.Hour)
reqParams := make(url.Values)
presignedUrl, err := l.svcCtx.MinioClient.PresignedGetObject(l.ctx, constant.ThumbnailBucketName, info.ThumbPath, time.Hour*24*7, reqParams)
if err == nil {
tagCoverMap.Store(tagKey, coverImageURL)
tagCoverMap.Store(tagKey, presignedUrl.String())
}
}
}
var thingListData []types.ThingListData
var thingListResponse []types.ThingListData
categoryMap.Range(func(category, tagData interface{}) bool {
var metas []types.ThingMeta
tagData.(*sync.Map).Range(func(tag, item interface{}) bool {
@@ -113,14 +118,14 @@ func (l *QueryThingImageListLogic) QueryThingImageList(req *types.ThingListReque
metas = append(metas, meta)
return true
})
thingListData = append(thingListData, types.ThingListData{
thingListResponse = append(thingListResponse, types.ThingListData{
Category: category.(string),
List: metas,
})
return true
})
return &types.ThingListResponse{Records: thingListData}, nil
return &types.ThingListResponse{Records: thingListResponse}, nil
}
// 提取解密操作为函数

View File

@@ -1,17 +1,23 @@
package storage
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/ccpwcn/kgo"
"github.com/minio/minio-go/v7"
"github.com/redis/go-redis/v9"
"github.com/zeromicro/go-zero/core/logx"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
"gorm.io/gorm"
"io"
"mime/multipart"
"net/http"
"net/url"
"path"
"path/filepath"
"schisandra-album-cloud-microservices/app/aisvc/rpc/pb"
@@ -56,72 +62,159 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) {
if err != nil {
return "", err
}
defer file.Close()
defer func(file multipart.File) {
_ = file.Close()
}(file)
// 解析 AI 识别结果
result, err := l.parseAIRecognitionResult(r)
if err != nil {
return "", err
}
var faceId int64 = 0
bytes, err := io.ReadAll(file)
data, err := io.ReadAll(file)
if err != nil {
return "", err
}
// 人脸识别
if result.FileType == "image/png" || result.FileType == "image/jpeg" {
face, err := l.svcCtx.AiSvcRpc.FaceRecognition(l.ctx, &pb.FaceRecognitionRequest{Face: bytes, UserId: uid})
if err != nil {
return "", err
// 解析上传的缩略图
thumbnail, _, err := l.getUploadedThumbnail(r)
if err != nil {
return "", err
}
defer thumbnail.Close()
// 解析图片信息识别结果
result, err := l.parseImageInfoResult(r)
if err != nil {
return "", err
}
// 使用 `errgroup.Group` 处理并发任务
var (
faceId int64
filePath string
minioFilePath string
presignedURL string
)
g, ctx := errgroup.WithContext(context.Background())
// 创建信号量,限制最大并发上传数(比如最多同时 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 face != nil {
faceId = face.GetFaceId()
}
}
if face != nil {
faceId = face.GetFaceId()
}
}
// 根据 GPS 信息获取地理位置信息
country, province, city, err := l.getGeoLocation(result.Latitude, result.Longitude)
if err != nil {
return "", err
}
return nil
})
// 上传文件到 OSS
// 重新设置文件指针到文件开头
if _, err = file.Seek(0, 0); err != nil {
return "", err
}
bucket, provider, filePath, url, err := l.uploadFileToOSS(uid, header, file, result)
if err != nil {
g.Go(func() error {
if err := sem.Acquire(ctx, 1); err != nil {
return err
}
defer sem.Release(1)
// 重新创建 `multipart.File` 兼容的 `Reader`
fileReader := struct {
*bytes.Reader
io.Closer
}{
Reader: bytes.NewReader(data),
Closer: io.NopCloser(nil),
}
fileUrl, err := l.uploadFileToOSS(uid, header, fileReader, result)
if err != nil {
return err
}
filePath = fileUrl
return nil
})
// 上传缩略图到 MinIO
g.Go(func() error {
if err := sem.Acquire(ctx, 1); err != nil {
return err
}
defer sem.Release(1)
path, url, err := l.uploadFileToMinio(uid, header, thumbnail, result)
if err != nil {
return err
}
minioFilePath = path
presignedURL = url
return nil
})
// 等待所有 goroutine 执行完毕
if err = g.Wait(); err != nil {
return "", err
}
// 将地址信息保存到数据库
locationId, err := l.saveFileLocationInfoToDB(uid, result.Provider, result.Bucket, result.Latitude, result.Longitude, country, province, city, filePath)
fileUploadMessage := &types.FileUploadMessage{
UID: uid,
Data: result,
FaceID: faceId,
FileHeader: header,
FilePath: filePath,
PresignedURL: presignedURL,
ThumbPath: minioFilePath,
}
// 转换为 JSON
messageData, err := json.Marshal(fileUploadMessage)
if err != nil {
return "", err
}
err = l.svcCtx.NSQProducer.Publish(constant.MQTopicImageProcess, messageData)
if err != nil {
return "", errors.New("publish message failed")
}
// 将 EXIF 和文件信息存入数据库
id, err := l.saveFileInfoToDB(uid, bucket, provider, header, result, locationId, faceId, filePath)
if err != nil {
return "", err
}
// 删除缓存
l.afterImageUpload(uid, provider, bucket)
// ------------------------------------------------------------------------
// redis 保存最近7天上传的文件列表
err = l.saveRecentFileList(uid, url, id, result, header.Filename)
if err != nil {
return "", err
}
//// 根据 GPS 信息获取地理位置信息
//country, province, city, err := l.getGeoLocation(result.Latitude, result.Longitude)
//if err != nil {
// return "", err
//}
//// 将地址信息保存到数据库
//locationId, err := l.saveFileLocationInfoToDB(uid, result.Provider, result.Bucket, result.Latitude, result.Longitude, country, province, city, filePath)
//if err != nil {
// return "", err
//}
//
//// 将 EXIF 和文件信息存入数据库
//id, err := l.saveFileInfoToDB(uid, bucket, provider, header, result, locationId, faceId, filePath)
//if err != nil {
// return "", err
//}
//// 删除缓存
//l.afterImageUpload(uid, provider, bucket)
//
//// redis 保存最近7天上传的文件列表
//err = l.saveRecentFileList(uid, url, id, result, header.Filename)
//if err != nil {
// return "", err
//}
return "success", nil
}
// 将 multipart.File 转为 Base64 字符串
func (l *UploadFileLogic) fileToBase64(file multipart.File) (string, error) {
// 读取文件内容
fileBytes, err := io.ReadAll(file)
if err != nil {
return "", err
}
// 将文件内容转为 Base64 编码
return base64.StdEncoding.EncodeToString(fileBytes), nil
}
// 获取用户 ID
func (l *UploadFileLogic) getUserID() (string, error) {
uid, ok := l.ctx.Value("user_id").(string)
@@ -150,8 +243,17 @@ func (l *UploadFileLogic) getUploadedFile(r *http.Request) (multipart.File, *mul
return file, header, nil
}
// 解析 AI 识别结果
func (l *UploadFileLogic) parseAIRecognitionResult(r *http.Request) (types.File, error) {
// 解析上传的文件
func (l *UploadFileLogic) getUploadedThumbnail(r *http.Request) (multipart.File, *multipart.FileHeader, error) {
file, header, err := r.FormFile("thumbnail")
if err != nil {
return nil, nil, errors.New("file not found")
}
return file, header, nil
}
// 解析图片信息结果
func (l *UploadFileLogic) parseImageInfoResult(r *http.Request) (types.File, error) {
formValue := r.PostFormValue("data")
var result types.File
if err := json.Unmarshal([]byte(formValue), &result); err != nil {
@@ -160,47 +262,11 @@ func (l *UploadFileLogic) parseAIRecognitionResult(r *http.Request) (types.File,
return result, nil
}
// 提取拍摄时间
func (l *UploadFileLogic) extractOriginalDateTime(exif map[string]interface{}) (string, error) {
if dateTimeOriginal, ok := exif["DateTimeOriginal"].(string); ok {
parsedTime, err := time.Parse(time.RFC3339, dateTimeOriginal)
if err == nil {
return parsedTime.Format("2006-01-02 15:04:05"), nil
}
}
return "", nil
}
// 根据 GPS 信息获取地理位置信息
func (l *UploadFileLogic) getGeoLocation(latitude, longitude float64) (string, string, string, error) {
if latitude == 0.000000 || longitude == 0.000000 {
return "", "", "", nil
}
//request := gao_map.ReGeoRequest{Location: fmt.Sprintf("%f,%f", latitude, longitude)}
//
//location, err := l.svcCtx.GaoMap.Location.ReGeo(&request)
//if err != nil {
// return nil, errors.New("regeo failed")
//}
//
//addressInfo := map[string]string{}
//if location.ReGeoCode.AddressComponent.Country != "" {
// addressInfo["country"] = location.ReGeoCode.AddressComponent.Country
//}
//if location.ReGeoCode.AddressComponent.Province != "" {
// addressInfo["province"] = location.ReGeoCode.AddressComponent.Province
//}
//if location.ReGeoCode.AddressComponent.City != "" {
// addressInfo["city"] = location.ReGeoCode.AddressComponent.City.(string)
//}
//if location.ReGeoCode.AddressComponent.District != "" {
// addressInfo["district"] = location.ReGeoCode.AddressComponent.District.(string)
//}
//if location.ReGeoCode.AddressComponent.Township != "" {
// addressInfo["township"] = location.ReGeoCode.AddressComponent.Township
//}
country, province, city, err := geo_json.GetAddress(latitude, longitude, l.svcCtx.GeoRegionData)
if err != nil {
return "", "", "", errors.New("get geo location failed")
@@ -210,20 +276,21 @@ func (l *UploadFileLogic) getGeoLocation(latitude, longitude float64) (string, s
}
// 上传文件到 OSS
func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHeader, file multipart.File, result types.File) (string, string, string, string, error) {
func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHeader, file multipart.File, result types.File) (string, error) {
cacheKey := constant.UserOssConfigPrefix + uid + ":" + result.Provider
ossConfig, err := l.getOssConfigFromCacheOrDb(cacheKey, uid, result.Provider)
if err != nil {
return "", "", "", "", errors.New("get oss config failed")
return "", errors.New("get oss config failed")
}
service, err := l.svcCtx.StorageManager.GetStorage(uid, ossConfig)
if err != nil {
return "", "", "", "", errors.New("get storage failed")
return "", errors.New("get storage failed")
}
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)),
)
@@ -231,13 +298,50 @@ func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHead
"Content-Type": header.Header.Get("Content-Type"),
})
if err != nil {
return "", "", "", "", errors.New("upload file failed")
return "", errors.New("upload file failed")
}
url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, objectKey, time.Hour*24*7)
//url, err := service.PresignedURL(l.ctx, ossConfig.BucketName, objectKey, time.Hour*24*7)
//if err != nil {
// return "", "", errors.New("presigned url failed")
//}
return objectKey, nil
}
func (l *UploadFileLogic) uploadFileToMinio(uid string, header *multipart.FileHeader, file multipart.File, result types.File) (string, 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 "", "", "", "", errors.New("presigned url failed")
return "", "", err
}
return ossConfig.BucketName, ossConfig.Provider, objectKey, url, nil
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, presignedURL.String(), nil
}
func (l *UploadFileLogic) saveFileLocationInfoToDB(uid string, provider string, bucket string, latitude float64, longitude float64, country string, province string, city string, filePath string) (int64, error) {

View File

@@ -0,0 +1,305 @@
package mq
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/nsqio/go-nsq"
"github.com/redis/go-redis/v9"
"github.com/zeromicro/go-zero/core/logx"
"gorm.io/gorm"
"mime/multipart"
"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"
"schisandra-album-cloud-microservices/common/geo_json"
"schisandra-album-cloud-microservices/common/nsqx"
"schisandra-album-cloud-microservices/common/storage/config"
"strconv"
"time"
)
type NsqImageProcessConsumer struct {
svcCtx *svc.ServiceContext
ctx context.Context
}
func NewImageProcessConsumer(svcCtx *svc.ServiceContext) {
consumer := nsqx.NewNSQConsumer(constant.MQTopicImageProcess)
consumer.AddHandler(&NsqImageProcessConsumer{
svcCtx: svcCtx,
ctx: context.Background(),
})
err := consumer.ConnectToNSQD(svcCtx.Config.NSQ.NSQDHost)
if err != nil {
panic(err)
}
}
func (c *NsqImageProcessConsumer) HandleMessage(msg *nsq.Message) error {
if len(msg.Body) == 0 {
return errors.New("empty message body")
}
var message types.FileUploadMessage
err := json.Unmarshal(msg.Body, &message)
if err != nil {
return err
}
// 根据 GPS 信息获取地理位置信息
country, province, city, err := c.getGeoLocation(message.Data.Latitude, message.Data.Longitude)
if err != nil {
return err
}
// 将地址信息保存到数据库
locationId, err := c.saveFileLocationInfoToDB(message.UID, message.Data.Provider, message.Data.Bucket, message.Data.Latitude, message.Data.Longitude, country, province, city, message.ThumbPath)
if err != nil {
return err
}
thumbnailId, err := c.saveFileThumbnailInfoToDB(message.UID, message.ThumbPath, message.Data.ThumbW, message.Data.ThumbH, message.Data.ThumbSize)
// 将文件信息存入数据库
id, err := c.saveFileInfoToDB(message.UID, message.Data.Bucket, message.Data.Provider, message.FileHeader, message.Data, locationId, message.FaceID, message.FilePath, thumbnailId)
if err != nil {
return err
}
// 删除缓存
c.afterImageUpload(message.UID, message.Data.Provider, message.Data.Bucket)
// redis 保存最近7天上传的文件列表
err = c.saveRecentFileList(message.UID, message.PresignedURL, id, message.Data, message.FileHeader.Filename)
if err != nil {
return err
}
return nil
}
// 根据 GPS 信息获取地理位置信息
func (c *NsqImageProcessConsumer) getGeoLocation(latitude, longitude float64) (string, string, string, error) {
if latitude == 0.000000 || longitude == 0.000000 {
return "", "", "", nil
}
country, province, city, err := geo_json.GetAddress(latitude, longitude, c.svcCtx.GeoRegionData)
if err != nil {
return "", "", "", errors.New("get geo location failed")
}
return country, province, city, nil
}
// 从缓存或数据库中获取 OSS 配置
func (c *NsqImageProcessConsumer) getOssConfigFromCacheOrDb(cacheKey, uid, provider string) (*config.StorageConfig, error) {
result, err := c.svcCtx.RedisClient.Get(c.ctx, cacheKey).Result()
if err != nil && !errors.Is(err, redis.Nil) {
return nil, errors.New("get oss config failed")
}
var ossConfig *config.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 c.decryptConfig(&redisOssConfig)
}
// 缓存未命中,从数据库中加载
scaOssConfig := c.svcCtx.DB.ScaStorageConfig
dbOssConfig, err := scaOssConfig.Where(scaOssConfig.UserID.Eq(uid), scaOssConfig.Provider.Eq(provider)).First()
if err != nil {
return nil, err
}
// 缓存数据库配置
ossConfig, err = c.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 = c.svcCtx.RedisClient.Set(c.ctx, cacheKey, marshalData, 0).Err()
if err != nil {
return nil, errors.New("set oss config failed")
}
return ossConfig, nil
}
func (c *NsqImageProcessConsumer) classifyFile(mimeType string, isScreenshot bool) string {
// 使用map存储MIME类型及其对应的分类
typeMap := map[string]string{
"image/jpeg": "image",
"image/png": "image",
"image/gif": "gif",
"image/bmp": "image",
"image/tiff": "image",
"image/webp": "image",
"video/mp4": "video",
"video/avi": "video",
"video/mpeg": "video",
"video/quicktime": "video",
"video/x-msvideo": "video",
"video/x-flv": "video",
"video/x-matroska": "video",
}
// 根据MIME类型从map中获取分类
if classification, exists := typeMap[mimeType]; exists {
return classification
}
// 如果isScreenshot为true则返回"screenshot"
if isScreenshot {
return "screenshot"
}
return "unknown"
}
// 保存最近7天上传的文件列表
func (c *NsqImageProcessConsumer) saveRecentFileList(uid, url string, id int64, result types.File, filename string) error {
redisKey := constant.ImageRecentPrefix + uid + ":" + strconv.FormatInt(id, 10)
imageMeta := types.ImageMeta{
ID: id,
URL: url,
FileName: filename,
Width: result.Width,
Height: result.Height,
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
}
marshal, err := json.Marshal(imageMeta)
if err != nil {
logx.Error(err)
return errors.New("marshal image meta failed")
}
err = c.svcCtx.RedisClient.Set(c.ctx, redisKey, marshal, time.Hour*24*7).Err()
if err != nil {
logx.Error(err)
return errors.New("save recent file list failed")
}
return nil
}
// 提取解密操作为函数
func (c *NsqImageProcessConsumer) decryptConfig(dbConfig *model.ScaStorageConfig) (*config.StorageConfig, error) {
accessKey, err := encrypt.Decrypt(dbConfig.AccessKey, c.svcCtx.Config.Encrypt.Key)
if err != nil {
return nil, errors.New("decrypt access key failed")
}
secretKey, err := encrypt.Decrypt(dbConfig.SecretKey, c.svcCtx.Config.Encrypt.Key)
if err != nil {
return nil, errors.New("decrypt secret key failed")
}
return &config.StorageConfig{
Provider: dbConfig.Provider,
Endpoint: dbConfig.Endpoint,
AccessKey: accessKey,
SecretKey: secretKey,
BucketName: dbConfig.Bucket,
Region: dbConfig.Region,
}, nil
}
func (c *NsqImageProcessConsumer) saveFileLocationInfoToDB(uid string, provider string, bucket string, latitude float64, longitude float64, country string, province string, city string, filePath string) (int64, error) {
if latitude == 0.000000 || longitude == 0.000000 {
return 0, nil
}
locationDB := c.svcCtx.DB.ScaStorageLocation
storageLocations, err := locationDB.Where(locationDB.UserID.Eq(uid), locationDB.Province.Eq(province), locationDB.City.Eq(city)).First()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return 0, err
}
if storageLocations == nil {
locationInfo := model.ScaStorageLocation{
Provider: provider,
Bucket: bucket,
UserID: uid,
Country: country,
City: city,
Province: province,
Latitude: fmt.Sprintf("%f", latitude),
Longitude: fmt.Sprintf("%f", longitude),
Total: 1,
CoverImage: filePath,
}
err = locationDB.Create(&locationInfo)
if err != nil {
return 0, err
}
return locationInfo.ID, nil
} else {
info, err := locationDB.Where(locationDB.ID.Eq(storageLocations.ID), locationDB.UserID.Eq(uid)).UpdateColumnSimple(locationDB.Total.Add(1), locationDB.CoverImage.Value(filePath))
if err != nil {
return 0, err
}
if info.RowsAffected == 0 {
return 0, errors.New("update location failed")
}
return storageLocations.ID, nil
}
}
func (c *NsqImageProcessConsumer) saveFileThumbnailInfoToDB(uid string, filePath string, width, height float64, size float64) (int64, error) {
storageThumb := c.svcCtx.DB.ScaStorageThumb
storageThumbInfo := &model.ScaStorageThumb{
UserID: uid,
ThumbPath: filePath,
ThumbW: width,
ThumbH: height,
ThumbSize: size,
}
err := storageThumb.Create(storageThumbInfo)
if err != nil {
logx.Error(err)
return 0, errors.New("create storage thumb failed")
}
return storageThumbInfo.ID, nil
}
// 将 EXIF 和文件信息存入数据库
func (c *NsqImageProcessConsumer) saveFileInfoToDB(uid, bucket, provider string, header *multipart.FileHeader, result types.File, locationId, faceId int64, filePath string, thumbnailId int64) (int64, error) {
typeName := c.classifyFile(result.FileType, result.IsScreenshot)
scaStorageInfo := &model.ScaStorageInfo{
UserID: uid,
Provider: provider,
Bucket: bucket,
FileName: header.Filename,
FileSize: strconv.FormatInt(header.Size, 10),
FileType: result.FileType,
Path: filePath,
Landscape: result.Landscape,
Tag: result.TagName,
IsAnime: strconv.FormatBool(result.IsAnime),
Category: result.TopCategory,
LocationID: locationId,
FaceID: faceId,
Type: typeName,
Width: result.Width,
Height: result.Height,
ThumbID: thumbnailId,
}
err := c.svcCtx.DB.ScaStorageInfo.Create(scaStorageInfo)
if err != nil {
return 0, errors.New("create storage info failed")
}
return scaStorageInfo.ID, nil
}
// 在UploadImageLogic或其他需要使缓存失效的逻辑中添加
func (c *NsqImageProcessConsumer) afterImageUpload(uid, provider, bucket string) {
for _, sort := range []bool{true, false} {
key := fmt.Sprintf("%s%s:%s:%s:%v", constant.ImageListPrefix, uid, provider, bucket, sort)
if err := c.svcCtx.RedisClient.Del(c.ctx, key).Err(); err != nil {
logx.Errorf("删除缓存键 %s 失败: %v", key, err)
}
}
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/casbin/casbin/v2"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"github.com/minio/minio-go/v7"
"github.com/nsqio/go-nsq"
"github.com/redis/go-redis/v9"
"github.com/wenlng/go-captcha/v2/rotate"
"github.com/wenlng/go-captcha/v2/slide"
@@ -21,6 +22,7 @@ import (
"schisandra-album-cloud-microservices/common/geo_json"
"schisandra-album-cloud-microservices/common/ip2region"
"schisandra-album-cloud-microservices/common/miniox"
"schisandra-album-cloud-microservices/common/nsqx"
"schisandra-album-cloud-microservices/common/redisx"
"schisandra-album-cloud-microservices/common/sensitivex"
"schisandra-album-cloud-microservices/common/storage"
@@ -45,13 +47,14 @@ type ServiceContext struct {
StorageManager *manager.Manager
MinioClient *minio.Client
GeoRegionData *geo_json.RegionData
NSQProducer *nsq.Producer
}
func NewServiceContext(c config.Config) *ServiceContext {
redisClient := redisx.NewRedis(c.Redis.Host, c.Redis.Pass, c.Redis.DB)
db, queryDB := mysql.NewMySQL(c.Mysql.DataSource, c.Mysql.MaxOpenConn, c.Mysql.MaxIdleConn, redisClient)
casbinEnforcer := casbinx.NewCasbin(db)
return &ServiceContext{
serviceContext := &ServiceContext{
Config: c,
SecurityHeadersMiddleware: middleware.NewSecurityHeadersMiddleware().Handle,
CasbinVerifyMiddleware: middleware.NewCasbinVerifyMiddleware(casbinEnforcer).Handle,
@@ -68,5 +71,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
AiSvcRpc: aiservice.NewAiService(zrpc.MustNewClient(c.AiSvcRpc)),
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),
}
return serviceContext
}

View File

@@ -1,17 +0,0 @@
package types
// File represents a file uploaded by the user.
type File struct {
Provider string `json:"provider"`
Bucket string `json:"bucket"`
FileType string `json:"fileType"`
IsAnime bool `json:"isAnime"`
TagName string `json:"tagName"`
Landscape string `json:"landscape"`
TopCategory string `json:"topCategory"`
IsScreenshot bool `json:"isScreenshot"`
Width float64 `json:"width"`
Height float64 `json:"height"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
}

View File

@@ -0,0 +1,56 @@
package types
import (
"mime/multipart"
"time"
)
// File represents a file uploaded by the user.
type File struct {
Provider string `json:"provider"`
Bucket string `json:"bucket"`
FileType string `json:"fileType"`
IsAnime bool `json:"isAnime"`
TagName string `json:"tagName"`
Landscape string `json:"landscape"`
TopCategory string `json:"topCategory"`
IsScreenshot bool `json:"isScreenshot"`
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"`
}
// 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"`
Data File `json:"data"`
UID string `json:"uid"`
FilePath string `json:"filePath"`
PresignedURL string `json:"presignedURL"`
ThumbPath string `json:"thumbPath"`
}
type FileInfoResult struct {
ID int64 `json:"id"`
FileName string `json:"file_name"`
ThumbPath string `json:"thumb_path"`
ThumbW float64 `json:"thumb_w"`
ThumbH float64 `json:"thumb_h"`
ThumbSize float64 `json:"thumb_size"`
CreatedAt time.Time `json:"created_at"`
Path string `json:"path"`
}
type ThingImageList struct {
ID int64 `json:"id"`
Category string `json:"category"`
Tag string `json:"tag"`
CreatedAt time.Time `json:"created_at"`
ThumbPath string `json:"thumb_path"`
Path string `json:"path"`
}

View File

@@ -178,6 +178,7 @@ type ImageMeta struct {
Width float64 `json:"width"`
Height float64 `json:"height"`
CreatedAt string `json:"created_at"`
Thumbnail string `json:"thumbnail"`
}
type LocationDetailListRequest struct {
@@ -305,6 +306,10 @@ type RotateCaptchaResponse struct {
Thumb string `json:"thumb"`
}
type SingleImageRequest struct {
ID int64 `json:"id"`
}
type SlideCaptchaResponse struct {
Key string `json:"key"`
Image string `json:"image"`

View File

@@ -117,6 +117,7 @@ func main() {
scaUserMessage := g.GenerateModel("sca_user_message", fieldOpts...)
scaStorageAlbum := g.GenerateModel("sca_storage_album", fieldOpts...)
scaStorageLocation := g.GenerateModel("sca_storage_location", fieldOpts...)
scaStorageThumb := g.GenerateModel("sca_storage_thumb", fieldOpts...)
g.ApplyBasic(
scaAuthMenu,
@@ -137,6 +138,7 @@ func main() {
scaUserMessage,
scaStorageAlbum,
scaStorageLocation,
scaStorageThumb,
)
g.Execute()

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaAuthMenu = "sca_auth_menu"
// ScaAuthMenu mapped from table <sca_auth_menu>
// ScaAuthMenu 菜单表
type ScaAuthMenu struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键ID;primary_key" json:"id,string"` // 主键ID
MenuName string `gorm:"column:menu_name;type:varchar(64);comment:名称" json:"menu_name"` // 名称

View File

@@ -6,7 +6,7 @@ package model
const TableNameScaAuthPermissionRule = "sca_auth_permission_rule"
// ScaAuthPermissionRule mapped from table <sca_auth_permission_rule>
// ScaAuthPermissionRule 接口权限表
type ScaAuthPermissionRule struct {
ID int64 `gorm:"column:id;type:int(11);primaryKey;autoIncrement:true;primary_key" json:"id,string"`
Ptype string `gorm:"column:ptype;type:varchar(100);uniqueIndex:idx_sca_auth_permission_rule,priority:1;index:IDX_sca_auth_permission_rule_ptype,priority:1" json:"ptype"`

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaAuthRole = "sca_auth_role"
// ScaAuthRole mapped from table <sca_auth_role>
// ScaAuthRole 角色表
type ScaAuthRole struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键ID;primary_key" json:"id,string"` // 主键ID
RoleName string `gorm:"column:role_name;type:varchar(32);not null;comment:角色名称" json:"role_name"` // 角色名称

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaAuthUser = "sca_auth_user"
// ScaAuthUser mapped from table <sca_auth_user>
// ScaAuthUser 用户表
type ScaAuthUser struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;uniqueIndex:id,priority:1;comment:自增ID;primary_key" json:"id,string"` // 自增ID
UID string `gorm:"column:uid;type:varchar(50);not null;uniqueIndex:uid,priority:1;comment:唯一ID" json:"uid"` // 唯一ID

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaAuthUserDevice = "sca_auth_user_device"
// ScaAuthUserDevice mapped from table <sca_auth_user_device>
// ScaAuthUserDevice 用户设备表
type ScaAuthUserDevice struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键ID;primary_key" json:"id,string"` // 主键ID
UserID string `gorm:"column:user_id;type:varchar(20);not null;comment:用户ID" json:"user_id"` // 用户ID

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaAuthUserSocial = "sca_auth_user_social"
// ScaAuthUserSocial mapped from table <sca_auth_user_social>
// ScaAuthUserSocial 第三方登录信息表
type ScaAuthUserSocial struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键ID;primary_key" json:"id,string"` // 主键ID
UserID string `gorm:"column:user_id;type:varchar(50);not null;comment:用户ID" json:"user_id"` // 用户ID

View File

@@ -10,7 +10,7 @@ import (
const TableNameScaCommentLike = "sca_comment_likes"
// ScaCommentLike mapped from table <sca_comment_likes>
// ScaCommentLike 评论点赞表
type ScaCommentLike struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键id;primary_key" json:"id,string"` // 主键id
TopicID string `gorm:"column:topic_id;type:varchar(50);not null;comment:话题ID" json:"topic_id"` // 话题ID

View File

@@ -13,7 +13,7 @@ import (
const TableNameScaCommentReply = "sca_comment_reply"
// ScaCommentReply mapped from table <sca_comment_reply>
// ScaCommentReply 评论表
type ScaCommentReply struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;uniqueIndex:id,priority:1;comment:主键id;primary_key" json:"id,string"` // 主键id
UserID string `gorm:"column:user_id;type:varchar(50);not null;comment:评论用户id" json:"user_id"` // 评论用户id

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaMessageReport = "sca_message_report"
// ScaMessageReport mapped from table <sca_message_report>
// ScaMessageReport 评论举报表
type ScaMessageReport struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键;primary_key" json:"id,string"` // 主键
UserID string `gorm:"column:user_id;type:varchar(20);comment:用户Id" json:"user_id"` // 用户Id

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaStorageAlbum = "sca_storage_album"
// ScaStorageAlbum mapped from table <sca_storage_album>
// ScaStorageAlbum 相册信息表
type ScaStorageAlbum struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键;primary_key" json:"id,string"` // 主键
UserID string `gorm:"column:user_id;type:varchar(50);comment:用户ID" json:"user_id"` // 用户ID

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaStorageConfig = "sca_storage_config"
// ScaStorageConfig mapped from table <sca_storage_config>
// ScaStorageConfig 用户存储配置表
type ScaStorageConfig struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键;primary_key" json:"id,string"` // 主键
UserID string `gorm:"column:user_id;type:varchar(50);not null;comment:用户ID" json:"user_id"` // 用户ID

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaStorageInfo = "sca_storage_info"
// ScaStorageInfo mapped from table <sca_storage_info>
// ScaStorageInfo 文件存储信息表
type ScaStorageInfo struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键;primary_key" json:"id,string"` // 主键
UserID string `gorm:"column:user_id;type:varchar(50);not null;comment:用户ID" json:"user_id"` // 用户ID
@@ -24,6 +24,7 @@ type ScaStorageInfo struct {
FileType string `gorm:"column:file_type;type:varchar(50);comment:文件类型" json:"file_type"` // 文件类型
Width float64 `gorm:"column:width;type:double;comment:宽" json:"width"` // 宽
Height float64 `gorm:"column:height;type:double;comment:高" json:"height"` // 高
ThumbID int64 `gorm:"column:thumb_id;type:bigint(20);comment:缩略图id" json:"thumb_id"` // 缩略图id
Category string `gorm:"column:category;type:varchar(50);comment:分类" json:"category"` // 分类
Tag string `gorm:"column:tag;type:varchar(255);comment:标签" json:"tag"` // 标签
Type string `gorm:"column:type;type:varchar(50);comment:类型" json:"type"` // 类型

View File

@@ -13,7 +13,7 @@ import (
const TableNameScaStorageLocation = "sca_storage_location"
// ScaStorageLocation mapped from table <sca_storage_location>
// ScaStorageLocation 文件地理位置信息表
type ScaStorageLocation struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键;primary_key" json:"id,string"` // 主键
UserID string `gorm:"column:user_id;type:varchar(50);comment:用户id" json:"user_id"` // 用户id

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaStorageTag = "sca_storage_tag"
// ScaStorageTag mapped from table <sca_storage_tag>
// ScaStorageTag 标签表
type ScaStorageTag struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键;primary_key" json:"id,string"` // 主键
FileID int64 `gorm:"column:file_id;type:bigint(20);comment:文件ID" json:"file_id"` // 文件ID

View File

@@ -0,0 +1,31 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
"gorm.io/gorm"
)
const TableNameScaStorageThumb = "sca_storage_thumb"
// ScaStorageThumb mapped from table <sca_storage_thumb>
type ScaStorageThumb struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键;primary_key" json:"id,string"` // 主键
UserID string `gorm:"column:user_id;type:varchar(50);not null;comment:用户ID" json:"user_id"` // 用户ID
ThumbPath string `gorm:"column:thumb_path;type:text;comment:缩略图路径" json:"thumb_path"` // 缩略图路径
ThumbW float64 `gorm:"column:thumb_w;type:double;comment:缩略图宽" json:"thumb_w"` // 缩略图宽
ThumbH float64 `gorm:"column:thumb_h;type:double;comment:缩略图高" json:"thumb_h"` // 缩略图高
ThumbSize float64 `gorm:"column:thumb_size;type:double;comment:缩略图大小" json:"thumb_size"` // 缩略图大小
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"` // 删除时间
}
// TableName ScaStorageThumb's table name
func (*ScaStorageThumb) TableName() string {
return TableNameScaStorageThumb
}

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaUserFollow = "sca_user_follows"
// ScaUserFollow mapped from table <sca_user_follows>
// ScaUserFollow 用户关注表
type ScaUserFollow struct {
FollowerID string `gorm:"column:follower_id;type:varchar(50);not null;comment:关注者" json:"follower_id"` // 关注者
FolloweeID string `gorm:"column:followee_id;type:varchar(50);not null;comment:被关注者" json:"followee_id"` // 被关注者

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaUserLevel = "sca_user_level"
// ScaUserLevel mapped from table <sca_user_level>
// ScaUserLevel 用户等级表
type ScaUserLevel struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;comment:主键;primary_key" json:"id,string"` // 主键
UserID string `gorm:"column:user_id;type:varchar(50);comment:用户Id" json:"user_id"` // 用户Id

View File

@@ -12,7 +12,7 @@ import (
const TableNameScaUserMessage = "sca_user_message"
// ScaUserMessage mapped from table <sca_user_message>
// ScaUserMessage 用户消息表
type ScaUserMessage struct {
ID int64 `gorm:"column:id;type:bigint(20);primaryKey;autoIncrement:true;comment:主键;primary_key" json:"id,string"` // 主键
TopicID string `gorm:"column:topic_id;type:varchar(50);comment:话题Id" json:"topic_id"` // 话题Id

View File

@@ -23,11 +23,11 @@ func NewMySQL(url string, maxOpenConn int, maxIdleConn int, client *redis.Client
Logger: logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second, // 慢sql日志
LogLevel: logger.Error, // 级别
Colorful: true, // 颜色
IgnoreRecordNotFoundError: true, // 忽略RecordNotFoundError
ParameterizedQueries: true, // 格式化SQL语句
SlowThreshold: time.Second, // 慢sql日志
LogLevel: logger.Info, // 级别
Colorful: true, // 颜色
IgnoreRecordNotFoundError: true, // 忽略RecordNotFoundError
ParameterizedQueries: true, // 格式化SQL语句
}),
})
if err != nil {
@@ -41,7 +41,7 @@ func NewMySQL(url string, maxOpenConn int, maxIdleConn int, client *redis.Client
sqlDB.SetMaxIdleConns(maxIdleConn)
useDB := query.Use(db)
// migrate
Migrate(db)
//Migrate(db)
// cache
gormCache, err := cache.NewGorm2Cache(&config.CacheConfig{
CacheLevel: config.CacheLevelAll,

View File

@@ -32,6 +32,7 @@ var (
ScaStorageLocation *scaStorageLocation
ScaStorageTag *scaStorageTag
ScaStorageTagInfo *scaStorageTagInfo
ScaStorageThumb *scaStorageThumb
ScaUserFollow *scaUserFollow
ScaUserLevel *scaUserLevel
ScaUserMessage *scaUserMessage
@@ -54,6 +55,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
ScaStorageLocation = &Q.ScaStorageLocation
ScaStorageTag = &Q.ScaStorageTag
ScaStorageTagInfo = &Q.ScaStorageTagInfo
ScaStorageThumb = &Q.ScaStorageThumb
ScaUserFollow = &Q.ScaUserFollow
ScaUserLevel = &Q.ScaUserLevel
ScaUserMessage = &Q.ScaUserMessage
@@ -77,6 +79,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
ScaStorageLocation: newScaStorageLocation(db, opts...),
ScaStorageTag: newScaStorageTag(db, opts...),
ScaStorageTagInfo: newScaStorageTagInfo(db, opts...),
ScaStorageThumb: newScaStorageThumb(db, opts...),
ScaUserFollow: newScaUserFollow(db, opts...),
ScaUserLevel: newScaUserLevel(db, opts...),
ScaUserMessage: newScaUserMessage(db, opts...),
@@ -101,6 +104,7 @@ type Query struct {
ScaStorageLocation scaStorageLocation
ScaStorageTag scaStorageTag
ScaStorageTagInfo scaStorageTagInfo
ScaStorageThumb scaStorageThumb
ScaUserFollow scaUserFollow
ScaUserLevel scaUserLevel
ScaUserMessage scaUserMessage
@@ -126,6 +130,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
ScaStorageLocation: q.ScaStorageLocation.clone(db),
ScaStorageTag: q.ScaStorageTag.clone(db),
ScaStorageTagInfo: q.ScaStorageTagInfo.clone(db),
ScaStorageThumb: q.ScaStorageThumb.clone(db),
ScaUserFollow: q.ScaUserFollow.clone(db),
ScaUserLevel: q.ScaUserLevel.clone(db),
ScaUserMessage: q.ScaUserMessage.clone(db),
@@ -158,6 +163,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
ScaStorageLocation: q.ScaStorageLocation.replaceDB(db),
ScaStorageTag: q.ScaStorageTag.replaceDB(db),
ScaStorageTagInfo: q.ScaStorageTagInfo.replaceDB(db),
ScaStorageThumb: q.ScaStorageThumb.replaceDB(db),
ScaUserFollow: q.ScaUserFollow.replaceDB(db),
ScaUserLevel: q.ScaUserLevel.replaceDB(db),
ScaUserMessage: q.ScaUserMessage.replaceDB(db),
@@ -180,6 +186,7 @@ type queryCtx struct {
ScaStorageLocation IScaStorageLocationDo
ScaStorageTag IScaStorageTagDo
ScaStorageTagInfo IScaStorageTagInfoDo
ScaStorageThumb IScaStorageThumbDo
ScaUserFollow IScaUserFollowDo
ScaUserLevel IScaUserLevelDo
ScaUserMessage IScaUserMessageDo
@@ -202,6 +209,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
ScaStorageLocation: q.ScaStorageLocation.WithContext(ctx),
ScaStorageTag: q.ScaStorageTag.WithContext(ctx),
ScaStorageTagInfo: q.ScaStorageTagInfo.WithContext(ctx),
ScaStorageThumb: q.ScaStorageThumb.WithContext(ctx),
ScaUserFollow: q.ScaUserFollow.WithContext(ctx),
ScaUserLevel: q.ScaUserLevel.WithContext(ctx),
ScaUserMessage: q.ScaUserMessage.WithContext(ctx),

View File

@@ -46,6 +46,7 @@ func newScaAuthMenu(db *gorm.DB, opts ...gen.DOOption) scaAuthMenu {
return _scaAuthMenu
}
// scaAuthMenu 菜单表
type scaAuthMenu struct {
scaAuthMenuDo

View File

@@ -41,6 +41,7 @@ func newScaAuthPermissionRule(db *gorm.DB, opts ...gen.DOOption) scaAuthPermissi
return _scaAuthPermissionRule
}
// scaAuthPermissionRule 接口权限表
type scaAuthPermissionRule struct {
scaAuthPermissionRuleDo

View File

@@ -39,6 +39,7 @@ func newScaAuthRole(db *gorm.DB, opts ...gen.DOOption) scaAuthRole {
return _scaAuthRole
}
// scaAuthRole 角色表
type scaAuthRole struct {
scaAuthRoleDo

View File

@@ -50,6 +50,7 @@ func newScaAuthUser(db *gorm.DB, opts ...gen.DOOption) scaAuthUser {
return _scaAuthUser
}
// scaAuthUser 用户表
type scaAuthUser struct {
scaAuthUserDo

View File

@@ -50,6 +50,7 @@ func newScaAuthUserDevice(db *gorm.DB, opts ...gen.DOOption) scaAuthUserDevice {
return _scaAuthUserDevice
}
// scaAuthUserDevice 用户设备表
type scaAuthUserDevice struct {
scaAuthUserDeviceDo

View File

@@ -41,6 +41,7 @@ func newScaAuthUserSocial(db *gorm.DB, opts ...gen.DOOption) scaAuthUserSocial {
return _scaAuthUserSocial
}
// scaAuthUserSocial 第三方登录信息表
type scaAuthUserSocial struct {
scaAuthUserSocialDo

View File

@@ -38,6 +38,7 @@ func newScaCommentLike(db *gorm.DB, opts ...gen.DOOption) scaCommentLike {
return _scaCommentLike
}
// scaCommentLike 评论点赞表
type scaCommentLike struct {
scaCommentLikeDo

View File

@@ -55,6 +55,7 @@ func newScaCommentReply(db *gorm.DB, opts ...gen.DOOption) scaCommentReply {
return _scaCommentReply
}
// scaCommentReply 评论表
type scaCommentReply struct {
scaCommentReplyDo

View File

@@ -45,6 +45,7 @@ func newScaMessageReport(db *gorm.DB, opts ...gen.DOOption) scaMessageReport {
return _scaMessageReport
}
// scaMessageReport 评论举报表
type scaMessageReport struct {
scaMessageReportDo

View File

@@ -41,6 +41,7 @@ func newScaStorageAlbum(db *gorm.DB, opts ...gen.DOOption) scaStorageAlbum {
return _scaStorageAlbum
}
// scaStorageAlbum 相册信息表
type scaStorageAlbum struct {
scaStorageAlbumDo

View File

@@ -44,6 +44,7 @@ func newScaStorageConfig(db *gorm.DB, opts ...gen.DOOption) scaStorageConfig {
return _scaStorageConfig
}
// scaStorageConfig 用户存储配置表
type scaStorageConfig struct {
scaStorageConfigDo

View File

@@ -37,6 +37,7 @@ func newScaStorageInfo(db *gorm.DB, opts ...gen.DOOption) scaStorageInfo {
_scaStorageInfo.FileType = field.NewString(tableName, "file_type")
_scaStorageInfo.Width = field.NewFloat64(tableName, "width")
_scaStorageInfo.Height = field.NewFloat64(tableName, "height")
_scaStorageInfo.ThumbID = field.NewInt64(tableName, "thumb_id")
_scaStorageInfo.Category = field.NewString(tableName, "category")
_scaStorageInfo.Tag = field.NewString(tableName, "tag")
_scaStorageInfo.Type = field.NewString(tableName, "type")
@@ -56,6 +57,7 @@ func newScaStorageInfo(db *gorm.DB, opts ...gen.DOOption) scaStorageInfo {
return _scaStorageInfo
}
// scaStorageInfo 文件存储信息表
type scaStorageInfo struct {
scaStorageInfoDo
@@ -70,6 +72,7 @@ type scaStorageInfo struct {
FileType field.String // 文件类型
Width field.Float64 // 宽
Height field.Float64 // 高
ThumbID field.Int64 // 缩略图id
Category field.String // 分类
Tag field.String // 标签
Type field.String // 类型
@@ -109,6 +112,7 @@ func (s *scaStorageInfo) updateTableName(table string) *scaStorageInfo {
s.FileType = field.NewString(table, "file_type")
s.Width = field.NewFloat64(table, "width")
s.Height = field.NewFloat64(table, "height")
s.ThumbID = field.NewInt64(table, "thumb_id")
s.Category = field.NewString(table, "category")
s.Tag = field.NewString(table, "tag")
s.Type = field.NewString(table, "type")
@@ -138,7 +142,7 @@ func (s *scaStorageInfo) GetFieldByName(fieldName string) (field.OrderExpr, bool
}
func (s *scaStorageInfo) fillFieldMap() {
s.fieldMap = make(map[string]field.Expr, 23)
s.fieldMap = make(map[string]field.Expr, 24)
s.fieldMap["id"] = s.ID
s.fieldMap["user_id"] = s.UserID
s.fieldMap["provider"] = s.Provider
@@ -149,6 +153,7 @@ func (s *scaStorageInfo) fillFieldMap() {
s.fieldMap["file_type"] = s.FileType
s.fieldMap["width"] = s.Width
s.fieldMap["height"] = s.Height
s.fieldMap["thumb_id"] = s.ThumbID
s.fieldMap["category"] = s.Category
s.fieldMap["tag"] = s.Tag
s.fieldMap["type"] = s.Type

View File

@@ -48,6 +48,7 @@ func newScaStorageLocation(db *gorm.DB, opts ...gen.DOOption) scaStorageLocation
return _scaStorageLocation
}
// scaStorageLocation 文件地理位置信息表
type scaStorageLocation struct {
scaStorageLocationDo

View File

@@ -39,6 +39,7 @@ func newScaStorageTag(db *gorm.DB, opts ...gen.DOOption) scaStorageTag {
return _scaStorageTag
}
// scaStorageTag 标签表
type scaStorageTag struct {
scaStorageTagDo

View File

@@ -0,0 +1,412 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"schisandra-album-cloud-microservices/app/auth/model/mysql/model"
)
func newScaStorageThumb(db *gorm.DB, opts ...gen.DOOption) scaStorageThumb {
_scaStorageThumb := scaStorageThumb{}
_scaStorageThumb.scaStorageThumbDo.UseDB(db, opts...)
_scaStorageThumb.scaStorageThumbDo.UseModel(&model.ScaStorageThumb{})
tableName := _scaStorageThumb.scaStorageThumbDo.TableName()
_scaStorageThumb.ALL = field.NewAsterisk(tableName)
_scaStorageThumb.ID = field.NewInt64(tableName, "id")
_scaStorageThumb.UserID = field.NewString(tableName, "user_id")
_scaStorageThumb.ThumbPath = field.NewString(tableName, "thumb_path")
_scaStorageThumb.ThumbW = field.NewFloat64(tableName, "thumb_w")
_scaStorageThumb.ThumbH = field.NewFloat64(tableName, "thumb_h")
_scaStorageThumb.ThumbSize = field.NewFloat64(tableName, "thumb_size")
_scaStorageThumb.CreatedAt = field.NewTime(tableName, "created_at")
_scaStorageThumb.UpdatedAt = field.NewTime(tableName, "updated_at")
_scaStorageThumb.DeletedAt = field.NewField(tableName, "deleted_at")
_scaStorageThumb.fillFieldMap()
return _scaStorageThumb
}
type scaStorageThumb struct {
scaStorageThumbDo
ALL field.Asterisk
ID field.Int64 // 主键
UserID field.String // 用户ID
ThumbPath field.String // 缩略图路径
ThumbW field.Float64 // 缩略图宽
ThumbH field.Float64 // 缩略图高
ThumbSize field.Float64 // 缩略图大小
CreatedAt field.Time // 创建时间
UpdatedAt field.Time // 更新时间
DeletedAt field.Field // 删除时间
fieldMap map[string]field.Expr
}
func (s scaStorageThumb) Table(newTableName string) *scaStorageThumb {
s.scaStorageThumbDo.UseTable(newTableName)
return s.updateTableName(newTableName)
}
func (s scaStorageThumb) As(alias string) *scaStorageThumb {
s.scaStorageThumbDo.DO = *(s.scaStorageThumbDo.As(alias).(*gen.DO))
return s.updateTableName(alias)
}
func (s *scaStorageThumb) updateTableName(table string) *scaStorageThumb {
s.ALL = field.NewAsterisk(table)
s.ID = field.NewInt64(table, "id")
s.UserID = field.NewString(table, "user_id")
s.ThumbPath = field.NewString(table, "thumb_path")
s.ThumbW = field.NewFloat64(table, "thumb_w")
s.ThumbH = field.NewFloat64(table, "thumb_h")
s.ThumbSize = field.NewFloat64(table, "thumb_size")
s.CreatedAt = field.NewTime(table, "created_at")
s.UpdatedAt = field.NewTime(table, "updated_at")
s.DeletedAt = field.NewField(table, "deleted_at")
s.fillFieldMap()
return s
}
func (s *scaStorageThumb) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := s.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (s *scaStorageThumb) fillFieldMap() {
s.fieldMap = make(map[string]field.Expr, 9)
s.fieldMap["id"] = s.ID
s.fieldMap["user_id"] = s.UserID
s.fieldMap["thumb_path"] = s.ThumbPath
s.fieldMap["thumb_w"] = s.ThumbW
s.fieldMap["thumb_h"] = s.ThumbH
s.fieldMap["thumb_size"] = s.ThumbSize
s.fieldMap["created_at"] = s.CreatedAt
s.fieldMap["updated_at"] = s.UpdatedAt
s.fieldMap["deleted_at"] = s.DeletedAt
}
func (s scaStorageThumb) clone(db *gorm.DB) scaStorageThumb {
s.scaStorageThumbDo.ReplaceConnPool(db.Statement.ConnPool)
return s
}
func (s scaStorageThumb) replaceDB(db *gorm.DB) scaStorageThumb {
s.scaStorageThumbDo.ReplaceDB(db)
return s
}
type scaStorageThumbDo struct{ gen.DO }
type IScaStorageThumbDo interface {
gen.SubQuery
Debug() IScaStorageThumbDo
WithContext(ctx context.Context) IScaStorageThumbDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() IScaStorageThumbDo
WriteDB() IScaStorageThumbDo
As(alias string) gen.Dao
Session(config *gorm.Session) IScaStorageThumbDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) IScaStorageThumbDo
Not(conds ...gen.Condition) IScaStorageThumbDo
Or(conds ...gen.Condition) IScaStorageThumbDo
Select(conds ...field.Expr) IScaStorageThumbDo
Where(conds ...gen.Condition) IScaStorageThumbDo
Order(conds ...field.Expr) IScaStorageThumbDo
Distinct(cols ...field.Expr) IScaStorageThumbDo
Omit(cols ...field.Expr) IScaStorageThumbDo
Join(table schema.Tabler, on ...field.Expr) IScaStorageThumbDo
LeftJoin(table schema.Tabler, on ...field.Expr) IScaStorageThumbDo
RightJoin(table schema.Tabler, on ...field.Expr) IScaStorageThumbDo
Group(cols ...field.Expr) IScaStorageThumbDo
Having(conds ...gen.Condition) IScaStorageThumbDo
Limit(limit int) IScaStorageThumbDo
Offset(offset int) IScaStorageThumbDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) IScaStorageThumbDo
Unscoped() IScaStorageThumbDo
Create(values ...*model.ScaStorageThumb) error
CreateInBatches(values []*model.ScaStorageThumb, batchSize int) error
Save(values ...*model.ScaStorageThumb) error
First() (*model.ScaStorageThumb, error)
Take() (*model.ScaStorageThumb, error)
Last() (*model.ScaStorageThumb, error)
Find() ([]*model.ScaStorageThumb, error)
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.ScaStorageThumb, err error)
FindInBatches(result *[]*model.ScaStorageThumb, batchSize int, fc func(tx gen.Dao, batch int) error) error
Pluck(column field.Expr, dest interface{}) error
Delete(...*model.ScaStorageThumb) (info gen.ResultInfo, err error)
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
Updates(value interface{}) (info gen.ResultInfo, err error)
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
UpdateFrom(q gen.SubQuery) gen.Dao
Attrs(attrs ...field.AssignExpr) IScaStorageThumbDo
Assign(attrs ...field.AssignExpr) IScaStorageThumbDo
Joins(fields ...field.RelationField) IScaStorageThumbDo
Preload(fields ...field.RelationField) IScaStorageThumbDo
FirstOrInit() (*model.ScaStorageThumb, error)
FirstOrCreate() (*model.ScaStorageThumb, error)
FindByPage(offset int, limit int) (result []*model.ScaStorageThumb, count int64, err error)
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
Scan(result interface{}) (err error)
Returning(value interface{}, columns ...string) IScaStorageThumbDo
UnderlyingDB() *gorm.DB
schema.Tabler
}
func (s scaStorageThumbDo) Debug() IScaStorageThumbDo {
return s.withDO(s.DO.Debug())
}
func (s scaStorageThumbDo) WithContext(ctx context.Context) IScaStorageThumbDo {
return s.withDO(s.DO.WithContext(ctx))
}
func (s scaStorageThumbDo) ReadDB() IScaStorageThumbDo {
return s.Clauses(dbresolver.Read)
}
func (s scaStorageThumbDo) WriteDB() IScaStorageThumbDo {
return s.Clauses(dbresolver.Write)
}
func (s scaStorageThumbDo) Session(config *gorm.Session) IScaStorageThumbDo {
return s.withDO(s.DO.Session(config))
}
func (s scaStorageThumbDo) Clauses(conds ...clause.Expression) IScaStorageThumbDo {
return s.withDO(s.DO.Clauses(conds...))
}
func (s scaStorageThumbDo) Returning(value interface{}, columns ...string) IScaStorageThumbDo {
return s.withDO(s.DO.Returning(value, columns...))
}
func (s scaStorageThumbDo) Not(conds ...gen.Condition) IScaStorageThumbDo {
return s.withDO(s.DO.Not(conds...))
}
func (s scaStorageThumbDo) Or(conds ...gen.Condition) IScaStorageThumbDo {
return s.withDO(s.DO.Or(conds...))
}
func (s scaStorageThumbDo) Select(conds ...field.Expr) IScaStorageThumbDo {
return s.withDO(s.DO.Select(conds...))
}
func (s scaStorageThumbDo) Where(conds ...gen.Condition) IScaStorageThumbDo {
return s.withDO(s.DO.Where(conds...))
}
func (s scaStorageThumbDo) Order(conds ...field.Expr) IScaStorageThumbDo {
return s.withDO(s.DO.Order(conds...))
}
func (s scaStorageThumbDo) Distinct(cols ...field.Expr) IScaStorageThumbDo {
return s.withDO(s.DO.Distinct(cols...))
}
func (s scaStorageThumbDo) Omit(cols ...field.Expr) IScaStorageThumbDo {
return s.withDO(s.DO.Omit(cols...))
}
func (s scaStorageThumbDo) Join(table schema.Tabler, on ...field.Expr) IScaStorageThumbDo {
return s.withDO(s.DO.Join(table, on...))
}
func (s scaStorageThumbDo) LeftJoin(table schema.Tabler, on ...field.Expr) IScaStorageThumbDo {
return s.withDO(s.DO.LeftJoin(table, on...))
}
func (s scaStorageThumbDo) RightJoin(table schema.Tabler, on ...field.Expr) IScaStorageThumbDo {
return s.withDO(s.DO.RightJoin(table, on...))
}
func (s scaStorageThumbDo) Group(cols ...field.Expr) IScaStorageThumbDo {
return s.withDO(s.DO.Group(cols...))
}
func (s scaStorageThumbDo) Having(conds ...gen.Condition) IScaStorageThumbDo {
return s.withDO(s.DO.Having(conds...))
}
func (s scaStorageThumbDo) Limit(limit int) IScaStorageThumbDo {
return s.withDO(s.DO.Limit(limit))
}
func (s scaStorageThumbDo) Offset(offset int) IScaStorageThumbDo {
return s.withDO(s.DO.Offset(offset))
}
func (s scaStorageThumbDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IScaStorageThumbDo {
return s.withDO(s.DO.Scopes(funcs...))
}
func (s scaStorageThumbDo) Unscoped() IScaStorageThumbDo {
return s.withDO(s.DO.Unscoped())
}
func (s scaStorageThumbDo) Create(values ...*model.ScaStorageThumb) error {
if len(values) == 0 {
return nil
}
return s.DO.Create(values)
}
func (s scaStorageThumbDo) CreateInBatches(values []*model.ScaStorageThumb, batchSize int) error {
return s.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (s scaStorageThumbDo) Save(values ...*model.ScaStorageThumb) error {
if len(values) == 0 {
return nil
}
return s.DO.Save(values)
}
func (s scaStorageThumbDo) First() (*model.ScaStorageThumb, error) {
if result, err := s.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.ScaStorageThumb), nil
}
}
func (s scaStorageThumbDo) Take() (*model.ScaStorageThumb, error) {
if result, err := s.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.ScaStorageThumb), nil
}
}
func (s scaStorageThumbDo) Last() (*model.ScaStorageThumb, error) {
if result, err := s.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.ScaStorageThumb), nil
}
}
func (s scaStorageThumbDo) Find() ([]*model.ScaStorageThumb, error) {
result, err := s.DO.Find()
return result.([]*model.ScaStorageThumb), err
}
func (s scaStorageThumbDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.ScaStorageThumb, err error) {
buf := make([]*model.ScaStorageThumb, 0, batchSize)
err = s.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (s scaStorageThumbDo) FindInBatches(result *[]*model.ScaStorageThumb, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return s.DO.FindInBatches(result, batchSize, fc)
}
func (s scaStorageThumbDo) Attrs(attrs ...field.AssignExpr) IScaStorageThumbDo {
return s.withDO(s.DO.Attrs(attrs...))
}
func (s scaStorageThumbDo) Assign(attrs ...field.AssignExpr) IScaStorageThumbDo {
return s.withDO(s.DO.Assign(attrs...))
}
func (s scaStorageThumbDo) Joins(fields ...field.RelationField) IScaStorageThumbDo {
for _, _f := range fields {
s = *s.withDO(s.DO.Joins(_f))
}
return &s
}
func (s scaStorageThumbDo) Preload(fields ...field.RelationField) IScaStorageThumbDo {
for _, _f := range fields {
s = *s.withDO(s.DO.Preload(_f))
}
return &s
}
func (s scaStorageThumbDo) FirstOrInit() (*model.ScaStorageThumb, error) {
if result, err := s.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.ScaStorageThumb), nil
}
}
func (s scaStorageThumbDo) FirstOrCreate() (*model.ScaStorageThumb, error) {
if result, err := s.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.ScaStorageThumb), nil
}
}
func (s scaStorageThumbDo) FindByPage(offset int, limit int) (result []*model.ScaStorageThumb, count int64, err error) {
result, err = s.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = s.Offset(-1).Limit(-1).Count()
return
}
func (s scaStorageThumbDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = s.Count()
if err != nil {
return
}
err = s.Offset(offset).Limit(limit).Scan(result)
return
}
func (s scaStorageThumbDo) Scan(result interface{}) (err error) {
return s.DO.Scan(result)
}
func (s scaStorageThumbDo) Delete(models ...*model.ScaStorageThumb) (result gen.ResultInfo, err error) {
return s.DO.Delete(models)
}
func (s *scaStorageThumbDo) withDO(do gen.Dao) *scaStorageThumbDo {
s.DO = *do.(*gen.DO)
return s
}

View File

@@ -40,6 +40,7 @@ func newScaUserFollow(db *gorm.DB, opts ...gen.DOOption) scaUserFollow {
return _scaUserFollow
}
// scaUserFollow 用户关注表
type scaUserFollow struct {
scaUserFollowDo

View File

@@ -44,6 +44,7 @@ func newScaUserLevel(db *gorm.DB, opts ...gen.DOOption) scaUserLevel {
return _scaUserLevel
}
// scaUserLevel 用户等级表
type scaUserLevel struct {
scaUserLevelDo

View File

@@ -42,6 +42,7 @@ func newScaUserMessage(db *gorm.DB, opts ...gen.DOOption) scaUserMessage {
return _scaUserMessage
}
// scaUserMessage 用户消息表
type scaUserMessage struct {
scaUserMessageDo

View File

@@ -0,0 +1,7 @@
package constant
const (
FaceBucketName = "schisandra-face-samples"
CommentImagesBucketName = "schisandra-comment-images"
ThumbnailBucketName = "schisandra-thumbnail-images"
)

View File

@@ -0,0 +1,6 @@
package constant
const (
MQTopicImageProcess = "image-process-topic"
MQTopicCommentLike = "comment-like-topic"
)

View File

@@ -1,6 +0,0 @@
package constant
const (
FaceBucketName = "faces"
CommentImagesBucketName = "comments"
)

View File

@@ -22,7 +22,7 @@ const (
const (
ImageListPrefix = "image:list:"
ImageListMetaPrefix = "image:meta:"
ImageRecentPrefix = "image:recent:"
ImageFaceListPrefix = "image:face:list:"
ImageFaceListPrefix = "image:faces:"
ImageSinglePrefix = "image:single:"
)

View File

@@ -0,0 +1,27 @@
version: '3'
services:
nsqlookupd:
image: nsqio/nsq
command: /nsqlookupd
ports:
- "4160:4160"
- "4161:4161"
nsqd:
image: nsqio/nsq
command: /nsqd --mem-queue-size=0 -data-path=/data --broadcast-address=1.95.0.111 --lookupd-tcp-address=nsqlookupd:4160
depends_on:
- nsqlookupd
volumes:
- ./nsqd/data:/data
ports:
- "4150:4150"
- "4151:4151"
nsqadmin:
image: nsqio/nsq
command: /nsqadmin --lookupd-http-address=nsqlookupd:4161
depends_on:
- nsqlookupd
ports:
- "4171:4171"

28
common/nsqx/nsq.go Normal file
View File

@@ -0,0 +1,28 @@
package nsqx
import (
"fmt"
"github.com/nsqio/go-nsq"
"time"
)
func NewNsqProducer(url string) *nsq.Producer {
producer, err := nsq.NewProducer(url, nsq.NewConfig())
if err != nil {
panic(err)
}
producer.SetLoggerLevel(nsq.LogLevelError)
return producer
}
func NewNSQConsumer(topic string) *nsq.Consumer {
config := nsq.NewConfig()
config.LookupdPollInterval = 15 * time.Second
consumer, err := nsq.NewConsumer(topic, "channel", config)
if err != nil {
fmt.Printf("InitNSQ consumer error: %v\n", err)
return nil
}
consumer.SetLoggerLevel(nsq.LogLevelError)
return consumer
}

View File

@@ -0,0 +1,41 @@
package utils
import (
"fmt"
"io"
"mime/multipart"
)
// ByteReader 实现了 multipart.File 接口
type ByteReader struct {
data []byte
index int
}
func (r *ByteReader) Read(p []byte) (n int, err error) {
if r.index >= len(r.data) {
return 0, io.EOF
}
n = copy(p, r.data[r.index:])
r.index += n
return n, nil
}
func (r *ByteReader) Seek(offset int64, whence int) (int64, error) {
return 0, fmt.Errorf("Seek not implemented")
}
func (r *ByteReader) ReadAt(p []byte, off int64) (n int, err error) {
return 0, fmt.Errorf("ReadAt not implemented")
}
// 实现 Close 方法,符合 multipart.File 接口
func (r *ByteReader) Close() error {
// 这里没有实际需要清理的资源,但必须实现 Close 方法
return nil
}
// ToMultipartFile 将 []byte 转换为 multipart.File
func ToMultipartFile(data []byte) multipart.File {
return &ByteReader{data: data}
}

41
go.mod
View File

@@ -4,7 +4,7 @@ go 1.23.4
require (
github.com/ArtisanCloud/PowerLibs/v3 v3.3.1
github.com/ArtisanCloud/PowerWeChat/v3 v3.3.3
github.com/ArtisanCloud/PowerWeChat/v3 v3.3.6
github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.2.0
github.com/asjdf/gorm-cache v1.2.3
@@ -18,23 +18,25 @@ require (
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20241220152942-06eb5c6e8230
github.com/lxzan/gws v1.8.8
github.com/microcosm-cc/bluemonday v1.0.27
github.com/minio/minio-go/v7 v7.0.84
github.com/minio/minio-go/v7 v7.0.85
github.com/mssola/useragent v1.0.0
github.com/nicksnyder/go-i18n/v2 v2.5.1
github.com/nsqio/go-nsq v1.1.0
github.com/paulmach/orb v0.11.1
github.com/pelletier/go-toml/v2 v2.2.3
github.com/pkg6/go-sms v0.1.2
github.com/redis/go-redis/v9 v9.7.0
github.com/tencentyun/cos-go-sdk-v5 v0.7.60
github.com/tencentyun/cos-go-sdk-v5 v0.7.61
github.com/wenlng/go-captcha-assets v1.0.1
github.com/wenlng/go-captcha/v2 v2.0.2
github.com/yitter/idgenerator-go v1.3.3
github.com/zeromicro/go-zero v1.8.0
github.com/zmexing/go-sensitive-word v1.3.0
gocv.io/x/gocv v0.40.0
golang.org/x/crypto v0.32.0
golang.org/x/crypto v0.33.0
golang.org/x/text v0.22.0
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.4
google.golang.org/protobuf v1.36.5
gorm.io/driver/mysql v1.5.7
gorm.io/gen v0.3.26
gorm.io/gorm v1.25.12
@@ -60,6 +62,7 @@ require (
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/dolthub/maphash v0.1.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
@@ -83,14 +86,15 @@ require (
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20250202011525-fc3143867406 // indirect
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/imroc/req/v3 v3.49.1 // indirect
@@ -122,7 +126,6 @@ require (
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/paulmach/orb v0.11.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg6/go-requests v0.2.3 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
@@ -135,8 +138,6 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tidwall/geoindex v1.7.0 // indirect
github.com/tidwall/rtree v1.10.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.etcd.io/etcd/api/v3 v3.5.18 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.18 // indirect
@@ -158,18 +159,18 @@ require (
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/oauth2 v0.26.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/time v0.10.0 // indirect
golang.org/x/tools v0.29.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 // indirect
golang.org/x/tools v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250212204824-5a70512c5d8b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@@ -178,16 +179,16 @@ require (
gorm.io/driver/postgres v1.5.11 // indirect
gorm.io/driver/sqlserver v1.5.4 // indirect
gorm.io/hints v1.1.2 // indirect
k8s.io/api v0.32.1 // indirect
k8s.io/apimachinery v0.32.1 // indirect
k8s.io/client-go v0.32.1 // indirect
k8s.io/api v0.32.2 // indirect
k8s.io/apimachinery v0.32.2 // indirect
k8s.io/client-go v0.32.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
modernc.org/libc v1.61.11 // indirect
modernc.org/libc v1.61.13 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
modernc.org/sqlite v1.34.5 // indirect
modernc.org/sqlite v1.35.0 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect

89
go.sum
View File

@@ -4,8 +4,8 @@ github.com/ArtisanCloud/PowerLibs/v3 v3.3.1 h1:SsxBygxATQpFS92pKuVtGrgdawwsscj9Y
github.com/ArtisanCloud/PowerLibs/v3 v3.3.1/go.mod h1:xFGsskCnzAu+6rFEJbGVAlwhrwZPXAny6m7j71S/B5k=
github.com/ArtisanCloud/PowerSocialite/v3 v3.0.7 h1:P+erNlErr+X2v7Et+yTWaTfIRhw+HfpAPdvNIEwk9Gw=
github.com/ArtisanCloud/PowerSocialite/v3 v3.0.7/go.mod h1:VZQNCvcK/rldF3QaExiSl1gJEAkyc5/I8RLOd3WFZq4=
github.com/ArtisanCloud/PowerWeChat/v3 v3.3.3 h1:DoXg2PcUJtVhC9Ly28C1Xhmug2Si023VbTBRoY49xPY=
github.com/ArtisanCloud/PowerWeChat/v3 v3.3.3/go.mod h1:J9Soww8NJcB5DVAJSrpjEpAj0y7bdwBD1wdpR8xPeqk=
github.com/ArtisanCloud/PowerWeChat/v3 v3.3.6 h1:63LAZisWFAN+2B1fTPNTkbPjiDn8pIL+yS9lbDxUvhQ=
github.com/ArtisanCloud/PowerWeChat/v3 v3.3.6/go.mod h1:nIs82Blb0W8QoD6qCx01Lp1W/kM+/n18BgX10UcRIkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
@@ -85,6 +85,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
@@ -148,6 +150,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -161,8 +165,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250202011525-fc3143867406 h1:wlQI2cYY0BsWmmPPAnxfQ8SDW0S3Jasn+4B8kXFxprg=
github.com/google/pprof v0.0.0-20250202011525-fc3143867406/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250208200701-d0013a598941 h1:43XjGa6toxLpeksjcxs1jIoIyr+vUfOqY2c6HB4bpoc=
github.com/google/pprof v0.0.0-20250208200701-d0013a598941/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -172,8 +176,8 @@ 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/v2 v2.26.0 h1:VD1gqscl4nYs1YxVuSdemTrSgTKrwOWDK0FVFMqm+Cg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0/go.mod h1:4EgsQoS4TOhJizV+JTFg40qx1Ofh3XmXEQNBpgvNT40=
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=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -248,8 +252,8 @@ github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3ao
github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
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.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E=
github.com/minio/minio-go/v7 v7.0.84/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY=
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/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
@@ -274,6 +278,8 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/nsqio/go-nsq v1.1.0 h1:PQg+xxiUjA7V+TLdXw7nVrJ5Jbl3sN86EhGCQj4+FYE=
github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
@@ -346,15 +352,9 @@ 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.60 h1:/e/tmvRmfKexr/QQIBzWhOkZWsmY3EK72NrI6G/Tv0o=
github.com/tencentyun/cos-go-sdk-v5 v0.7.60/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0=
github.com/tidwall/cities v0.1.0/go.mod h1:lV/HDp2gCcRcHJWqgt6Di54GiDrTZwh1aG2ZUPNbqa4=
github.com/tidwall/geoindex v1.7.0 h1:jtk41sfgwIt8MEDyC3xyKSj75iXXf6rjReJGDNPtR5o=
github.com/tidwall/geoindex v1.7.0/go.mod h1:rvVVNEFfkJVWGUdEfU8QaoOg/9zFX0h9ofWzA60mz1I=
github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
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/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/rtree v1.10.0 h1:+EcI8fboEaW1L3/9oW/6AMoQ8HiEIHyR7bQOGnmz4Mg=
github.com/tidwall/rtree v1.10.0/go.mod h1:iDJQ9NBRtbfKkzZu02za+mIlaP+bjYPnunbSNidpbCQ=
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/v2 v2.0.2 h1:8twz6pI6xZwPvEGFezoFX395oFso1MuOlJt/tLiv7pk=
@@ -438,10 +438,11 @@ 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.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
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/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/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
@@ -471,8 +472,8 @@ golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -541,22 +542,22 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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-20250204164813-702378808489 h1:fCuMM4fowGzigT89NCIsW57Pk9k2D12MMi2ODn+Nk+o=
google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 h1:5bKytslY8ViY0Cj/ewmRtrWHW64bNF03cAatUUFCdFI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
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/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/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -599,12 +600,12 @@ gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
gorm.io/plugin/optimisticlock v1.1.3 h1:uFK8zz+Ln6ju3vGkTd1LY3xR2VBmMxjdU12KBb58PBA=
gorm.io/plugin/optimisticlock v1.1.3/go.mod h1:S+MH7qnHGQHxDBc9phjgN+DpNPn/qESd1q69fA3dtkg=
k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc=
k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k=
k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs=
k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU=
k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg=
k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg=
@@ -613,14 +614,14 @@ k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJ
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.15 h1:wFDan71KnYqeHz4eF63vmGE6Q6Pc0PUGDpP0PRMYjDc=
modernc.org/ccgo/v4 v4.23.15/go.mod h1:nJX30dks/IWuBOnVa7VRii9Me4/9TZ1SC9GNtmARTy0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.2 h1:YBXi5Kqp6aCK3fIxwKQ3/fErvawVKwjOLItxj1brGds=
modernc.org/gc/v2 v2.6.2/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.11 h1:6sZG8uB6EMMG7iTLPTndi8jyTdgAQNIeLGjCFICACZw=
modernc.org/libc v1.61.11/go.mod h1:HHX+srFdn839oaJRd0W8hBM3eg+mieyZCAjWwB08/nM=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
@@ -629,8 +630,8 @@ 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.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
modernc.org/sqlite v1.35.0 h1:yQps4fegMnZFdphtzlfQTCNBWtS0CZv48pRpW3RFHRw=
modernc.org/sqlite v1.35.0/go.mod h1:9cr2sicr7jIaWTBKQmAxQLfBv9LL0su4ZTEV+utt3ic=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=