diff --git a/.idea/GOHCache.xml b/.idea/GOHCache.xml
index 281c67c..e59a619 100644
--- a/.idea/GOHCache.xml
+++ b/.idea/GOHCache.xml
@@ -31,6 +31,13 @@
+
+
+
+
+
+
+
@@ -38,6 +45,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -59,6 +80,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -84,7 +119,6 @@
-
@@ -175,10 +209,18 @@
+
+
+
+
+
+
+
+
@@ -186,6 +228,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -193,6 +256,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -235,6 +327,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -319,6 +425,13 @@
+
+
+
+
+
+
+
@@ -326,6 +439,13 @@
+
+
+
+
+
+
+
@@ -337,7 +457,6 @@
-
@@ -418,6 +537,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -463,6 +603,7 @@
+
@@ -470,10 +611,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -551,6 +714,13 @@
+
+
+
+
+
+
+
@@ -558,6 +728,13 @@
+
+
+
+
+
+
+
@@ -614,20 +791,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -635,17 +798,38 @@
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -701,6 +885,7 @@
+
@@ -775,6 +960,13 @@
+
+
+
+
+
+
+
@@ -800,7 +992,20 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -832,6 +1037,13 @@
+
+
+
+
+
+
+
@@ -839,6 +1051,13 @@
+
+
+
+
+
+
+
@@ -849,6 +1068,7 @@
+
@@ -965,34 +1185,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1007,31 +1199,73 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1077,14 +1311,200 @@
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/app/ai/rpc/ai.proto b/app/ai/rpc/ai.proto
deleted file mode 100644
index 45e385d..0000000
--- a/app/ai/rpc/ai.proto
+++ /dev/null
@@ -1,71 +0,0 @@
-syntax = "proto3";
-
-package auth;
-option go_package = "./pb";
-
-message AccountLoginRequest {
- string account = 1;
- string password = 2;
- bool auto_login = 3;
- int64 angle = 4;
- string key = 5;
-}
-message PhoneLoginRequest {
- string phone = 1;
- int64 captcha = 2;
- bool auto_login = 3;
-}
-message ResetPasswordRequest {
- string phone = 1;
- string captcha = 2;
- string password = 3;
- string repassword = 4;
-}
-message WechatOffiaccountLoginRequest {
- string openid = 1;
- string client_id = 2;
-}
-message GetWechatQrcodeRequest {
- string client_id = 1;
-}
-message LoginResponse {
- string access_token = 1;
- int64 expire_at = 2;
- string uid = 3;
- string username = 4;
- string nickname = 5;
- string avatar = 6;
- int64 status = 7;
-}
-message ResetPasswordResponse {
- bool success = 1;
-}
-message GetWechatQrcodeResponse {
- string qrcode = 1;
-}
-// The LoginService service definition.
-service LoginService{
- // AccountLogin
- rpc AccountLogin (AccountLoginRequest) returns (LoginResponse);
- // PhoneLogin
- rpc PhoneLogin (AccountLoginRequest) returns (LoginResponse);
- // ResetPassword
- rpc ResetPassword (ResetPasswordRequest) returns (ResetPasswordResponse);
- // WechatOffiaccountLogin
- rpc WechatOffiaccountLogin (WechatOffiaccountLoginRequest) returns (LoginResponse);
- // GetWechatOffiaccountQrcode
- rpc GetWechatOffiaccountQrcode (GetWechatQrcodeRequest) returns (GetWechatQrcodeResponse);
-}
-
-// The TokenService service definition.
-message TokenRequest {
- string uid = 1;
-}
-message RefreshTokenResponse {
- string access_token = 1;
- int64 expire_at = 2;
-}
-service TokenService{
- // RefreshToken
- rpc RefreshToken (TokenRequest) returns (RefreshTokenResponse);
-}
\ No newline at end of file
diff --git a/app/ai/rpc/generate.go b/app/ai/rpc/generate.go
deleted file mode 100644
index 0777479..0000000
--- a/app/ai/rpc/generate.go
+++ /dev/null
@@ -1,3 +0,0 @@
-package rpc
-
-//go:generate goctl rpc protoc ai.proto --go_out=. --go-grpc_out=. --zrpc_out=. --client=true -m --style=go_zero
diff --git a/app/aisvc/model/mysql/generate/generate.go b/app/aisvc/model/mysql/generate/generate.go
new file mode 100644
index 0000000..8041cdd
--- /dev/null
+++ b/app/aisvc/model/mysql/generate/generate.go
@@ -0,0 +1,107 @@
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+
+ "gorm.io/driver/mysql"
+ "gorm.io/gen"
+ "gorm.io/gen/field"
+ "gorm.io/gorm"
+)
+
+const MySQLDSN = "root:LDQ20020618xxx@tcp(1.95.0.111:3306)/schisandra-cloud-album?charset=utf8mb4&parseTime=True&loc=Local"
+
+func main() {
+
+ // 连接数据库
+ db, err := gorm.Open(mysql.Open(MySQLDSN))
+ if err != nil {
+ panic(err)
+ }
+
+ dir, err := os.Getwd()
+ if err != nil {
+ panic(err)
+ }
+ path := filepath.Join(dir, "app/aisvc/model/mysql/", "query")
+ // 生成实例
+ g := gen.NewGenerator(gen.Config{
+ // 相对执行`go run`时的路径, 会自动创建目录
+ OutPath: path,
+ // 生成的文件名,默认gen.go
+ OutFile: "gen.go",
+ // 生成DAO代码的包名,默认是:model
+ ModelPkgPath: "model",
+ // 是否为DAO包生成单元测试代码,默认:false
+ WithUnitTest: false,
+
+ // WithDefaultQuery 生成默认查询结构体(作为全局变量使用), 即`Q`结构体和其字段(各表模型)
+ // WithoutContext 生成没有context调用限制的代码供查询
+ // WithQueryInterface 生成interface形式的查询代码(可导出), 如`Where()`方法返回的就是一个可导出的接口类型
+ Mode: gen.WithDefaultQuery | gen.WithQueryInterface | gen.WithoutContext,
+
+ // 表字段可为 null 值时, 对应结体字段使用指针类型
+ FieldNullable: false, // generate pointer when field is nullable
+
+ // 表字段默认值与模型结构体字段零值不一致的字段, 在插入数据时需要赋值该字段值为零值的, 结构体字段须是指针类型才能成功, 即`FieldCoverable:true`配置下生成的结构体字段.
+ // 因为在插入时遇到字段为零值的会被GORM赋予默认值. 如字段`age`表默认值为10, 即使你显式设置为0最后也会被GORM设为10提交.
+ // 如果该字段没有上面提到的插入时赋零值的特殊需要, 则字段为非指针类型使用起来会比较方便.
+ FieldCoverable: true,
+
+ // 模型结构体字段的数字类型的符号表示是否与表字段的一致, `false`指示都用有符号类型
+ FieldSignable: false,
+ // 生成 gorm 标签的字段索引属性
+ FieldWithIndexTag: true,
+ // 生成 gorm 标签的字段类型属性
+ FieldWithTypeTag: true,
+ })
+ // 设置目标 db
+ g.UseDB(db)
+
+ // 自定义字段的数据类型
+ // 统一数字类型为int64,兼容protobuf
+ dataMap := map[string]func(columnType gorm.ColumnType) (dataType string){
+ "tinyint": func(columnType gorm.ColumnType) (dataType string) { return "int64" },
+ "smallint": func(columnType gorm.ColumnType) (dataType string) { return "int64" },
+ "mediumint": func(columnType gorm.ColumnType) (dataType string) { return "int64" },
+ "bigint": func(columnType gorm.ColumnType) (dataType string) { return "int64" },
+ "int": func(columnType gorm.ColumnType) (dataType string) { return "int64" },
+ }
+ // 要先于`ApplyBasic`执行
+ g.WithDataTypeMap(dataMap)
+
+ // 自定义模型结体字段的标签
+ // 将特定字段名的 json 标签加上`string`属性,即 MarshalJSON 时该字段由数字类型转成字符串类型
+ jsonField := gen.FieldJSONTagWithNS(func(columnName string) (tagContent string) {
+ toStringField := `id, `
+ if strings.Contains(toStringField, columnName) {
+ return columnName + ",string"
+ }
+ return columnName
+ })
+ // 将非默认字段名的字段定义为自动时间戳和软删除字段;
+ // 自动时间戳默认字段名为:`updated_at`、`created_at, 表字段数据类型为: INT 或 DATETIME
+ // 软删除默认字段名为:`deleted_at`, 表字段数据类型为: DATETIME
+ idField := gen.FieldGORMTag("id", func(tag field.GormTag) field.GormTag {
+ return tag.Append("primary_key")
+ })
+ autoUpdateTimeField := gen.FieldGORMTag("updated_at", func(tag field.GormTag) field.GormTag {
+ return tag.Append("autoUpdateTime")
+ })
+ autoCreateTimeField := gen.FieldGORMTag("created_at", func(tag field.GormTag) field.GormTag {
+ return tag.Append("autoCreateTime")
+ })
+ softDeleteField := gen.FieldType("delete_at", "gorm.DeletedAt")
+ versionField := gen.FieldType("version", "optimisticlock.Version")
+ // 模型自定义选项组
+ fieldOpts := []gen.ModelOpt{jsonField, idField, autoUpdateTimeField, autoCreateTimeField, softDeleteField, versionField}
+
+ // 创建全部模型文件, 并覆盖前面创建的同名模型
+ model := g.GenerateModel("sca_storage_face", fieldOpts...)
+
+ g.ApplyBasic(model)
+
+ g.Execute()
+}
diff --git a/app/aisvc/model/mysql/model/sca_storage_face.gen.go b/app/aisvc/model/mysql/model/sca_storage_face.gen.go
new file mode 100644
index 0000000..2bad54a
--- /dev/null
+++ b/app/aisvc/model/mysql/model/sca_storage_face.gen.go
@@ -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 TableNameScaStorageFace = "sca_storage_face"
+
+// ScaStorageFace 人脸特征向量表
+type ScaStorageFace 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
+ FaceName string `gorm:"column:face_name;type:varchar(255);comment:人脸名称" json:"face_name"` // 人脸名称
+ FaceVector string `gorm:"column:face_vector;type:json;comment:人脸特征向量" json:"face_vector"` // 人脸特征向量
+ FaceImagePath string `gorm:"column:face_image_path;type:varchar(255);comment:人脸图像路径" json:"face_image_path"` // 人脸图像路径
+ FaceType string `gorm:"column:face_type;type:varchar(50);comment:人脸类型标识" json:"face_type"` // 人脸类型标识
+ 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 ScaStorageFace's table name
+func (*ScaStorageFace) TableName() string {
+ return TableNameScaStorageFace
+}
diff --git a/app/aisvc/model/mysql/mysql.go b/app/aisvc/model/mysql/mysql.go
new file mode 100644
index 0000000..c5a7f83
--- /dev/null
+++ b/app/aisvc/model/mysql/mysql.go
@@ -0,0 +1,76 @@
+package mysql
+
+import (
+ "log"
+ "os"
+ "schisandra-album-cloud-microservices/app/aisvc/model/mysql/model"
+ "schisandra-album-cloud-microservices/app/aisvc/model/mysql/query"
+ "time"
+
+ "github.com/asjdf/gorm-cache/cache"
+ "github.com/asjdf/gorm-cache/config"
+ "github.com/asjdf/gorm-cache/storage"
+ "github.com/redis/go-redis/v9"
+ "gorm.io/driver/mysql"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+)
+
+func NewMySQL(url string, maxOpenConn int, maxIdleConn int, client *redis.Client) (*gorm.DB, *query.Query) {
+ db, err := gorm.Open(mysql.Open(url), &gorm.Config{
+ SkipDefaultTransaction: true,
+ PrepareStmt: true,
+ 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语句
+ }),
+ })
+ if err != nil {
+ panic(err)
+ }
+ sqlDB, err := db.DB()
+ if err != nil {
+ panic(err)
+ }
+ sqlDB.SetMaxOpenConns(maxOpenConn)
+ sqlDB.SetMaxIdleConns(maxIdleConn)
+ useDB := query.Use(db)
+ // migrate
+ Migrate(db)
+ // cache
+ gormCache, err := cache.NewGorm2Cache(&config.CacheConfig{
+ CacheLevel: config.CacheLevelAll,
+ CacheStorage: storage.NewRedis(&storage.RedisStoreConfig{
+ KeyPrefix: "cache",
+ Client: client,
+ }),
+ InvalidateWhenUpdate: true, // when you create/update/delete objects, invalidate cache
+ CacheTTL: 10000, // 5000 ms
+ CacheMaxItemCnt: 0, // if length of objects retrieved one single time
+ AsyncWrite: true, // async write to cache
+ DebugMode: false,
+ DisableCachePenetrationProtect: true, // disable cache penetration protect
+ })
+ if err != nil {
+ panic(err)
+ }
+ err = db.Use(gormCache)
+ if err != nil {
+ panic(err)
+ }
+
+ return db, useDB
+}
+
+func Migrate(db *gorm.DB) {
+ err := db.AutoMigrate(
+ &model.ScaStorageFace{})
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/app/aisvc/model/mysql/query/gen.go b/app/aisvc/model/mysql/query/gen.go
new file mode 100644
index 0000000..7539f29
--- /dev/null
+++ b/app/aisvc/model/mysql/query/gen.go
@@ -0,0 +1,103 @@
+// 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"
+ "database/sql"
+
+ "gorm.io/gorm"
+
+ "gorm.io/gen"
+
+ "gorm.io/plugin/dbresolver"
+)
+
+var (
+ Q = new(Query)
+ ScaStorageFace *scaStorageFace
+)
+
+func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
+ *Q = *Use(db, opts...)
+ ScaStorageFace = &Q.ScaStorageFace
+}
+
+func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
+ return &Query{
+ db: db,
+ ScaStorageFace: newScaStorageFace(db, opts...),
+ }
+}
+
+type Query struct {
+ db *gorm.DB
+
+ ScaStorageFace scaStorageFace
+}
+
+func (q *Query) Available() bool { return q.db != nil }
+
+func (q *Query) clone(db *gorm.DB) *Query {
+ return &Query{
+ db: db,
+ ScaStorageFace: q.ScaStorageFace.clone(db),
+ }
+}
+
+func (q *Query) ReadDB() *Query {
+ return q.ReplaceDB(q.db.Clauses(dbresolver.Read))
+}
+
+func (q *Query) WriteDB() *Query {
+ return q.ReplaceDB(q.db.Clauses(dbresolver.Write))
+}
+
+func (q *Query) ReplaceDB(db *gorm.DB) *Query {
+ return &Query{
+ db: db,
+ ScaStorageFace: q.ScaStorageFace.replaceDB(db),
+ }
+}
+
+type queryCtx struct {
+ ScaStorageFace IScaStorageFaceDo
+}
+
+func (q *Query) WithContext(ctx context.Context) *queryCtx {
+ return &queryCtx{
+ ScaStorageFace: q.ScaStorageFace.WithContext(ctx),
+ }
+}
+
+func (q *Query) Transaction(fc func(tx *Query) error, opts ...*sql.TxOptions) error {
+ return q.db.Transaction(func(tx *gorm.DB) error { return fc(q.clone(tx)) }, opts...)
+}
+
+func (q *Query) Begin(opts ...*sql.TxOptions) *QueryTx {
+ tx := q.db.Begin(opts...)
+ return &QueryTx{Query: q.clone(tx), Error: tx.Error}
+}
+
+type QueryTx struct {
+ *Query
+ Error error
+}
+
+func (q *QueryTx) Commit() error {
+ return q.db.Commit().Error
+}
+
+func (q *QueryTx) Rollback() error {
+ return q.db.Rollback().Error
+}
+
+func (q *QueryTx) SavePoint(name string) error {
+ return q.db.SavePoint(name).Error
+}
+
+func (q *QueryTx) RollbackTo(name string) error {
+ return q.db.RollbackTo(name).Error
+}
diff --git a/app/aisvc/model/mysql/query/sca_storage_face.gen.go b/app/aisvc/model/mysql/query/sca_storage_face.gen.go
new file mode 100644
index 0000000..7e4c145
--- /dev/null
+++ b/app/aisvc/model/mysql/query/sca_storage_face.gen.go
@@ -0,0 +1,413 @@
+// 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/aisvc/model/mysql/model"
+)
+
+func newScaStorageFace(db *gorm.DB, opts ...gen.DOOption) scaStorageFace {
+ _scaStorageFace := scaStorageFace{}
+
+ _scaStorageFace.scaStorageFaceDo.UseDB(db, opts...)
+ _scaStorageFace.scaStorageFaceDo.UseModel(&model.ScaStorageFace{})
+
+ tableName := _scaStorageFace.scaStorageFaceDo.TableName()
+ _scaStorageFace.ALL = field.NewAsterisk(tableName)
+ _scaStorageFace.ID = field.NewInt64(tableName, "id")
+ _scaStorageFace.UserID = field.NewString(tableName, "user_id")
+ _scaStorageFace.FaceName = field.NewString(tableName, "face_name")
+ _scaStorageFace.FaceVector = field.NewString(tableName, "face_vector")
+ _scaStorageFace.FaceImagePath = field.NewString(tableName, "face_image_path")
+ _scaStorageFace.FaceType = field.NewString(tableName, "face_type")
+ _scaStorageFace.CreatedAt = field.NewTime(tableName, "created_at")
+ _scaStorageFace.UpdatedAt = field.NewTime(tableName, "updated_at")
+ _scaStorageFace.DeletedAt = field.NewField(tableName, "deleted_at")
+
+ _scaStorageFace.fillFieldMap()
+
+ return _scaStorageFace
+}
+
+// scaStorageFace 人脸特征向量表
+type scaStorageFace struct {
+ scaStorageFaceDo
+
+ ALL field.Asterisk
+ ID field.Int64 // 主键
+ UserID field.String // 用户ID
+ FaceName field.String // 人脸名称
+ FaceVector field.String // 人脸特征向量
+ FaceImagePath field.String // 人脸图像路径
+ FaceType field.String // 人脸类型标识
+ CreatedAt field.Time // 创建时间
+ UpdatedAt field.Time // 更新时间
+ DeletedAt field.Field // 删除时间
+
+ fieldMap map[string]field.Expr
+}
+
+func (s scaStorageFace) Table(newTableName string) *scaStorageFace {
+ s.scaStorageFaceDo.UseTable(newTableName)
+ return s.updateTableName(newTableName)
+}
+
+func (s scaStorageFace) As(alias string) *scaStorageFace {
+ s.scaStorageFaceDo.DO = *(s.scaStorageFaceDo.As(alias).(*gen.DO))
+ return s.updateTableName(alias)
+}
+
+func (s *scaStorageFace) updateTableName(table string) *scaStorageFace {
+ s.ALL = field.NewAsterisk(table)
+ s.ID = field.NewInt64(table, "id")
+ s.UserID = field.NewString(table, "user_id")
+ s.FaceName = field.NewString(table, "face_name")
+ s.FaceVector = field.NewString(table, "face_vector")
+ s.FaceImagePath = field.NewString(table, "face_image_path")
+ s.FaceType = field.NewString(table, "face_type")
+ 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 *scaStorageFace) 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 *scaStorageFace) fillFieldMap() {
+ s.fieldMap = make(map[string]field.Expr, 9)
+ s.fieldMap["id"] = s.ID
+ s.fieldMap["user_id"] = s.UserID
+ s.fieldMap["face_name"] = s.FaceName
+ s.fieldMap["face_vector"] = s.FaceVector
+ s.fieldMap["face_image_path"] = s.FaceImagePath
+ s.fieldMap["face_type"] = s.FaceType
+ s.fieldMap["created_at"] = s.CreatedAt
+ s.fieldMap["updated_at"] = s.UpdatedAt
+ s.fieldMap["deleted_at"] = s.DeletedAt
+}
+
+func (s scaStorageFace) clone(db *gorm.DB) scaStorageFace {
+ s.scaStorageFaceDo.ReplaceConnPool(db.Statement.ConnPool)
+ return s
+}
+
+func (s scaStorageFace) replaceDB(db *gorm.DB) scaStorageFace {
+ s.scaStorageFaceDo.ReplaceDB(db)
+ return s
+}
+
+type scaStorageFaceDo struct{ gen.DO }
+
+type IScaStorageFaceDo interface {
+ gen.SubQuery
+ Debug() IScaStorageFaceDo
+ WithContext(ctx context.Context) IScaStorageFaceDo
+ WithResult(fc func(tx gen.Dao)) gen.ResultInfo
+ ReplaceDB(db *gorm.DB)
+ ReadDB() IScaStorageFaceDo
+ WriteDB() IScaStorageFaceDo
+ As(alias string) gen.Dao
+ Session(config *gorm.Session) IScaStorageFaceDo
+ Columns(cols ...field.Expr) gen.Columns
+ Clauses(conds ...clause.Expression) IScaStorageFaceDo
+ Not(conds ...gen.Condition) IScaStorageFaceDo
+ Or(conds ...gen.Condition) IScaStorageFaceDo
+ Select(conds ...field.Expr) IScaStorageFaceDo
+ Where(conds ...gen.Condition) IScaStorageFaceDo
+ Order(conds ...field.Expr) IScaStorageFaceDo
+ Distinct(cols ...field.Expr) IScaStorageFaceDo
+ Omit(cols ...field.Expr) IScaStorageFaceDo
+ Join(table schema.Tabler, on ...field.Expr) IScaStorageFaceDo
+ LeftJoin(table schema.Tabler, on ...field.Expr) IScaStorageFaceDo
+ RightJoin(table schema.Tabler, on ...field.Expr) IScaStorageFaceDo
+ Group(cols ...field.Expr) IScaStorageFaceDo
+ Having(conds ...gen.Condition) IScaStorageFaceDo
+ Limit(limit int) IScaStorageFaceDo
+ Offset(offset int) IScaStorageFaceDo
+ Count() (count int64, err error)
+ Scopes(funcs ...func(gen.Dao) gen.Dao) IScaStorageFaceDo
+ Unscoped() IScaStorageFaceDo
+ Create(values ...*model.ScaStorageFace) error
+ CreateInBatches(values []*model.ScaStorageFace, batchSize int) error
+ Save(values ...*model.ScaStorageFace) error
+ First() (*model.ScaStorageFace, error)
+ Take() (*model.ScaStorageFace, error)
+ Last() (*model.ScaStorageFace, error)
+ Find() ([]*model.ScaStorageFace, error)
+ FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.ScaStorageFace, err error)
+ FindInBatches(result *[]*model.ScaStorageFace, batchSize int, fc func(tx gen.Dao, batch int) error) error
+ Pluck(column field.Expr, dest interface{}) error
+ Delete(...*model.ScaStorageFace) (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) IScaStorageFaceDo
+ Assign(attrs ...field.AssignExpr) IScaStorageFaceDo
+ Joins(fields ...field.RelationField) IScaStorageFaceDo
+ Preload(fields ...field.RelationField) IScaStorageFaceDo
+ FirstOrInit() (*model.ScaStorageFace, error)
+ FirstOrCreate() (*model.ScaStorageFace, error)
+ FindByPage(offset int, limit int) (result []*model.ScaStorageFace, 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) IScaStorageFaceDo
+ UnderlyingDB() *gorm.DB
+ schema.Tabler
+}
+
+func (s scaStorageFaceDo) Debug() IScaStorageFaceDo {
+ return s.withDO(s.DO.Debug())
+}
+
+func (s scaStorageFaceDo) WithContext(ctx context.Context) IScaStorageFaceDo {
+ return s.withDO(s.DO.WithContext(ctx))
+}
+
+func (s scaStorageFaceDo) ReadDB() IScaStorageFaceDo {
+ return s.Clauses(dbresolver.Read)
+}
+
+func (s scaStorageFaceDo) WriteDB() IScaStorageFaceDo {
+ return s.Clauses(dbresolver.Write)
+}
+
+func (s scaStorageFaceDo) Session(config *gorm.Session) IScaStorageFaceDo {
+ return s.withDO(s.DO.Session(config))
+}
+
+func (s scaStorageFaceDo) Clauses(conds ...clause.Expression) IScaStorageFaceDo {
+ return s.withDO(s.DO.Clauses(conds...))
+}
+
+func (s scaStorageFaceDo) Returning(value interface{}, columns ...string) IScaStorageFaceDo {
+ return s.withDO(s.DO.Returning(value, columns...))
+}
+
+func (s scaStorageFaceDo) Not(conds ...gen.Condition) IScaStorageFaceDo {
+ return s.withDO(s.DO.Not(conds...))
+}
+
+func (s scaStorageFaceDo) Or(conds ...gen.Condition) IScaStorageFaceDo {
+ return s.withDO(s.DO.Or(conds...))
+}
+
+func (s scaStorageFaceDo) Select(conds ...field.Expr) IScaStorageFaceDo {
+ return s.withDO(s.DO.Select(conds...))
+}
+
+func (s scaStorageFaceDo) Where(conds ...gen.Condition) IScaStorageFaceDo {
+ return s.withDO(s.DO.Where(conds...))
+}
+
+func (s scaStorageFaceDo) Order(conds ...field.Expr) IScaStorageFaceDo {
+ return s.withDO(s.DO.Order(conds...))
+}
+
+func (s scaStorageFaceDo) Distinct(cols ...field.Expr) IScaStorageFaceDo {
+ return s.withDO(s.DO.Distinct(cols...))
+}
+
+func (s scaStorageFaceDo) Omit(cols ...field.Expr) IScaStorageFaceDo {
+ return s.withDO(s.DO.Omit(cols...))
+}
+
+func (s scaStorageFaceDo) Join(table schema.Tabler, on ...field.Expr) IScaStorageFaceDo {
+ return s.withDO(s.DO.Join(table, on...))
+}
+
+func (s scaStorageFaceDo) LeftJoin(table schema.Tabler, on ...field.Expr) IScaStorageFaceDo {
+ return s.withDO(s.DO.LeftJoin(table, on...))
+}
+
+func (s scaStorageFaceDo) RightJoin(table schema.Tabler, on ...field.Expr) IScaStorageFaceDo {
+ return s.withDO(s.DO.RightJoin(table, on...))
+}
+
+func (s scaStorageFaceDo) Group(cols ...field.Expr) IScaStorageFaceDo {
+ return s.withDO(s.DO.Group(cols...))
+}
+
+func (s scaStorageFaceDo) Having(conds ...gen.Condition) IScaStorageFaceDo {
+ return s.withDO(s.DO.Having(conds...))
+}
+
+func (s scaStorageFaceDo) Limit(limit int) IScaStorageFaceDo {
+ return s.withDO(s.DO.Limit(limit))
+}
+
+func (s scaStorageFaceDo) Offset(offset int) IScaStorageFaceDo {
+ return s.withDO(s.DO.Offset(offset))
+}
+
+func (s scaStorageFaceDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IScaStorageFaceDo {
+ return s.withDO(s.DO.Scopes(funcs...))
+}
+
+func (s scaStorageFaceDo) Unscoped() IScaStorageFaceDo {
+ return s.withDO(s.DO.Unscoped())
+}
+
+func (s scaStorageFaceDo) Create(values ...*model.ScaStorageFace) error {
+ if len(values) == 0 {
+ return nil
+ }
+ return s.DO.Create(values)
+}
+
+func (s scaStorageFaceDo) CreateInBatches(values []*model.ScaStorageFace, 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 scaStorageFaceDo) Save(values ...*model.ScaStorageFace) error {
+ if len(values) == 0 {
+ return nil
+ }
+ return s.DO.Save(values)
+}
+
+func (s scaStorageFaceDo) First() (*model.ScaStorageFace, error) {
+ if result, err := s.DO.First(); err != nil {
+ return nil, err
+ } else {
+ return result.(*model.ScaStorageFace), nil
+ }
+}
+
+func (s scaStorageFaceDo) Take() (*model.ScaStorageFace, error) {
+ if result, err := s.DO.Take(); err != nil {
+ return nil, err
+ } else {
+ return result.(*model.ScaStorageFace), nil
+ }
+}
+
+func (s scaStorageFaceDo) Last() (*model.ScaStorageFace, error) {
+ if result, err := s.DO.Last(); err != nil {
+ return nil, err
+ } else {
+ return result.(*model.ScaStorageFace), nil
+ }
+}
+
+func (s scaStorageFaceDo) Find() ([]*model.ScaStorageFace, error) {
+ result, err := s.DO.Find()
+ return result.([]*model.ScaStorageFace), err
+}
+
+func (s scaStorageFaceDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.ScaStorageFace, err error) {
+ buf := make([]*model.ScaStorageFace, 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 scaStorageFaceDo) FindInBatches(result *[]*model.ScaStorageFace, batchSize int, fc func(tx gen.Dao, batch int) error) error {
+ return s.DO.FindInBatches(result, batchSize, fc)
+}
+
+func (s scaStorageFaceDo) Attrs(attrs ...field.AssignExpr) IScaStorageFaceDo {
+ return s.withDO(s.DO.Attrs(attrs...))
+}
+
+func (s scaStorageFaceDo) Assign(attrs ...field.AssignExpr) IScaStorageFaceDo {
+ return s.withDO(s.DO.Assign(attrs...))
+}
+
+func (s scaStorageFaceDo) Joins(fields ...field.RelationField) IScaStorageFaceDo {
+ for _, _f := range fields {
+ s = *s.withDO(s.DO.Joins(_f))
+ }
+ return &s
+}
+
+func (s scaStorageFaceDo) Preload(fields ...field.RelationField) IScaStorageFaceDo {
+ for _, _f := range fields {
+ s = *s.withDO(s.DO.Preload(_f))
+ }
+ return &s
+}
+
+func (s scaStorageFaceDo) FirstOrInit() (*model.ScaStorageFace, error) {
+ if result, err := s.DO.FirstOrInit(); err != nil {
+ return nil, err
+ } else {
+ return result.(*model.ScaStorageFace), nil
+ }
+}
+
+func (s scaStorageFaceDo) FirstOrCreate() (*model.ScaStorageFace, error) {
+ if result, err := s.DO.FirstOrCreate(); err != nil {
+ return nil, err
+ } else {
+ return result.(*model.ScaStorageFace), nil
+ }
+}
+
+func (s scaStorageFaceDo) FindByPage(offset int, limit int) (result []*model.ScaStorageFace, 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 scaStorageFaceDo) 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 scaStorageFaceDo) Scan(result interface{}) (err error) {
+ return s.DO.Scan(result)
+}
+
+func (s scaStorageFaceDo) Delete(models ...*model.ScaStorageFace) (result gen.ResultInfo, err error) {
+ return s.DO.Delete(models)
+}
+
+func (s *scaStorageFaceDo) withDO(do gen.Dao) *scaStorageFaceDo {
+ s.DO = *do.(*gen.DO)
+ return s
+}
diff --git a/app/aisvc/resources/models/face/dlib_face_recognition_resnet_model_v1.dat b/app/aisvc/resources/models/face/dlib_face_recognition_resnet_model_v1.dat
new file mode 100644
index 0000000..ddb5158
Binary files /dev/null and b/app/aisvc/resources/models/face/dlib_face_recognition_resnet_model_v1.dat differ
diff --git a/app/aisvc/resources/models/face/mmod_human_face_detector.dat b/app/aisvc/resources/models/face/mmod_human_face_detector.dat
new file mode 100644
index 0000000..f112a0a
Binary files /dev/null and b/app/aisvc/resources/models/face/mmod_human_face_detector.dat differ
diff --git a/app/aisvc/resources/models/face/shape_predictor_5_face_landmarks.dat b/app/aisvc/resources/models/face/shape_predictor_5_face_landmarks.dat
new file mode 100644
index 0000000..67878ed
Binary files /dev/null and b/app/aisvc/resources/models/face/shape_predictor_5_face_landmarks.dat differ
diff --git a/app/aisvc/rpc/aisvc.go b/app/aisvc/rpc/aisvc.go
new file mode 100644
index 0000000..ab4890c
--- /dev/null
+++ b/app/aisvc/rpc/aisvc.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+
+ "schisandra-album-cloud-microservices/app/aisvc/rpc/internal/config"
+ aiserviceServer "schisandra-album-cloud-microservices/app/aisvc/rpc/internal/server/aiservice"
+ "schisandra-album-cloud-microservices/app/aisvc/rpc/internal/svc"
+ "schisandra-album-cloud-microservices/app/aisvc/rpc/pb"
+
+ "github.com/zeromicro/go-zero/core/conf"
+ "github.com/zeromicro/go-zero/core/service"
+ "github.com/zeromicro/go-zero/zrpc"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/reflection"
+)
+
+var configFile = flag.String("f", "rpc/etc/aisvc.yaml", "the config file")
+
+func main() {
+ flag.Parse()
+
+ var c config.Config
+ conf.MustLoad(*configFile, &c)
+ ctx := svc.NewServiceContext(c)
+
+ s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
+ pb.RegisterAiServiceServer(grpcServer, aiserviceServer.NewAiServiceServer(ctx))
+
+ if c.Mode == service.DevMode || c.Mode == service.TestMode {
+ reflection.Register(grpcServer)
+ }
+ })
+ defer s.Stop()
+
+ fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
+ s.Start()
+}
diff --git a/app/aisvc/rpc/aisvc.proto b/app/aisvc/rpc/aisvc.proto
new file mode 100644
index 0000000..73373ae
--- /dev/null
+++ b/app/aisvc/rpc/aisvc.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+package ai;
+option go_package = "./pb";
+
+
+
+message FaceRecognitionRequest {
+ bytes face = 1;
+ string user_id = 2;
+}
+message FaceRecognitionResponse {
+ int64 face_id = 1;
+}
+service AiService {
+ // FaceRecognition
+ rpc FaceRecognition (FaceRecognitionRequest) returns (FaceRecognitionResponse);
+}
\ No newline at end of file
diff --git a/app/aisvc/rpc/client/aiservice/ai_service.go b/app/aisvc/rpc/client/aiservice/ai_service.go
new file mode 100644
index 0000000..70dce29
--- /dev/null
+++ b/app/aisvc/rpc/client/aiservice/ai_service.go
@@ -0,0 +1,40 @@
+// Code generated by goctl. DO NOT EDIT.
+// goctl 1.7.3
+// Source: aisvc.proto
+
+package aiservice
+
+import (
+ "context"
+
+ "schisandra-album-cloud-microservices/app/aisvc/rpc/pb"
+
+ "github.com/zeromicro/go-zero/zrpc"
+ "google.golang.org/grpc"
+)
+
+type (
+ FaceRecognitionRequest = pb.FaceRecognitionRequest
+ FaceRecognitionResponse = pb.FaceRecognitionResponse
+
+ AiService interface {
+ // FaceRecognition
+ FaceRecognition(ctx context.Context, in *FaceRecognitionRequest, opts ...grpc.CallOption) (*FaceRecognitionResponse, error)
+ }
+
+ defaultAiService struct {
+ cli zrpc.Client
+ }
+)
+
+func NewAiService(cli zrpc.Client) AiService {
+ return &defaultAiService{
+ cli: cli,
+ }
+}
+
+// FaceRecognition
+func (m *defaultAiService) FaceRecognition(ctx context.Context, in *FaceRecognitionRequest, opts ...grpc.CallOption) (*FaceRecognitionResponse, error) {
+ client := pb.NewAiServiceClient(m.cli.Conn())
+ return client.FaceRecognition(ctx, in, opts...)
+}
diff --git a/app/aisvc/rpc/etc/aisvc.yaml b/app/aisvc/rpc/etc/aisvc.yaml
new file mode 100644
index 0000000..42a3336
--- /dev/null
+++ b/app/aisvc/rpc/etc/aisvc.yaml
@@ -0,0 +1,22 @@
+Name: aisvc.rpc
+ListenOn: 0.0.0.0:8080
+Etcd:
+ Hosts:
+ - 127.0.0.1:2379
+ Key: aisvc.rpc
+# MySQL 配置
+Mysql:
+ # 数据源dsn
+ DataSource: root:LDQ20020618xxx@tcp(1.95.0.111:3306)/schisandra-cloud-album?charset=utf8mb4&parseTime=True&loc=Local
+ # 最大连接数
+ MaxOpenConn: 10
+ # 最大空闲连接数
+ MaxIdleConn: 5
+# RedisConf 配置
+RedisConf:
+ # Redis 地址
+ Host: 1.95.0.111:6379
+ # Redis 密码
+ Pass: LDQ20020618xxx
+ # Redis 数据库
+ DB: 0
diff --git a/app/aisvc/rpc/generate.go b/app/aisvc/rpc/generate.go
new file mode 100644
index 0000000..aac107a
--- /dev/null
+++ b/app/aisvc/rpc/generate.go
@@ -0,0 +1,3 @@
+package main
+
+//go:generate goctl rpc protoc aisvc.proto --go_out=. --go-grpc_out=. --zrpc_out=. --client=true -m --style=go_zero
diff --git a/app/aisvc/rpc/internal/config/config.go b/app/aisvc/rpc/internal/config/config.go
new file mode 100644
index 0000000..0adbbb6
--- /dev/null
+++ b/app/aisvc/rpc/internal/config/config.go
@@ -0,0 +1,18 @@
+package config
+
+import "github.com/zeromicro/go-zero/zrpc"
+
+type Config struct {
+ zrpc.RpcServerConf
+
+ Mysql struct {
+ DataSource string
+ MaxOpenConn int
+ MaxIdleConn int
+ }
+ RedisConf struct {
+ Host string
+ Pass string
+ DB int
+ }
+}
diff --git a/app/aisvc/rpc/internal/logic/aiservice/face_recognition_logic.go b/app/aisvc/rpc/internal/logic/aiservice/face_recognition_logic.go
new file mode 100644
index 0000000..200a475
--- /dev/null
+++ b/app/aisvc/rpc/internal/logic/aiservice/face_recognition_logic.go
@@ -0,0 +1,355 @@
+package aiservicelogic
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "github.com/Kagami/go-face"
+ "github.com/ccpwcn/kgo"
+ "github.com/zeromicro/go-zero/core/logx"
+ "image"
+ "image/jpeg"
+ "os"
+ "path/filepath"
+ "schisandra-album-cloud-microservices/app/aisvc/model/mysql/model"
+ "schisandra-album-cloud-microservices/app/aisvc/rpc/internal/svc"
+ "schisandra-album-cloud-microservices/app/aisvc/rpc/pb"
+ "schisandra-album-cloud-microservices/common/constant"
+ "strconv"
+ "sync"
+ "time"
+)
+
+type FaceRecognitionLogic struct {
+ ctx context.Context
+ svcCtx *svc.ServiceContext
+ logx.Logger
+ directoryCache sync.Map
+ wg sync.WaitGroup
+ mu sync.Mutex
+}
+
+func NewFaceRecognitionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FaceRecognitionLogic {
+ return &FaceRecognitionLogic{
+ ctx: ctx,
+ svcCtx: svcCtx,
+ Logger: logx.WithContext(ctx),
+ directoryCache: sync.Map{},
+ wg: sync.WaitGroup{},
+ mu: sync.Mutex{},
+ }
+}
+
+// FaceRecognition 人脸识别
+func (l *FaceRecognitionLogic) FaceRecognition(in *pb.FaceRecognitionRequest) (*pb.FaceRecognitionResponse, error) {
+ // 提取人脸特征
+ faceFeatures, err := l.svcCtx.FaceRecognizer.RecognizeSingle(in.GetFace())
+ if err != nil {
+ return nil, err
+ }
+ if faceFeatures == nil {
+ return nil, nil
+ }
+
+ hashKey := fmt.Sprintf("user:%s:faces", in.GetUserId())
+ // 从 Redis 加载人脸数据
+ samples, ids, err := l.loadFacesFromRedisHash(hashKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to query Redis: %v", err)
+ }
+ // 如果缓存中没有数据,则查询数据库
+ if len(samples) == 0 {
+ samples, ids, err = l.loadExistingFaces(in.GetUserId())
+ if err != nil {
+ return nil, err
+ }
+
+ // 如果数据库也没有数据,直接保存当前人脸
+ if len(samples) == 0 || len(ids) == 0 {
+ return l.saveNewFace(in, faceFeatures, hashKey)
+ }
+
+ // 将数据写入 Redis
+ err = l.cacheFacesToRedisHash(hashKey, samples, ids)
+ if err != nil {
+ return nil, fmt.Errorf("failed to cache faces to Redis: %v", err)
+ }
+ }
+
+ // 设置人脸特征
+ l.svcCtx.FaceRecognizer.SetSamples(samples, ids)
+
+ // 人脸分类
+ classify := l.svcCtx.FaceRecognizer.ClassifyThreshold(faceFeatures.Descriptor, 0.6)
+ if classify >= 0 {
+ return &pb.FaceRecognitionResponse{
+ FaceId: int64(ids[classify]),
+ }, nil
+ }
+
+ // 如果未找到匹配的人脸,则保存为新样本
+ return l.saveNewFace(in, faceFeatures, hashKey)
+}
+
+// 保存新的人脸样本到数据库和 Redis
+func (l *FaceRecognitionLogic) saveNewFace(in *pb.FaceRecognitionRequest, faceFeatures *face.Face, hashKey string) (*pb.FaceRecognitionResponse, error) {
+ // 人脸有效性判断 (大小必须大于50)
+ if !l.isFaceValid(faceFeatures.Rectangle) {
+ return nil, nil
+ }
+
+ // 保存人脸图片到本地
+ faceImagePath, err := l.saveCroppedFaceToLocal(in.GetFace(), faceFeatures.Rectangle, "face_samples", in.GetUserId())
+ if err != nil {
+ return nil, err
+ }
+
+ // 保存到数据库
+ storageFace, err := l.saveFaceToDatabase(in.GetUserId(), faceFeatures.Descriptor, faceImagePath)
+ if err != nil {
+ return nil, err
+ }
+
+ // 将新增数据写入 Redis
+ err = l.appendFaceToRedisHash(hashKey, storageFace.ID, faceFeatures.Descriptor)
+ if err != nil {
+ return nil, fmt.Errorf("failed to append face to Redis: %v", err)
+ }
+
+ return &pb.FaceRecognitionResponse{
+ FaceId: storageFace.ID,
+ }, nil
+}
+
+// 加载数据库中的已有人脸
+func (l *FaceRecognitionLogic) loadExistingFaces(userId string) ([]face.Descriptor, []int32, error) {
+ if userId == "" {
+ return nil, nil, fmt.Errorf("user ID is required")
+ }
+ storageFace := l.svcCtx.DB.ScaStorageFace
+ existingFaces, err := storageFace.
+ Select(storageFace.FaceVector, storageFace.ID).
+ Where(storageFace.UserID.Eq(userId), storageFace.FaceType.Eq(constant.FaceTypeSample)).
+ Find()
+ if err != nil {
+ return nil, nil, err
+ }
+ if len(existingFaces) == 0 {
+ return nil, nil, nil
+ }
+
+ var samples []face.Descriptor
+ var ids []int32
+ // 使用并发处理每个数据
+ for _, existingFace := range existingFaces {
+ l.wg.Add(1)
+ go func(faceData *model.ScaStorageFace) {
+ defer l.wg.Done()
+
+ var descriptor face.Descriptor
+ if err = json.Unmarshal([]byte(faceData.FaceVector), &descriptor); err != nil {
+ l.Errorf("failed to unmarshal face vector: %v", err)
+ return
+ }
+ // 使用锁来保证并发访问时对切片的安全操作
+ l.mu.Lock()
+ samples = append(samples, descriptor)
+ ids = append(ids, int32(faceData.ID))
+ l.mu.Unlock()
+ }(existingFace)
+ }
+ l.wg.Wait()
+ return samples, ids, nil
+}
+
+const (
+ minFaceWidth = 50 // 最小允许的人脸宽度
+ minFaceHeight = 50 // 最小允许的人脸高度
+)
+
+// 判断人脸是否有效
+func (l *FaceRecognitionLogic) isFaceValid(rect image.Rectangle) bool {
+ width := rect.Dx()
+ height := rect.Dy()
+ return width >= minFaceWidth && height >= minFaceHeight
+}
+
+// 保存人脸特征和路径到数据库
+func (l *FaceRecognitionLogic) saveFaceToDatabase(userId string, descriptor face.Descriptor, faceImagePath string) (*model.ScaStorageFace, error) {
+ jsonBytes, err := json.Marshal(descriptor)
+ if err != nil {
+ return nil, err
+ }
+ storageFace := model.ScaStorageFace{
+ FaceVector: string(jsonBytes),
+ FaceImagePath: faceImagePath,
+ FaceType: constant.FaceTypeSample,
+ UserID: userId,
+ }
+ err = l.svcCtx.DB.ScaStorageFace.Create(&storageFace)
+ if err != nil {
+ return nil, err
+ }
+ return &storageFace, nil
+}
+
+func (l *FaceRecognitionLogic) saveCroppedFaceToLocal(faceImage []byte, rect image.Rectangle, baseSavePath string, userID string) (string, error) {
+ // 动态生成用户目录和时间分级目录
+ subDir := filepath.Join(baseSavePath, userID, time.Now().Format("2006/01")) // 格式://YYYY/MM
+
+ // 缓存目录检查,避免重复调用 os.MkdirAll
+ if !l.isDirectoryCached(subDir) {
+ if err := os.MkdirAll(subDir, os.ModePerm); err != nil {
+ return "", fmt.Errorf("failed to create directory: %w", err)
+ }
+ l.cacheDirectory(subDir) // 缓存已创建的目录路径
+ }
+
+ // 解码图像
+ img, _, err := image.Decode(bytes.NewReader(faceImage))
+ if err != nil {
+ return "", fmt.Errorf("image decode failed: %w", err)
+ }
+
+ // 获取图像边界
+ imgBounds := img.Bounds()
+ // 增加边距(比如 20 像素)
+ margin := 20
+ extendedRect := image.Rect(
+ max(rect.Min.X-margin, imgBounds.Min.X), // 确保不超出左边界
+ max(rect.Min.Y-margin, imgBounds.Min.Y), // 确保不超出上边界
+ min(rect.Max.X+margin, imgBounds.Max.X), // 确保不超出右边界
+ min(rect.Max.Y+margin, imgBounds.Max.Y), // 确保不超出下边界
+ )
+ // 裁剪图像
+ croppedImage := img.(interface {
+ SubImage(r image.Rectangle) image.Image
+ }).SubImage(extendedRect)
+
+ // 生成唯一文件名(时间戳 + UUID)
+ fileName := fmt.Sprintf("%s_%s.jpg", time.Now().Format("20060102_150405"), kgo.SimpleUuid())
+ outputPath := filepath.Join(subDir, fileName)
+
+ // 写入文件
+ if err = l.writeImageToFile(outputPath, croppedImage); err != nil {
+ return "", err
+ }
+ return outputPath, nil
+}
+
+// 判断目录是否已缓存
+func (l *FaceRecognitionLogic) isDirectoryCached(dir string) bool {
+ _, exists := l.directoryCache.Load(dir)
+ return exists
+}
+
+// 缓存目录
+func (l *FaceRecognitionLogic) cacheDirectory(dir string) {
+ l.directoryCache.Store(dir, struct{}{})
+}
+
+// 将图像写入文件
+func (l *FaceRecognitionLogic) writeImageToFile(path string, img image.Image) error {
+ file, err := os.Create(path)
+ if err != nil {
+ return fmt.Errorf("failed to create file: %w", err)
+ }
+ defer func(file *os.File) {
+ _ = file.Close()
+ }(file)
+
+ if err = jpeg.Encode(file, img, nil); err != nil {
+ return fmt.Errorf("failed to encode and save image: %w", err)
+ }
+ return nil
+}
+
+// 从 Redis 的 Hash 中加载人脸数据
+func (l *FaceRecognitionLogic) loadFacesFromRedisHash(hashKey string) ([]face.Descriptor, []int32, error) {
+ // 从 Redis 获取 Hash 的所有字段和值
+ data, err := l.svcCtx.RedisClient.HGetAll(l.ctx, hashKey).Result()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var samples []face.Descriptor
+ var ids []int32
+ for idStr, descriptorStr := range data {
+ var descriptor face.Descriptor
+ if err = json.Unmarshal([]byte(descriptorStr), &descriptor); err != nil {
+ return nil, nil, err
+ }
+
+ // 转换 ID 为 int32
+ id, err := parseInt32(idStr)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ samples = append(samples, descriptor)
+ ids = append(ids, id)
+ }
+ return samples, ids, nil
+}
+
+// 将人脸数据写入 Redis 的 Hash
+func (l *FaceRecognitionLogic) cacheFacesToRedisHash(hashKey string, samples []face.Descriptor, ids []int32) error {
+ // 开启事务
+ pipe := l.svcCtx.RedisClient.Pipeline()
+
+ for i := range samples {
+ descriptorData, err := json.Marshal(samples[i])
+ if err != nil {
+ return err
+ }
+
+ // 使用 HSET 设置 Hash 字段和值
+ pipe.HSet(l.ctx, hashKey, fmt.Sprintf("%d", ids[i]), descriptorData)
+ }
+
+ // 设置缓存过期时间
+ pipe.Expire(l.ctx, hashKey, 3600*time.Second)
+
+ _, err := pipe.Exec(l.ctx)
+ return err
+}
+
+// 将新增的人脸数据追加到 Redis 的 Hash
+func (l *FaceRecognitionLogic) appendFaceToRedisHash(hashKey string, id int64, descriptor face.Descriptor) error {
+ descriptorData, err := json.Marshal(descriptor)
+ if err != nil {
+ return err
+ }
+
+ // 追加数据到 Hash
+ err = l.svcCtx.RedisClient.HSet(l.ctx, hashKey, fmt.Sprintf("%d", id), descriptorData).Err()
+ if err != nil {
+ return err
+ }
+ // 检查是否已设置过期时间
+ ttl, err := l.svcCtx.RedisClient.TTL(l.ctx, hashKey).Result()
+ if err != nil {
+ return err
+ }
+
+ // 如果未设置过期时间或已经过期,设置固定过期时间
+ if ttl < 0 {
+ err = l.svcCtx.RedisClient.Expire(l.ctx, hashKey, 3600*time.Second).Err()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// 辅助函数:字符串转换为 int32
+func parseInt32(s string) (int32, error) {
+ var i int64
+ var err error
+ if i, err = strconv.ParseInt(s, 10, 32); err != nil {
+ return 0, err
+ }
+ return int32(i), nil
+}
diff --git a/app/aisvc/rpc/internal/server/aiservice/ai_service_server.go b/app/aisvc/rpc/internal/server/aiservice/ai_service_server.go
new file mode 100644
index 0000000..0e69b19
--- /dev/null
+++ b/app/aisvc/rpc/internal/server/aiservice/ai_service_server.go
@@ -0,0 +1,30 @@
+// Code generated by goctl. DO NOT EDIT.
+// goctl 1.7.3
+// Source: aisvc.proto
+
+package server
+
+import (
+ "context"
+
+ "schisandra-album-cloud-microservices/app/aisvc/rpc/internal/logic/aiservice"
+ "schisandra-album-cloud-microservices/app/aisvc/rpc/internal/svc"
+ "schisandra-album-cloud-microservices/app/aisvc/rpc/pb"
+)
+
+type AiServiceServer struct {
+ svcCtx *svc.ServiceContext
+ pb.UnimplementedAiServiceServer
+}
+
+func NewAiServiceServer(svcCtx *svc.ServiceContext) *AiServiceServer {
+ return &AiServiceServer{
+ svcCtx: svcCtx,
+ }
+}
+
+// FaceRecognition
+func (s *AiServiceServer) FaceRecognition(ctx context.Context, in *pb.FaceRecognitionRequest) (*pb.FaceRecognitionResponse, error) {
+ l := aiservicelogic.NewFaceRecognitionLogic(ctx, s.svcCtx)
+ return l.FaceRecognition(in)
+}
diff --git a/app/aisvc/rpc/internal/svc/service_context.go b/app/aisvc/rpc/internal/svc/service_context.go
new file mode 100644
index 0000000..c7dde9b
--- /dev/null
+++ b/app/aisvc/rpc/internal/svc/service_context.go
@@ -0,0 +1,29 @@
+package svc
+
+import (
+ "github.com/Kagami/go-face"
+ "github.com/redis/go-redis/v9"
+ "schisandra-album-cloud-microservices/app/aisvc/model/mysql"
+ "schisandra-album-cloud-microservices/app/aisvc/model/mysql/query"
+ "schisandra-album-cloud-microservices/app/aisvc/rpc/internal/config"
+ "schisandra-album-cloud-microservices/common/face_recognizer"
+ "schisandra-album-cloud-microservices/common/redisx"
+)
+
+type ServiceContext struct {
+ Config config.Config
+ FaceRecognizer *face.Recognizer
+ DB *query.Query
+ RedisClient *redis.Client
+}
+
+func NewServiceContext(c config.Config) *ServiceContext {
+ redisClient := redisx.NewRedis(c.RedisConf.Host, c.RedisConf.Pass, c.RedisConf.DB)
+ _, queryDB := mysql.NewMySQL(c.Mysql.DataSource, c.Mysql.MaxOpenConn, c.Mysql.MaxIdleConn, redisClient)
+ return &ServiceContext{
+ Config: c,
+ FaceRecognizer: face_recognizer.NewFaceRecognition(),
+ DB: queryDB,
+ RedisClient: redisClient,
+ }
+}
diff --git a/app/aisvc/rpc/pb/aisvc.pb.go b/app/aisvc/rpc/pb/aisvc.pb.go
new file mode 100644
index 0000000..b477062
--- /dev/null
+++ b/app/aisvc/rpc/pb/aisvc.pb.go
@@ -0,0 +1,192 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.35.1
+// protoc v3.19.4
+// source: aisvc.proto
+
+package pb
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type FaceRecognitionRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Face []byte `protobuf:"bytes,1,opt,name=face,proto3" json:"face,omitempty"`
+ UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
+}
+
+func (x *FaceRecognitionRequest) Reset() {
+ *x = FaceRecognitionRequest{}
+ mi := &file_aisvc_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *FaceRecognitionRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FaceRecognitionRequest) ProtoMessage() {}
+
+func (x *FaceRecognitionRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_aisvc_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FaceRecognitionRequest.ProtoReflect.Descriptor instead.
+func (*FaceRecognitionRequest) Descriptor() ([]byte, []int) {
+ return file_aisvc_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *FaceRecognitionRequest) GetFace() []byte {
+ if x != nil {
+ return x.Face
+ }
+ return nil
+}
+
+func (x *FaceRecognitionRequest) GetUserId() string {
+ if x != nil {
+ return x.UserId
+ }
+ return ""
+}
+
+type FaceRecognitionResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ FaceId int64 `protobuf:"varint,1,opt,name=face_id,json=faceId,proto3" json:"face_id,omitempty"`
+}
+
+func (x *FaceRecognitionResponse) Reset() {
+ *x = FaceRecognitionResponse{}
+ mi := &file_aisvc_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *FaceRecognitionResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FaceRecognitionResponse) ProtoMessage() {}
+
+func (x *FaceRecognitionResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_aisvc_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FaceRecognitionResponse.ProtoReflect.Descriptor instead.
+func (*FaceRecognitionResponse) Descriptor() ([]byte, []int) {
+ return file_aisvc_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *FaceRecognitionResponse) GetFaceId() int64 {
+ if x != nil {
+ return x.FaceId
+ }
+ return 0
+}
+
+var File_aisvc_proto protoreflect.FileDescriptor
+
+var file_aisvc_proto_rawDesc = []byte{
+ 0x0a, 0x0b, 0x61, 0x69, 0x73, 0x76, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x61,
+ 0x69, 0x22, 0x45, 0x0a, 0x16, 0x46, 0x61, 0x63, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x67, 0x6e, 0x69,
+ 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x66,
+ 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x66, 0x61, 0x63, 0x65, 0x12,
+ 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x32, 0x0a, 0x17, 0x46, 0x61, 0x63, 0x65,
+ 0x52, 0x65, 0x63, 0x6f, 0x67, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x66, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x66, 0x61, 0x63, 0x65, 0x49, 0x64, 0x32, 0x57, 0x0a, 0x09,
+ 0x41, 0x69, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x0f, 0x46, 0x61, 0x63,
+ 0x65, 0x52, 0x65, 0x63, 0x6f, 0x67, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x2e, 0x61,
+ 0x69, 0x2e, 0x46, 0x61, 0x63, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x67, 0x6e, 0x69, 0x74, 0x69, 0x6f,
+ 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x69, 0x2e, 0x46, 0x61,
+ 0x63, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x67, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_aisvc_proto_rawDescOnce sync.Once
+ file_aisvc_proto_rawDescData = file_aisvc_proto_rawDesc
+)
+
+func file_aisvc_proto_rawDescGZIP() []byte {
+ file_aisvc_proto_rawDescOnce.Do(func() {
+ file_aisvc_proto_rawDescData = protoimpl.X.CompressGZIP(file_aisvc_proto_rawDescData)
+ })
+ return file_aisvc_proto_rawDescData
+}
+
+var file_aisvc_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_aisvc_proto_goTypes = []any{
+ (*FaceRecognitionRequest)(nil), // 0: ai.FaceRecognitionRequest
+ (*FaceRecognitionResponse)(nil), // 1: ai.FaceRecognitionResponse
+}
+var file_aisvc_proto_depIdxs = []int32{
+ 0, // 0: ai.AiService.FaceRecognition:input_type -> ai.FaceRecognitionRequest
+ 1, // 1: ai.AiService.FaceRecognition:output_type -> ai.FaceRecognitionResponse
+ 1, // [1:2] is the sub-list for method output_type
+ 0, // [0:1] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_aisvc_proto_init() }
+func file_aisvc_proto_init() {
+ if File_aisvc_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_aisvc_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_aisvc_proto_goTypes,
+ DependencyIndexes: file_aisvc_proto_depIdxs,
+ MessageInfos: file_aisvc_proto_msgTypes,
+ }.Build()
+ File_aisvc_proto = out.File
+ file_aisvc_proto_rawDesc = nil
+ file_aisvc_proto_goTypes = nil
+ file_aisvc_proto_depIdxs = nil
+}
diff --git a/app/aisvc/rpc/pb/aisvc_grpc.pb.go b/app/aisvc/rpc/pb/aisvc_grpc.pb.go
new file mode 100644
index 0000000..d074ee3
--- /dev/null
+++ b/app/aisvc/rpc/pb/aisvc_grpc.pb.go
@@ -0,0 +1,123 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.5.1
+// - protoc v3.19.4
+// source: aisvc.proto
+
+package pb
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+ AiService_FaceRecognition_FullMethodName = "/ai.AiService/FaceRecognition"
+)
+
+// AiServiceClient is the client API for AiService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type AiServiceClient interface {
+ // FaceRecognition
+ FaceRecognition(ctx context.Context, in *FaceRecognitionRequest, opts ...grpc.CallOption) (*FaceRecognitionResponse, error)
+}
+
+type aiServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewAiServiceClient(cc grpc.ClientConnInterface) AiServiceClient {
+ return &aiServiceClient{cc}
+}
+
+func (c *aiServiceClient) FaceRecognition(ctx context.Context, in *FaceRecognitionRequest, opts ...grpc.CallOption) (*FaceRecognitionResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(FaceRecognitionResponse)
+ err := c.cc.Invoke(ctx, AiService_FaceRecognition_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// AiServiceServer is the server API for AiService service.
+// All implementations must embed UnimplementedAiServiceServer
+// for forward compatibility.
+type AiServiceServer interface {
+ // FaceRecognition
+ FaceRecognition(context.Context, *FaceRecognitionRequest) (*FaceRecognitionResponse, error)
+ mustEmbedUnimplementedAiServiceServer()
+}
+
+// UnimplementedAiServiceServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedAiServiceServer struct{}
+
+func (UnimplementedAiServiceServer) FaceRecognition(context.Context, *FaceRecognitionRequest) (*FaceRecognitionResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method FaceRecognition not implemented")
+}
+func (UnimplementedAiServiceServer) mustEmbedUnimplementedAiServiceServer() {}
+func (UnimplementedAiServiceServer) testEmbeddedByValue() {}
+
+// UnsafeAiServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to AiServiceServer will
+// result in compilation errors.
+type UnsafeAiServiceServer interface {
+ mustEmbedUnimplementedAiServiceServer()
+}
+
+func RegisterAiServiceServer(s grpc.ServiceRegistrar, srv AiServiceServer) {
+ // If the following call pancis, it indicates UnimplementedAiServiceServer was
+ // embedded by pointer and is nil. This will cause panics if an
+ // unimplemented method is ever invoked, so we test this at initialization
+ // time to prevent it from happening at runtime later due to I/O.
+ if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+ t.testEmbeddedByValue()
+ }
+ s.RegisterService(&AiService_ServiceDesc, srv)
+}
+
+func _AiService_FaceRecognition_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(FaceRecognitionRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AiServiceServer).FaceRecognition(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: AiService_FaceRecognition_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AiServiceServer).FaceRecognition(ctx, req.(*FaceRecognitionRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// AiService_ServiceDesc is the grpc.ServiceDesc for AiService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var AiService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "ai.AiService",
+ HandlerType: (*AiServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "FaceRecognition",
+ Handler: _AiService_FaceRecognition_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "aisvc.proto",
+}
diff --git a/app/auth/api/etc/auth.yaml b/app/auth/api/etc/auth.yaml
index be2cc8f..0ebba12 100644
--- a/app/auth/api/etc/auth.yaml
+++ b/app/auth/api/etc/auth.yaml
@@ -18,6 +18,12 @@ CpuThreshold: 900
MaxBytes: 10485760
# 是否打印详细日志
Verbose: false
+# RPC 配置
+AiSvcRpc:
+ Etcd:
+ Hosts:
+ - 127.0.0.1:2379
+ Key: aisvc.rpc
# 日志配置
Log:
# 服务名称
@@ -161,4 +167,7 @@ SMS:
# 短信宝用户账号
Username: landaiqing
# 短信宝用户密码
- Password: $LDQ20020618xxx$
\ No newline at end of file
+ Password: $LDQ20020618xxx$
+Map:
+ # 高德地图API Key
+ Key: 54823a494909959a9c8cd8af101bbc32
\ No newline at end of file
diff --git a/app/auth/api/internal/config/config.go b/app/auth/api/internal/config/config.go
index f33ecd1..624604f 100644
--- a/app/auth/api/internal/config/config.go
+++ b/app/auth/api/internal/config/config.go
@@ -1,10 +1,14 @@
package config
-import "github.com/zeromicro/go-zero/rest"
+import (
+ "github.com/zeromicro/go-zero/rest"
+ "github.com/zeromicro/go-zero/zrpc"
+)
type Config struct {
rest.RestConf
- Web struct {
+ AiSvcRpc zrpc.RpcClientConf
+ Web struct {
URL string
}
Auth struct {
@@ -67,4 +71,7 @@ type Config struct {
Password string
}
}
+ Map struct {
+ Key string
+ }
}
diff --git a/app/auth/api/internal/logic/storage/upload_file_logic.go b/app/auth/api/internal/logic/storage/upload_file_logic.go
index cbd4df2..b1b8863 100644
--- a/app/auth/api/internal/logic/storage/upload_file_logic.go
+++ b/app/auth/api/internal/logic/storage/upload_file_logic.go
@@ -2,12 +2,23 @@ package storage
import (
"context"
+ "encoding/json"
"errors"
+ "fmt"
"github.com/zeromicro/go-zero/core/logx"
+ "io"
+ "mime/multipart"
"net/http"
+ "schisandra-album-cloud-microservices/app/aisvc/rpc/pb"
"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/encrypt"
+ "schisandra-album-cloud-microservices/common/gao_map"
"schisandra-album-cloud-microservices/common/storage/config"
+ "strconv"
+ "strings"
+ "time"
)
type UploadFileLogic struct {
@@ -25,36 +36,199 @@ func NewUploadFileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Upload
}
func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) {
+ // 获取用户 ID
+ uid, err := l.getUserID()
+ if err != nil {
+ return "", err
+ }
+
+ // 解析上传的文件
+ file, header, err := l.getUploadedFile(r)
+ if err != nil {
+ return "", err
+ }
+ defer func(file multipart.File) {
+ _ = file.Close()
+ }(file)
+
+ // 解析 AI 识别结果
+ result, err := l.parseAIRecognitionResult(r)
+ if err != nil {
+ return "", err
+ }
+ var faceId int64
+ bytes, err := io.ReadAll(file)
+ if err != nil {
+ return "", err
+ }
+ // 人脸识别
+ face, err := l.svcCtx.AiSvcRpc.FaceRecognition(l.ctx, &pb.FaceRecognitionRequest{Face: bytes, UserId: uid})
+ if err != nil {
+ return "", err
+ }
+ if face != nil {
+ faceId = face.GetFaceId()
+ }
+
+ // 解析 EXIF 信息
+ exif, err := l.parseExifData(result.Exif)
+ if err != nil {
+ return "", err
+ }
+
+ // 提取拍摄时间
+ originalDateTime, err := l.extractOriginalDateTime(exif)
+ if err != nil {
+ return "", err
+ }
+
+ // 提取 GPS 信息
+ latitude, longitude := l.extractGPSCoordinates(exif)
+
+ // 根据 GPS 信息获取地理位置信息
+ locationString, gpsString, err := l.getGeoLocation(latitude, longitude)
+ if err != nil {
+ return "", err
+ }
+
+ // 上传文件到 OSS
+ if err = l.uploadFileToOSS(uid, header, file); err != nil {
+ return "", err
+ }
+
+ // 将 EXIF 和文件信息存入数据库
+ if err = l.saveFileInfoToDB(uid, header, result, originalDateTime, gpsString, locationString, exif, faceId); err != nil {
+ return "", err
+ }
+
+ return "success", nil
+}
+
+// 获取用户 ID
+func (l *UploadFileLogic) getUserID() (string, error) {
uid, ok := l.ctx.Value("user_id").(string)
if !ok {
return "", errors.New("user_id not found")
}
+ return uid, nil
+}
+
+// 解析上传的文件
+func (l *UploadFileLogic) getUploadedFile(r *http.Request) (multipart.File, *multipart.FileHeader, error) {
file, header, err := r.FormFile("file")
if err != nil {
- return "", errors.New("file not found")
+ return nil, nil, errors.New("file not found")
}
- defer file.Close()
- //formValue := r.PostFormValue("result")
- //
- //var result types.File
- //err = json.Unmarshal([]byte(formValue), &result)
- //if err != nil {
- // return "", errors.New("invalid result")
- //}
- //fmt.Println(result)
+ return file, header, nil
+}
+
+// 解析 AI 识别结果
+func (l *UploadFileLogic) parseAIRecognitionResult(r *http.Request) (types.File, error) {
+ formValue := r.PostFormValue("data")
+ var result types.File
+ if err := json.Unmarshal([]byte(formValue), &result); err != nil {
+ return result, errors.New("invalid result")
+ }
+ return result, nil
+}
+
+// 解析 EXIF 数据
+func (l *UploadFileLogic) parseExifData(exifData interface{}) (map[string]interface{}, error) {
+ marshaledExif, err := json.Marshal(exifData)
+ if err != nil {
+ return nil, errors.New("invalid exif")
+ }
+
+ var exif map[string]interface{}
+ if err = json.Unmarshal(marshaledExif, &exif); err != nil {
+ return nil, errors.New("invalid exif")
+ }
+ return exif, 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) extractGPSCoordinates(exif map[string]interface{}) (float64, float64) {
+ var latitude, longitude float64
+ if lat, ok := exif["latitude"].(float64); ok {
+ latitude = lat
+ }
+ if long, ok := exif["longitude"].(float64); ok {
+ longitude = long
+ }
+ return latitude, longitude
+}
+
+// 根据 GPS 信息获取地理位置信息
+func (l *UploadFileLogic) getGeoLocation(latitude, longitude float64) (string, string, error) {
+ if latitude == 0 || longitude == 0 {
+ return "", "", nil
+ }
+
+ gpsString := fmt.Sprintf("[%f,%f]", latitude, longitude)
+ request := gao_map.ReGeoRequest{Location: fmt.Sprintf("%f,%f", latitude, longitude)}
+
+ location, err := l.svcCtx.GaoMap.Location.ReGeo(&request)
+ if err != nil {
+ return "", "", errors.New("regeo failed")
+ }
+
+ addressInfo := map[string]string{}
+ if location.ReGeoCode.AddressComponent.Country != "" {
+ addressInfo["county"] = 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
+ }
+
+ locationString := ""
+ if len(addressInfo) > 0 {
+ addressJSON, err := json.Marshal(addressInfo)
+ if err != nil {
+ return "", "", errors.New("marshal address info failed")
+ }
+ locationString = string(addressJSON)
+ }
+
+ return locationString, gpsString, nil
+}
+
+// 上传文件到 OSS
+func (l *UploadFileLogic) uploadFileToOSS(uid string, header *multipart.FileHeader, file multipart.File) error {
ossConfig := l.svcCtx.DB.ScaStorageConfig
dbConfig, err := ossConfig.Where(ossConfig.UserID.Eq(uid)).First()
if err != nil {
- return "", errors.New("oss config not found")
+ return errors.New("oss config not found")
}
+
accessKey, err := encrypt.Decrypt(dbConfig.AccessKey, l.svcCtx.Config.Encrypt.Key)
if err != nil {
- return "", errors.New("decrypt access key failed")
+ return errors.New("decrypt access key failed")
}
secretKey, err := encrypt.Decrypt(dbConfig.SecretKey, l.svcCtx.Config.Encrypt.Key)
if err != nil {
- return "", errors.New("decrypt secret key failed")
+ return errors.New("decrypt secret key failed")
}
+
storageConfig := &config.StorageConfig{
Provider: dbConfig.Type,
Endpoint: dbConfig.Endpoint,
@@ -63,13 +237,48 @@ func (l *UploadFileLogic) UploadFile(r *http.Request) (resp string, err error) {
BucketName: dbConfig.Bucket,
Region: dbConfig.Region,
}
+
service, err := l.svcCtx.StorageManager.GetStorage(uid, storageConfig)
if err != nil {
- return "", errors.New("get storage failed")
+ return errors.New("get storage failed")
}
- result, err := service.UploadFileSimple(l.ctx, dbConfig.Bucket, header.Filename, file, map[string]string{})
+
+ _, err = service.UploadFileSimple(l.ctx, dbConfig.Bucket, header.Filename, file, map[string]string{})
if err != nil {
- return "", errors.New("upload file failed")
+ return errors.New("upload file failed")
}
- return *result.ContentMD5, nil
+ return nil
+}
+
+// 将 EXIF 和文件信息存入数据库
+func (l *UploadFileLogic) saveFileInfoToDB(uid string, header *multipart.FileHeader, result types.File, originalDateTime, gpsString, locationString string, exif map[string]interface{}, faceId int64) error {
+ exifJSON, err := json.Marshal(exif)
+ if err != nil {
+ return errors.New("marshal exif failed")
+ }
+
+ scaStorageInfo := &model.ScaStorageInfo{
+ UserID: uid,
+ Provider: result.FileType,
+ Bucket: result.TopCategory,
+ FileName: header.Filename,
+ FileSize: strconv.FormatInt(header.Size, 10),
+ FileType: result.FileType,
+ Landscape: result.Landscape,
+ Objects: strings.Join(result.ObjectArray, ", "),
+ Anime: strconv.FormatBool(result.IsAnime),
+ Category: result.TopCategory,
+ Screenshot: strconv.FormatBool(result.IsScreenshot),
+ OriginalTime: originalDateTime,
+ Gps: gpsString,
+ Location: locationString,
+ Exif: string(exifJSON),
+ FaceID: faceId,
+ }
+
+ err = l.svcCtx.DB.ScaStorageInfo.Create(scaStorageInfo)
+ if err != nil {
+ return errors.New("create storage info failed")
+ }
+ return nil
}
diff --git a/app/auth/api/internal/logic/token/refresh_token_logic.go b/app/auth/api/internal/logic/token/refresh_token_logic.go
index bd0ba68..3250f9b 100644
--- a/app/auth/api/internal/logic/token/refresh_token_logic.go
+++ b/app/auth/api/internal/logic/token/refresh_token_logic.go
@@ -73,7 +73,7 @@ func (l *RefreshTokenLogic) RefreshToken(r *http.Request) (resp *types.RefreshTo
GeneratedIP: redisTokenData.GeneratedIP,
UpdatedAt: time.Now().Format(constant.TimeFormat),
}
- err = l.svcCtx.RedisClient.Set(l.ctx, constant.UserTokenPrefix+refreshToken.UserID, redisToken, time.Hour*24*7).Err()
+ err = l.svcCtx.RedisClient.Set(l.ctx, constant.UserTokenPrefix+refreshToken.UserID, redisToken, time.Hour*24*3).Err()
if err != nil {
return nil, err
}
diff --git a/app/auth/api/internal/svc/service_context.go b/app/auth/api/internal/svc/service_context.go
index 1e04e7c..4eeb39d 100644
--- a/app/auth/api/internal/svc/service_context.go
+++ b/app/auth/api/internal/svc/service_context.go
@@ -8,8 +8,10 @@ import (
"github.com/wenlng/go-captcha/v2/rotate"
"github.com/wenlng/go-captcha/v2/slide"
"github.com/zeromicro/go-zero/rest"
+ "github.com/zeromicro/go-zero/zrpc"
sensitive "github.com/zmexing/go-sensitive-word"
"go.mongodb.org/mongo-driver/v2/mongo"
+ "schisandra-album-cloud-microservices/app/aisvc/rpc/client/aiservice"
"schisandra-album-cloud-microservices/app/auth/api/internal/config"
"schisandra-album-cloud-microservices/app/auth/api/internal/middleware"
"schisandra-album-cloud-microservices/app/auth/model/mongodb"
@@ -17,16 +19,18 @@ import (
"schisandra-album-cloud-microservices/app/auth/model/mysql/query"
"schisandra-album-cloud-microservices/common/captcha/initialize"
"schisandra-album-cloud-microservices/common/casbinx"
+ "schisandra-album-cloud-microservices/common/gao_map"
"schisandra-album-cloud-microservices/common/ip2region"
"schisandra-album-cloud-microservices/common/redisx"
"schisandra-album-cloud-microservices/common/sensitivex"
"schisandra-album-cloud-microservices/common/storage"
- storage2 "schisandra-album-cloud-microservices/common/storage/manager"
+ "schisandra-album-cloud-microservices/common/storage/manager"
"schisandra-album-cloud-microservices/common/wechat_official"
)
type ServiceContext struct {
Config config.Config
+ AiSvcRpc aiservice.AiService
SecurityHeadersMiddleware rest.Middleware
CasbinVerifyMiddleware rest.Middleware
AuthorizationMiddleware rest.Middleware
@@ -40,7 +44,8 @@ type ServiceContext struct {
RotateCaptcha rotate.Captcha
SlideCaptcha slide.Captcha
Sensitive *sensitive.Manager
- StorageManager *storage2.Manager
+ StorageManager *manager.Manager
+ GaoMap *gao_map.AmapClient
}
func NewServiceContext(c config.Config) *ServiceContext {
@@ -63,5 +68,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
MongoClient: mongodb.NewMongoDB(c.Mongo.Uri, c.Mongo.Username, c.Mongo.Password, c.Mongo.AuthSource, c.Mongo.Database),
Sensitive: sensitivex.NewSensitive(),
StorageManager: storage.InitStorageManager(),
+ GaoMap: gao_map.NewAmapClient(c.Map.Key, ""),
+ AiSvcRpc: aiservice.NewAiService(zrpc.MustNewClient(c.AiSvcRpc)),
}
}
diff --git a/app/auth/api/internal/types/file.go b/app/auth/api/internal/types/file.go
index 4559ca1..7079cf7 100644
--- a/app/auth/api/internal/types/file.go
+++ b/app/auth/api/internal/types/file.go
@@ -2,13 +2,11 @@ package types
// File represents a file uploaded by the user.
type File struct {
- UID string `json:"uid"`
- FileName string `json:"fileName"`
- FileType string `json:"fileType"`
- DetectionResult struct {
- IsAnime bool `json:"isAnime"`
- HasFace bool `json:"hasFace"`
- ObjectArray []string `json:"objectArray"`
- Landscape string `json:"landscape"`
- }
+ FileType string `json:"fileType"`
+ IsAnime bool `json:"isAnime"`
+ ObjectArray []string `json:"objectArray"`
+ Landscape string `json:"landscape"`
+ TopCategory string `json:"topCategory"`
+ IsScreenshot bool `json:"isScreenshot"`
+ Exif any `json:"exif"`
}
diff --git a/app/auth/model/mysql/generate/generate.go b/app/auth/model/mysql/generate/generate.go
index dd758f7..dc32eba 100644
--- a/app/auth/model/mysql/generate/generate.go
+++ b/app/auth/model/mysql/generate/generate.go
@@ -99,9 +99,41 @@ func main() {
fieldOpts := []gen.ModelOpt{jsonField, idField, autoUpdateTimeField, autoCreateTimeField, softDeleteField, versionField}
// 创建全部模型文件, 并覆盖前面创建的同名模型
- allModel := g.GenerateAllTable(fieldOpts...)
+ scaAuthMenu := g.GenerateModel("sca_auth_menu", fieldOpts...)
+ scaAuthPermissionRule := g.GenerateModel("sca_auth_permission_rule", fieldOpts...)
+ scaAuthRole := g.GenerateModel("sca_auth_role", fieldOpts...)
+ scaAuthUser := g.GenerateModel("sca_auth_user", fieldOpts...)
+ scaAuthUserDevice := g.GenerateModel("sca_auth_user_device", fieldOpts...)
+ scaAuthUserSocial := g.GenerateModel("sca_auth_user_social", fieldOpts...)
+ scaCommentReply := g.GenerateModel("sca_comment_reply", fieldOpts...)
+ scaCommentLikes := g.GenerateModel("sca_comment_likes", fieldOpts...)
+ scaMessageReport := g.GenerateModel("sca_message_report", fieldOpts...)
+ scaStorageConfig := g.GenerateModel("sca_storage_config", fieldOpts...)
+ scaStorageInfo := g.GenerateModel("sca_storage_info", fieldOpts...)
+ scaStorageTag := g.GenerateModel("sca_storage_tag", fieldOpts...)
+ scaStorageTagInfo := g.GenerateModel("sca_storage_tag_info", fieldOpts...)
+ scaUserFollows := g.GenerateModel("sca_user_follows", fieldOpts...)
+ scaUserLevel := g.GenerateModel("sca_user_level", fieldOpts...)
+ scaUserMessage := g.GenerateModel("sca_user_message", fieldOpts...)
- g.ApplyBasic(allModel...)
+ g.ApplyBasic(
+ scaAuthMenu,
+ scaAuthPermissionRule,
+ scaAuthRole,
+ scaAuthUser,
+ scaAuthUserDevice,
+ scaAuthUserSocial,
+ scaCommentReply,
+ scaCommentLikes,
+ scaMessageReport,
+ scaStorageConfig,
+ scaStorageInfo,
+ scaStorageTag,
+ scaStorageTagInfo,
+ scaUserFollows,
+ scaUserLevel,
+ scaUserMessage,
+ )
g.Execute()
}
diff --git a/app/auth/model/mysql/model/sca_storage_info.gen.go b/app/auth/model/mysql/model/sca_storage_info.gen.go
index 1b37a2a..d134ec4 100644
--- a/app/auth/model/mysql/model/sca_storage_info.gen.go
+++ b/app/auth/model/mysql/model/sca_storage_info.gen.go
@@ -14,24 +14,29 @@ const TableNameScaStorageInfo = "sca_storage_info"
// ScaStorageInfo mapped from table
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
- Storage string `gorm:"column:storage;type:varchar(50);comment:存储空间" json:"storage"` // 存储空间
- Bucket string `gorm:"column:bucket;type:varchar(50);comment:存储桶" json:"bucket"` // 存储桶
- Type string `gorm:"column:type;type:varchar(50);comment:类型" json:"type"` // 类型
- Path string `gorm:"column:path;type:varchar(255);comment:路径" json:"path"` // 路径
- FileName string `gorm:"column:file_name;type:varchar(100);comment:名称" json:"file_name"` // 名称
- Category string `gorm:"column:category;type:varchar(50);comment:分类" json:"category"` // 分类
- Loaction string `gorm:"column:loaction;type:varchar(100);comment:地址" json:"loaction"` // 地址
- Hash string `gorm:"column:hash;type:varchar(255);comment:哈希值" json:"hash"` // 哈希值
- Anime string `gorm:"column:anime;type:varchar(50);comment:是否是动漫图片" json:"anime"` // 是否是动漫图片
- HasFace string `gorm:"column:has_face;type:varchar(50);comment:是否人像" json:"has_face"` // 是否人像
- FaceID int64 `gorm:"column:face_id;type:bigint(20);comment:人像ID" json:"face_id"` // 人像ID
- Landscape string `gorm:"column:landscape;type:varchar(50);comment:风景类型" json:"landscape"` // 风景类型
- Objects string `gorm:"column:objects;type:varchar(50);comment:对象识别" json:"objects"` // 对象识别
- 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"` // 删除时间
+ 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
+ Provider string `gorm:"column:provider;type:varchar(50);comment:供应商" json:"provider"` // 供应商
+ Bucket string `gorm:"column:bucket;type:varchar(50);comment:存储桶" json:"bucket"` // 存储桶
+ Path string `gorm:"column:path;type:varchar(255);comment:路径" json:"path"` // 路径
+ FileName string `gorm:"column:file_name;type:varchar(100);comment:文件名称" json:"file_name"` // 文件名称
+ FileSize string `gorm:"column:file_size;type:varchar(50);comment:文件大小" json:"file_size"` // 文件大小
+ FileType string `gorm:"column:file_type;type:varchar(50);comment:文件类型" json:"file_type"` // 文件类型
+ Category string `gorm:"column:category;type:varchar(50);comment:分类" json:"category"` // 分类
+ Tags string `gorm:"column:tags;type:varchar(255);comment:标签" json:"tags"` // 标签
+ Location string `gorm:"column:location;type:varchar(100);comment:地址" json:"location"` // 地址
+ Hash string `gorm:"column:hash;type:varchar(255);comment:哈希值" json:"hash"` // 哈希值
+ Anime string `gorm:"column:anime;type:varchar(50);comment:是否是动漫图片" json:"anime"` // 是否是动漫图片
+ FaceID int64 `gorm:"column:face_id;type:bigint(20);comment:人像ID" json:"face_id"` // 人像ID
+ Landscape string `gorm:"column:landscape;type:varchar(50);comment:风景类型" json:"landscape"` // 风景类型
+ Objects string `gorm:"column:objects;type:varchar(50);comment:对象识别" json:"objects"` // 对象识别
+ OriginalTime string `gorm:"column:original_time;type:varchar(50);comment:拍摄时间" json:"original_time"` // 拍摄时间
+ Gps string `gorm:"column:gps;type:varchar(255);comment:GPS" json:"gps"` // GPS
+ Screenshot string `gorm:"column:screenshot;type:varchar(50);comment:是否是截图" json:"screenshot"` // 是否是截图
+ Exif string `gorm:"column:exif;type:json;comment:exif 信息" json:"exif"` // exif 信息
+ 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 ScaStorageInfo's table name
diff --git a/app/auth/model/mysql/query/sca_storage_info.gen.go b/app/auth/model/mysql/query/sca_storage_info.gen.go
index 89f1a91..ced7fe2 100644
--- a/app/auth/model/mysql/query/sca_storage_info.gen.go
+++ b/app/auth/model/mysql/query/sca_storage_info.gen.go
@@ -29,19 +29,24 @@ func newScaStorageInfo(db *gorm.DB, opts ...gen.DOOption) scaStorageInfo {
_scaStorageInfo.ALL = field.NewAsterisk(tableName)
_scaStorageInfo.ID = field.NewInt64(tableName, "id")
_scaStorageInfo.UserID = field.NewString(tableName, "user_id")
- _scaStorageInfo.Storage = field.NewString(tableName, "storage")
+ _scaStorageInfo.Provider = field.NewString(tableName, "provider")
_scaStorageInfo.Bucket = field.NewString(tableName, "bucket")
- _scaStorageInfo.Type = field.NewString(tableName, "type")
_scaStorageInfo.Path = field.NewString(tableName, "path")
_scaStorageInfo.FileName = field.NewString(tableName, "file_name")
+ _scaStorageInfo.FileSize = field.NewString(tableName, "file_size")
+ _scaStorageInfo.FileType = field.NewString(tableName, "file_type")
_scaStorageInfo.Category = field.NewString(tableName, "category")
- _scaStorageInfo.Loaction = field.NewString(tableName, "loaction")
+ _scaStorageInfo.Tags = field.NewString(tableName, "tags")
+ _scaStorageInfo.Location = field.NewString(tableName, "location")
_scaStorageInfo.Hash = field.NewString(tableName, "hash")
_scaStorageInfo.Anime = field.NewString(tableName, "anime")
- _scaStorageInfo.HasFace = field.NewString(tableName, "has_face")
_scaStorageInfo.FaceID = field.NewInt64(tableName, "face_id")
_scaStorageInfo.Landscape = field.NewString(tableName, "landscape")
_scaStorageInfo.Objects = field.NewString(tableName, "objects")
+ _scaStorageInfo.OriginalTime = field.NewString(tableName, "original_time")
+ _scaStorageInfo.Gps = field.NewString(tableName, "gps")
+ _scaStorageInfo.Screenshot = field.NewString(tableName, "screenshot")
+ _scaStorageInfo.Exif = field.NewString(tableName, "exif")
_scaStorageInfo.CreatedAt = field.NewTime(tableName, "created_at")
_scaStorageInfo.UpdatedAt = field.NewTime(tableName, "updated_at")
_scaStorageInfo.DeletedAt = field.NewField(tableName, "deleted_at")
@@ -54,25 +59,30 @@ func newScaStorageInfo(db *gorm.DB, opts ...gen.DOOption) scaStorageInfo {
type scaStorageInfo struct {
scaStorageInfoDo
- ALL field.Asterisk
- ID field.Int64 // 主键
- UserID field.String // 用户ID
- Storage field.String // 存储空间
- Bucket field.String // 存储桶
- Type field.String // 类型
- Path field.String // 路径
- FileName field.String // 名称
- Category field.String // 分类
- Loaction field.String // 地址
- Hash field.String // 哈希值
- Anime field.String // 是否是动漫图片
- HasFace field.String // 是否人像
- FaceID field.Int64 // 人像ID
- Landscape field.String // 风景类型
- Objects field.String // 对象识别
- CreatedAt field.Time // 创建时间
- UpdatedAt field.Time // 更新时间
- DeletedAt field.Field // 删除时间
+ ALL field.Asterisk
+ ID field.Int64 // 主键
+ UserID field.String // 用户ID
+ Provider field.String // 供应商
+ Bucket field.String // 存储桶
+ Path field.String // 路径
+ FileName field.String // 文件名称
+ FileSize field.String // 文件大小
+ FileType field.String // 文件类型
+ Category field.String // 分类
+ Tags field.String // 标签
+ Location field.String // 地址
+ Hash field.String // 哈希值
+ Anime field.String // 是否是动漫图片
+ FaceID field.Int64 // 人像ID
+ Landscape field.String // 风景类型
+ Objects field.String // 对象识别
+ OriginalTime field.String // 拍摄时间
+ Gps field.String // GPS
+ Screenshot field.String // 是否是截图
+ Exif field.String // exif 信息
+ CreatedAt field.Time // 创建时间
+ UpdatedAt field.Time // 更新时间
+ DeletedAt field.Field // 删除时间
fieldMap map[string]field.Expr
}
@@ -91,19 +101,24 @@ func (s *scaStorageInfo) updateTableName(table string) *scaStorageInfo {
s.ALL = field.NewAsterisk(table)
s.ID = field.NewInt64(table, "id")
s.UserID = field.NewString(table, "user_id")
- s.Storage = field.NewString(table, "storage")
+ s.Provider = field.NewString(table, "provider")
s.Bucket = field.NewString(table, "bucket")
- s.Type = field.NewString(table, "type")
s.Path = field.NewString(table, "path")
s.FileName = field.NewString(table, "file_name")
+ s.FileSize = field.NewString(table, "file_size")
+ s.FileType = field.NewString(table, "file_type")
s.Category = field.NewString(table, "category")
- s.Loaction = field.NewString(table, "loaction")
+ s.Tags = field.NewString(table, "tags")
+ s.Location = field.NewString(table, "location")
s.Hash = field.NewString(table, "hash")
s.Anime = field.NewString(table, "anime")
- s.HasFace = field.NewString(table, "has_face")
s.FaceID = field.NewInt64(table, "face_id")
s.Landscape = field.NewString(table, "landscape")
s.Objects = field.NewString(table, "objects")
+ s.OriginalTime = field.NewString(table, "original_time")
+ s.Gps = field.NewString(table, "gps")
+ s.Screenshot = field.NewString(table, "screenshot")
+ s.Exif = field.NewString(table, "exif")
s.CreatedAt = field.NewTime(table, "created_at")
s.UpdatedAt = field.NewTime(table, "updated_at")
s.DeletedAt = field.NewField(table, "deleted_at")
@@ -123,22 +138,27 @@ func (s *scaStorageInfo) GetFieldByName(fieldName string) (field.OrderExpr, bool
}
func (s *scaStorageInfo) fillFieldMap() {
- s.fieldMap = make(map[string]field.Expr, 18)
+ s.fieldMap = make(map[string]field.Expr, 23)
s.fieldMap["id"] = s.ID
s.fieldMap["user_id"] = s.UserID
- s.fieldMap["storage"] = s.Storage
+ s.fieldMap["provider"] = s.Provider
s.fieldMap["bucket"] = s.Bucket
- s.fieldMap["type"] = s.Type
s.fieldMap["path"] = s.Path
s.fieldMap["file_name"] = s.FileName
+ s.fieldMap["file_size"] = s.FileSize
+ s.fieldMap["file_type"] = s.FileType
s.fieldMap["category"] = s.Category
- s.fieldMap["loaction"] = s.Loaction
+ s.fieldMap["tags"] = s.Tags
+ s.fieldMap["location"] = s.Location
s.fieldMap["hash"] = s.Hash
s.fieldMap["anime"] = s.Anime
- s.fieldMap["has_face"] = s.HasFace
s.fieldMap["face_id"] = s.FaceID
s.fieldMap["landscape"] = s.Landscape
s.fieldMap["objects"] = s.Objects
+ s.fieldMap["original_time"] = s.OriginalTime
+ s.fieldMap["gps"] = s.Gps
+ s.fieldMap["screenshot"] = s.Screenshot
+ s.fieldMap["exif"] = s.Exif
s.fieldMap["created_at"] = s.CreatedAt
s.fieldMap["updated_at"] = s.UpdatedAt
s.fieldMap["deleted_at"] = s.DeletedAt
diff --git a/common/constant/face_type.go b/common/constant/face_type.go
new file mode 100644
index 0000000..90d4610
--- /dev/null
+++ b/common/constant/face_type.go
@@ -0,0 +1,5 @@
+package constant
+
+const (
+ FaceTypeSample = "sample"
+)
diff --git a/common/constant/redis_prefix.go b/common/constant/redis_prefix.go
index 58e3eff..4af6da6 100644
--- a/common/constant/redis_prefix.go
+++ b/common/constant/redis_prefix.go
@@ -13,3 +13,7 @@ const (
const (
SystemApiNoncePrefix = "system:nonce:"
)
+
+const (
+ FaceSamplePrefix = "face:sample:"
+)
diff --git a/common/face_recognizer/face_recognizer.go b/common/face_recognizer/face_recognizer.go
new file mode 100644
index 0000000..c361192
--- /dev/null
+++ b/common/face_recognizer/face_recognizer.go
@@ -0,0 +1,24 @@
+package face_recognizer
+
+import (
+ "github.com/Kagami/go-face"
+ "os"
+ "path/filepath"
+)
+
+// NewFaceRecognition creates a new instance of FaceRecognition
+func NewFaceRecognition() *face.Recognizer {
+ dir, err := os.Getwd()
+ if err != nil {
+ panic(err)
+ return nil
+ }
+ modelDir := filepath.Join(dir, "/resources/models/face")
+ rec, err := face.NewRecognizer(modelDir)
+ if err != nil {
+ panic(err)
+ return nil
+ }
+ //defer rec.Close()
+ return rec
+}
diff --git a/common/gao_map/amap.go b/common/gao_map/amap.go
new file mode 100644
index 0000000..c6a41e5
--- /dev/null
+++ b/common/gao_map/amap.go
@@ -0,0 +1,84 @@
+package gao_map
+
+import (
+ "errors"
+ "github.com/duke-git/lancet/v2/cryptor"
+ "github.com/duke-git/lancet/v2/maputil"
+ "github.com/duke-git/lancet/v2/netutil"
+ "github.com/duke-git/lancet/v2/slice"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+type AmapClient struct {
+ key string
+ secret string
+ Location *Location
+ Direction *Direction
+ Place *Place
+ Weather *Weather
+}
+
+func NewAmapClient(key, secret string) *AmapClient {
+ amap := &AmapClient{key: key, secret: secret}
+ amap.Location = &Location{client: amap}
+ amap.Direction = &Direction{client: amap}
+ amap.Place = &Place{client: amap}
+ amap.Weather = &Weather{client: amap}
+
+ return amap
+}
+
+func (a *AmapClient) DoRequest(url, method string, params url.Values) (resp *http.Response, err error) {
+ params.Set("key", a.key)
+ if a.secret != "" {
+ params.Set("sig", makeSign(a, ¶ms))
+ }
+
+ switch strings.ToUpper(method) {
+ case "GET":
+ resp, err = netutil.HttpGet(url, map[string]string{}, params)
+ case "POST":
+ resp, err = netutil.HttpPost(url, map[string]string{}, params)
+ default:
+ err = errors.New("unknown request method")
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode != 200 {
+ return nil, errors.New("amap request error: " + resp.Status)
+ }
+
+ return
+}
+
+type Response struct {
+ Status string `json:"status"`
+ Info string `json:"info"`
+ Infocode string `json:"Infocode"`
+}
+
+func makeSign(client *AmapClient, values *url.Values) string {
+ keys := maputil.Keys(*values)
+ slice.SortBy(keys, func(a, b string) bool {
+ return a < b
+ })
+
+ data := slice.Map(keys, func(index int, item string) string {
+ return item + "=" + values.Get(item)
+ })
+ str := slice.Join(data, "&") + client.secret
+ return cryptor.Md5String(str)
+}
+
+func checkResponse(response Response) error {
+ if response.Status != "1" || response.Infocode != "10000" {
+ return errors.New("amap response error: " + response.Info)
+ }
+
+ return nil
+}
diff --git a/common/gao_map/beans.go b/common/gao_map/beans.go
new file mode 100644
index 0000000..d00707f
--- /dev/null
+++ b/common/gao_map/beans.go
@@ -0,0 +1,105 @@
+package gao_map
+
+type WeatherResponse struct {
+ Response
+
+ Lives []struct {
+ Adcode string `json:"adcode"`
+ City string `json:"city"`
+ Province string `json:"province"`
+ Weather string `json:"weather"`
+ Temperature string `json:"temperature"`
+ WindDirection string `json:"winddirection"`
+ WindPower string `json:"windpower"`
+ Humidity string `json:"humidity"`
+ ReportTime string `json:"reporttime"`
+ TemperatureFloat string `json:"temperature_float"`
+ HumidityFloat string `json:"humidity_float"`
+ } `json:"lives"`
+ Forecast []struct {
+ City string `json:"city"`
+ Adcode string `json:"adcode"`
+ Province string `json:"province"`
+ ReportTime string `json:"reporttime"`
+ Casts []struct {
+ Date string `json:"date"`
+ Week string `json:"week"`
+ DayWeather string `json:"dayweather"`
+ NightWeather string `json:"nightweather"`
+ DayTemp string `json:"daytemp"`
+ NightTemp string `json:"nighttemp"`
+ DayWind string `json:"daywind"`
+ NightWind string `json:"nightwind"`
+ DayPower string `json:"daypower"`
+ NightPower string `json:"nightpower"`
+ } `json:"casts"`
+ } `json:"forecast"`
+}
+
+type IpResponse struct {
+ Response
+ Province string `json:"province"`
+ City any `json:"city"`
+ Adcode string `json:"adcode"`
+ Rectangle string `json:"rectangle"`
+}
+
+type District struct {
+ Citycode any `json:"citycode"`
+ Adcode string `json:"adcode"`
+ Name string `json:"name"`
+ Center string `json:"center"`
+ Level string `json:"level"`
+ Districts []*District `json:"districts"`
+}
+
+type AllDistrictResponse struct {
+ Response
+
+ Count int `json:"count,string"`
+ Districts []*District
+}
+
+type Geo struct {
+ FormattedAddress string `json:"formatted_address"`
+ Country string `json:"country"`
+ Province string `json:"province"`
+ Citycode string `json:"citycode"`
+ City any `json:"city"`
+ Adcode string `json:"adcode"`
+ Location string `json:"location"`
+ Level string `json:"level"`
+}
+
+type GeoResponse struct {
+ Response
+
+ Count string `json:"count"`
+ GeoCodes []Geo `json:"geocodes"`
+}
+
+type ReGeoResponse struct {
+ Response
+
+ ReGeoCode struct {
+ FormattedAddress string `json:"formatted_address"`
+ AddressComponent struct {
+ City any `json:"city"`
+ Country string `json:"country"`
+ Province string `json:"province"`
+ Citycode string `json:"citycode"`
+ District any `json:"district"`
+ Adcode string `json:"adcode"`
+ Township string `json:"township"`
+ Towncode string `json:"towncode"`
+ } `json:"addressComponent"`
+ } `json:"regeocode"`
+}
+
+type ReGeoRequest struct {
+ Location string
+ //PoiType string
+ Radius int
+ //Extensions string
+ //RoadLevel int
+}
diff --git a/common/gao_map/direction.go b/common/gao_map/direction.go
new file mode 100644
index 0000000..fe1c4b1
--- /dev/null
+++ b/common/gao_map/direction.go
@@ -0,0 +1,117 @@
+package gao_map
+
+import (
+ "github.com/duke-git/lancet/v2/convertor"
+ "github.com/duke-git/lancet/v2/netutil"
+ "github.com/duke-git/lancet/v2/strutil"
+ "net/url"
+ "strings"
+)
+
+const drivingUrl = "https://restapi.amap.com/v5/direction/driving" //驾车
+
+type Direction struct {
+ client *AmapClient
+}
+
+type DrivingRequest struct {
+ Origin string
+ Destination string
+ Strategy int
+ Waypoints []string
+ ShowFields string
+}
+
+type BicyclingRequest struct {
+ Origin string
+ Destination string
+ ShowFields string
+}
+
+type Route struct {
+ Origin string `json:"origin"`
+ Destination string `json:"destination"`
+ Paths []struct {
+ Distance float64 `json:"distance,string"`
+
+ Steps []struct {
+ Cost struct {
+ Duration float64 `json:"duration,string"`
+ Tolls float64 `json:"tolls,string"`
+ TollsDistance float64 `json:"tolls_distance,string"`
+ } `json:"cost"`
+ Cities []struct {
+ Adcode string `json:"adcode"`
+ Citycode string `json:"citycode"`
+ City string `json:"city"`
+ } `json:"cities"`
+ Polyline string `json:"polyline"`
+ } `json:"steps"`
+ } `json:"paths"`
+}
+
+type DrivingResponse struct {
+ Response
+
+ Count int `json:"count,string"`
+ Route Route `json:"route"`
+}
+
+type BicyclingResponse struct {
+ Response
+
+ Count int `json:"count,string"`
+ Route Route `json:"route"`
+}
+
+func (d *Direction) Bicycling(request *BicyclingRequest) (*BicyclingResponse, error) {
+ val := url.Values{}
+ val.Set("origin", request.Origin)
+ val.Set("destination", request.Destination)
+ if !strutil.IsBlank(request.ShowFields) {
+ val.Set("show_fields", request.ShowFields)
+ }
+
+ resp, err := d.client.DoRequest(drivingUrl, "POST", val)
+ if err != nil {
+ return nil, err
+ }
+
+ var data BicyclingResponse
+
+ if err = netutil.ParseHttpResponse(resp, &data); err != nil {
+ return nil, err
+ } else if err = checkResponse(data.Response); err != nil {
+ return nil, err
+ }
+
+ return &data, err
+}
+
+func (d *Direction) Driving(request *DrivingRequest) (*DrivingResponse, error) {
+ val := url.Values{}
+ val.Set("origin", request.Origin)
+ val.Set("destination", request.Destination)
+ val.Set("strategy", convertor.ToString(request.Strategy))
+ if len(request.Waypoints) > 0 {
+ val.Set("waypoints", strings.Join(request.Waypoints, ";"))
+ }
+ if !strutil.IsBlank(request.ShowFields) {
+ val.Set("show_fields", request.ShowFields)
+ }
+
+ resp, err := d.client.DoRequest(drivingUrl, "POST", val)
+ if err != nil {
+ return nil, err
+ }
+
+ var data DrivingResponse
+
+ if err = netutil.ParseHttpResponse(resp, &data); err != nil {
+ return nil, err
+ } else if err = checkResponse(data.Response); err != nil {
+ return nil, err
+ }
+
+ return &data, err
+}
diff --git a/common/gao_map/location.go b/common/gao_map/location.go
new file mode 100644
index 0000000..c02c368
--- /dev/null
+++ b/common/gao_map/location.go
@@ -0,0 +1,109 @@
+package gao_map
+
+import (
+ "errors"
+ "github.com/duke-git/lancet/v2/condition"
+ "github.com/duke-git/lancet/v2/convertor"
+ "github.com/duke-git/lancet/v2/netutil"
+ "github.com/duke-git/lancet/v2/validator"
+ "net/url"
+)
+
+const (
+ ipUrl = "https://restapi.amap.com/v3/ip"
+ geoUrl = "https://restapi.amap.com/v3/geocode/geo"
+ regeoUrl = "https://restapi.amap.com/v3/geocode/regeo"
+ zoneUrl = "https://restapi.amap.com/v3/config/district"
+)
+
+type Location struct {
+ client *AmapClient
+}
+
+func (l *Location) ChinaDistricts() ([]*District, error) {
+ val := url.Values{}
+ val.Set("keywords", "中国")
+ val.Set("subdistrict", "3")
+
+ resp, err := l.client.DoRequest(zoneUrl, "GET", val)
+ if err != nil {
+ return nil, err
+ }
+
+ var data AllDistrictResponse
+
+ if err = netutil.ParseHttpResponse(resp, &data); err != nil {
+ return nil, err
+ } else if err = checkResponse(data.Response); err != nil {
+ return nil, err
+ } else if data.Count <= 0 {
+ return nil, errors.New("no record")
+ }
+
+ return data.Districts[0].Districts, err
+}
+
+func (l *Location) IpLocation(ip string) (*IpResponse, error) {
+ val := url.Values{}
+ val.Set("ip", ip)
+
+ resp, err := l.client.DoRequest(ipUrl, "GET", val)
+ if err != nil {
+ return nil, err
+ }
+
+ var data IpResponse
+
+ if err = netutil.ParseHttpResponse(resp, &data); err != nil {
+ return nil, err
+ } else if err = checkResponse(data.Response); err != nil {
+ return nil, err
+ }
+
+ return &data, nil
+}
+
+func (l *Location) ReGeo(request *ReGeoRequest) (*ReGeoResponse, error) {
+ if validator.IsEmptyString(request.Location) {
+ return nil, errors.New("location can't empty")
+ }
+
+ val := url.Values{}
+ val.Set("location", request.Location)
+ //if request.PoiType != "" {
+ // val.Set("poitype", request.PoiType)
+ //}
+ //val.Set("extensions", condition.Ternary(request.Extensions == "", "base", request.Extensions))
+ val.Set("radius", convertor.ToString(condition.Ternary(request.Radius == 0, 1000, request.Radius)))
+ //val.Set("roadlevel", convertor.ToString(request.RoadLevel))
+
+ resp, err := l.client.DoRequest(regeoUrl, "GET", val)
+ var data ReGeoResponse
+
+ if err = netutil.ParseHttpResponse(resp, &data); err != nil {
+ return nil, err
+ } else if err = checkResponse(data.Response); err != nil {
+ return nil, err
+ }
+
+ return &data, nil
+}
+
+func (l *Location) Geo(address, city string) (*GeoResponse, error) {
+ val := url.Values{}
+ val.Set("address", address)
+ if city != "" {
+ val.Set("city", city)
+ }
+
+ resp, err := l.client.DoRequest(geoUrl, "GET", val)
+ var data GeoResponse
+
+ if err = netutil.ParseHttpResponse(resp, &data); err != nil {
+ return nil, err
+ } else if err = checkResponse(data.Response); err != nil {
+ return nil, err
+ }
+
+ return &data, nil
+}
diff --git a/common/gao_map/location_test.go b/common/gao_map/location_test.go
new file mode 100644
index 0000000..6a059f9
--- /dev/null
+++ b/common/gao_map/location_test.go
@@ -0,0 +1,50 @@
+package gao_map
+
+import (
+ "fmt"
+ "testing"
+)
+
+const key = ""
+const secret = ""
+
+func TestDistricts(t *testing.T) {
+ client := NewAmapClient(key, secret)
+ resp, err := client.Location.ChinaDistricts()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fmt.Println(resp)
+}
+
+func TestIp(t *testing.T) {
+ client := NewAmapClient(key, secret)
+ resp, err := client.Location.IpLocation("121.224.33.99")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ fmt.Println(*resp)
+}
+
+func TestGeo(t *testing.T) {
+ client := NewAmapClient(key, secret)
+ resp, err := client.Location.Geo("乐东公园一品墅", "")
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ fmt.Println(*resp)
+}
+
+func TestReGeo(t *testing.T) {
+ client := NewAmapClient(key, secret)
+ request := ReGeoRequest{Location: "118.567824,29.306175"}
+ resp, err := client.Location.ReGeo(&request)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ fmt.Println(*resp)
+}
diff --git a/common/gao_map/place.go b/common/gao_map/place.go
new file mode 100644
index 0000000..e80bf06
--- /dev/null
+++ b/common/gao_map/place.go
@@ -0,0 +1,82 @@
+package gao_map
+
+import (
+ "github.com/duke-git/lancet/v2/condition"
+ "github.com/duke-git/lancet/v2/convertor"
+ "github.com/duke-git/lancet/v2/netutil"
+ "github.com/duke-git/lancet/v2/strutil"
+ "net/url"
+)
+
+const placeUrl = "https://restapi.amap.com/v5/place/text" //驾车
+
+type Place struct {
+ client *AmapClient
+}
+
+type PlaceRequest struct {
+ Keywords string
+ Types string
+ Region string
+ CityLimit bool
+ PageSize int
+ PageNum int
+ ShowFields string
+}
+
+type PlaceResponse struct {
+ Response
+
+ Count int `json:"count,string"`
+ Pois []struct {
+ Address string `json:"address"`
+ Adcode string `json:"adcode"`
+ Type string `json:"type"`
+ Typecode string `json:"typecode"`
+ CityCode string `json:"citycode"`
+ Name string `json:"name"`
+ Location string `json:"location"`
+ Id string `json:"id"`
+ Business struct {
+ Tel string `json:"tel"`
+ Cost string `json:"cost"`
+ Tag string `json:"tag"`
+ OpentimeToday string `json:"opentime_today"`
+ OpentimeWeek string `json:"opentime_week"`
+ } `json:"business"`
+ Photos []struct {
+ Title string `json:"title"`
+ Url string `json:"url"`
+ } `json:"photos"`
+ } `json:"pois"`
+}
+
+func (d *Place) Search(request *PlaceRequest) (*PlaceResponse, error) {
+ val := url.Values{}
+ val.Set("keywords", request.Keywords)
+ val.Set("types", request.Types)
+ if !strutil.IsBlank(request.Region) {
+ val.Set("region", request.Region)
+ }
+ val.Set("city_limit", condition.TernaryOperator(request.CityLimit, "1", "0"))
+ val.Set("page_size", convertor.ToString(condition.TernaryOperator(request.PageSize > 0, request.PageSize, 25)))
+ val.Set("page_num", convertor.ToString(condition.TernaryOperator(request.PageNum > 0, request.PageNum, 1)))
+ if !strutil.IsBlank(request.ShowFields) {
+ val.Set("show_fields", request.ShowFields)
+ }
+
+ resp, err := d.client.DoRequest(placeUrl, "GET", val)
+ if err != nil {
+ return nil, err
+ }
+
+ var data PlaceResponse
+
+ if err = netutil.ParseHttpResponse(resp, &data); err != nil {
+ return nil, err
+ } else if err = checkResponse(data.Response); err != nil {
+ return nil, err
+ }
+
+ return &data, err
+}
diff --git a/common/gao_map/weather.go b/common/gao_map/weather.go
new file mode 100644
index 0000000..dd9711a
--- /dev/null
+++ b/common/gao_map/weather.go
@@ -0,0 +1,32 @@
+package gao_map
+
+import (
+ "github.com/duke-git/lancet/v2/netutil"
+ "net/url"
+)
+
+const weatherInfoUrl = "https://restapi.amap.com/v3/weather/weatherInfo"
+
+type Weather struct {
+ client *AmapClient
+}
+
+func (w *Weather) Info(adcode, extensions string) (*WeatherResponse, error) {
+ val := url.Values{}
+ val.Set("city", adcode)
+
+ resp, err := w.client.DoRequest(weatherInfoUrl, "GET", val)
+ if err != nil {
+ return nil, err
+ }
+
+ var data WeatherResponse
+
+ if err = netutil.ParseHttpResponse(resp, &data); err != nil {
+ return nil, err
+ } else if err = checkResponse(data.Response); err != nil {
+ return nil, err
+ }
+
+ return &data, nil
+}
diff --git a/common/ip2region/ip2region.go b/common/ip2region/ip2region.go
index 00a0e7e..d41fa85 100644
--- a/common/ip2region/ip2region.go
+++ b/common/ip2region/ip2region.go
@@ -1,7 +1,6 @@
package ip2region
import (
- "fmt"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"os"
"path/filepath"
@@ -13,7 +12,6 @@ func NewIP2Region() *xdb.Searcher {
if err != nil {
panic(err)
}
- fmt.Println(cwd)
dbPath := filepath.Join(cwd, "resources/ip2region", "ip2region.xdb")
cBuff, err := xdb.LoadContentFromFile(dbPath)
if err != nil {
diff --git a/common/utils/generate_file_hash.go b/common/utils/generate_file_hash.go
deleted file mode 100644
index 8b583d7..0000000
--- a/common/utils/generate_file_hash.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package utils
-
-import (
- "crypto"
- "encoding/hex"
- "fmt"
- "hash"
- "io"
- "os"
-)
-
-// SupportedHashFuncs 定义支持的哈希函数类型
-var SupportedHashFuncs = map[string]func() hash.Hash{
- "md5": crypto.MD5.New,
- "sha1": crypto.SHA1.New,
- "sha256": crypto.SHA256.New,
- "sha512": crypto.SHA512.New,
-}
-
-// CalculateFileHash 根据指定的哈希算法计算文件的哈希值
-func CalculateFileHash(filePath string, algorithm string) (string, error) {
- // 获取对应的哈希函数
- hashFunc, exists := SupportedHashFuncs[algorithm]
- if !exists {
- return "", fmt.Errorf("unsupported hash algorithm: %s", algorithm)
- }
-
- // 打开文件
- file, err := os.Open(filePath)
- if err != nil {
- return "", fmt.Errorf("failed to open file: %w", err)
- }
- defer file.Close()
-
- // 创建哈希对象
- h := hashFunc()
-
- // 计算哈希值
- if _, err := io.Copy(h, file); err != nil {
- return "", fmt.Errorf("failed to calculate hash: %w", err)
- }
-
- // 返回哈希值的十六进制字符串
- return hex.EncodeToString(h.Sum(nil)), nil
-}
-
-// CalculateStreamHash 计算输入流的哈希值
-func CalculateStreamHash(reader io.Reader, algorithm string) (string, error) {
- // 获取对应的哈希函数
- hashFunc, exists := SupportedHashFuncs[algorithm]
- if !exists {
- return "", fmt.Errorf("unsupported hash algorithm: %s", algorithm)
- }
-
- // 创建哈希对象
- h := hashFunc()
-
- // 从输入流计算哈希值
- if _, err := io.Copy(h, reader); err != nil {
- return "", fmt.Errorf("failed to calculate hash: %w", err)
- }
-
- // 返回哈希值的十六进制字符串
- return hex.EncodeToString(h.Sum(nil)), nil
-}
diff --git a/common/utils/generate_image_hash.go b/common/utils/generate_image_hash.go
new file mode 100644
index 0000000..46362f4
--- /dev/null
+++ b/common/utils/generate_image_hash.go
@@ -0,0 +1,24 @@
+package utils
+
+import (
+ "github.com/corona10/goimagehash"
+ "image"
+)
+
+// CalculatePerceptualHash 计算感知哈希
+func CalculatePerceptualHash(img image.Image) (string, error) {
+ hash, err := goimagehash.PerceptionHash(img)
+ if err != nil {
+ return "", err
+ }
+ return hash.ToString(), nil
+}
+
+// CalculateHash 计算平均哈希
+func CalculateHash(img image.Image) (uint64, error) {
+ hash, err := goimagehash.AverageHash(img)
+ if err != nil {
+ return 0, err
+ }
+ return hash.GetHash(), nil
+}
diff --git a/common/utils/image_util.go b/common/utils/image_util.go
index dce5db6..7c8e719 100644
--- a/common/utils/image_util.go
+++ b/common/utils/image_util.go
@@ -1,8 +1,13 @@
package utils
import (
+ "bytes"
"encoding/base64"
"errors"
+ "fmt"
+ "image"
+ _ "image/jpeg" // 引入 jpeg 解码器
+ _ "image/png" // 引入 png 解码器
"io"
"regexp"
"strings"
@@ -88,3 +93,25 @@ func Base64ToBytes(base64Str string) ([]byte, error) {
}
return data, nil
}
+
+// Base64ToImage 将 Base64 字符串转换为 image.Image 格式
+func Base64ToImage(base64Str string) (image.Image, error) {
+ // 使用正则表达式去除前缀
+ re := regexp.MustCompile(`^data:image/([a-zA-Z]*);base64,`)
+ // 去除前缀部分
+ base64Str = re.ReplaceAllString(base64Str, "")
+
+ // 解码 Base64 字符串
+ data, err := base64.StdEncoding.DecodeString(base64Str)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode base64 string: %v", err)
+ }
+
+ // 使用 image.Decode 解码字节数据
+ img, _, err := image.Decode(bytes.NewReader(data))
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode image: %v", err)
+ }
+
+ return img, nil
+}
diff --git a/go.mod b/go.mod
index 42b6de3..6c47f8d 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
github.com/casbin/gorm-adapter/v3 v3.32.0
github.com/ccpwcn/kgo v1.2.8
github.com/chenmingyong0423/go-mongox/v2 v2.0.0
+ github.com/duke-git/lancet/v2 v2.3.4
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20241220152942-06eb5c6e8230
@@ -31,6 +32,7 @@ require (
golang.org/x/crypto v0.32.0
golang.org/x/text v0.21.0
google.golang.org/grpc v1.69.4
+ google.golang.org/protobuf v1.36.3
gorm.io/driver/mysql v1.5.7
gorm.io/gen v0.3.26
gorm.io/gorm v1.25.12
@@ -41,6 +43,7 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/ArtisanCloud/PowerSocialite/v3 v3.0.7 // indirect
+ github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@@ -52,26 +55,41 @@ require (
github.com/clbanning/mxj v1.8.4 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.5.0 // indirect
+ github.com/coreos/go-semver v0.3.1 // indirect
+ github.com/coreos/go-systemd/v22 v22.5.0 // indirect
+ github.com/corona10/goimagehash v1.1.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/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
github.com/fatih/color v1.18.0 // indirect
+ github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/glebarez/sqlite v1.11.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-openapi/jsonpointer v0.21.0 // indirect
+ github.com/go-openapi/jsonreference v0.21.0 // indirect
+ github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.24.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
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/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
+ github.com/google/gofuzz v1.2.0 // indirect
+ github.com/google/pprof v0.0.0-20250120214715-4e5bb2051dab // 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.25.1 // indirect
@@ -84,11 +102,13 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/karlseguin/ccache/v3 v3.0.6 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.7 // indirect
+ github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/microsoft/go-mssqldb v1.8.0 // indirect
@@ -98,6 +118,7 @@ require (
github.com/mozillazg/go-httpheader v0.4.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
+ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/orcaman/concurrent-map/v2 v2.0.1 // indirect
@@ -106,28 +127,32 @@ require (
github.com/pkg6/go-requests v0.2.3 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
- github.com/prometheus/common v0.61.0 // indirect
+ github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect
github.com/refraction-networking/utls v1.6.7 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
+ go.etcd.io/etcd/api/v3 v3.5.17 // indirect
+ go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect
+ go.etcd.io/etcd/client/v3 v3.5.17 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
- go.opentelemetry.io/otel v1.33.0 // indirect
+ go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect
- go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 // indirect
- go.opentelemetry.io/otel/exporters/zipkin v1.33.0 // indirect
- go.opentelemetry.io/otel/metric v1.33.0 // indirect
- go.opentelemetry.io/otel/sdk v1.33.0 // indirect
- go.opentelemetry.io/otel/trace v1.33.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
+ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 // indirect
+ go.opentelemetry.io/otel/exporters/zipkin v1.34.0 // indirect
+ go.opentelemetry.io/otel/metric v1.34.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.34.0 // indirect
+ go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.0 // indirect
@@ -137,21 +162,33 @@ require (
golang.org/x/image v0.23.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect
+ golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
- golang.org/x/time v0.8.0 // indirect
+ golang.org/x/term v0.28.0 // indirect
+ golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.29.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect
- google.golang.org/protobuf v1.36.2 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // 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
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/datatypes v1.2.5 // indirect
gorm.io/driver/postgres v1.5.11 // indirect
gorm.io/driver/sqlserver v1.5.4 // indirect
gorm.io/hints v1.1.2 // indirect
- modernc.org/libc v1.61.7 // 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/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.9 // indirect
modernc.org/mathutil v1.7.1 // indirect
- modernc.org/memory v1.8.1 // indirect
- modernc.org/sqlite v1.34.4 // indirect
+ modernc.org/memory v1.8.2 // indirect
+ modernc.org/sqlite v1.34.5 // 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
)
diff --git a/go.sum b/go.sum
index 6de4f1d..72eb075 100644
--- a/go.sum
+++ b/go.sum
@@ -29,7 +29,13 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mx
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e h1:lqIUFzxaqyYqUn4MhzAvSAh4wIte/iLNcIEWxpT/qbc=
+github.com/Kagami/go-face v0.0.0-20210630145111-0c14797b4d0e/go.mod h1:9wdDJkRgo3SGTcFwbQ7elVIQhIr2bbBjecuY7VoqmPU=
github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=
+github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
+github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
+github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0=
+github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8=
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.2.0 h1:gUWBCekWIFWYK7jXhRvPHIa7mRhJih2BGPjzvzodO18=
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.2.0/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
@@ -69,19 +75,36 @@ github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyM
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
+github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
+github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI=
+github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+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/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=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
+github.com/duke-git/lancet/v2 v2.3.4 h1:8XGI7P9w+/GqmEBEXYaH/XuNiM0f4/90Ioti0IvYJls=
+github.com/duke-git/lancet/v2 v2.3.4/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
+github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
+github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
@@ -90,10 +113,24 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
+github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -105,8 +142,12 @@ github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
@@ -126,15 +167,24 @@ 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.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
+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=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
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-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/pprof v0.0.0-20250120214715-4e5bb2051dab h1:FcOz+Hn0n9Ek4Ij6mvYUhvHGg0+EhV9ImR+PMmTP0S0=
+github.com/google/pprof v0.0.0-20250120214715-4e5bb2051dab/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=
@@ -144,6 +194,7 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
@@ -175,14 +226,21 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/karlseguin/ccache/v3 v3.0.6 h1:6wC04CXSdptebuSUBgsQixNrrRMUdimtwmjlJUpCf/4=
github.com/karlseguin/ccache/v3 v3.0.6/go.mod h1:b0qfdUOHl4vJgKFQN41paXIdBb3acAtyX2uWrBAZs1w=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
@@ -195,6 +253,10 @@ github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20241220152942-06eb5c6e8
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20241220152942-06eb5c6e8230/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
github.com/lxzan/gws v1.8.8 h1:st193ZG8qN8sSw8/g/UituFhs7etmKzS7jUqhijg5wM=
github.com/lxzan/gws v1.8.8/go.mod h1:FcGeRMB7HwGuTvMLR24ku0Zx0p6RXqeKASeMc4VYgi4=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
+github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -226,6 +288,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.4.1 h1:zwzjtX4uYyiaU02K5Ia3zSkpJZrByARkRB4V3YPrr0g=
github.com/nicksnyder/go-i18n/v2 v2.4.1/go.mod h1:++Pl70FR6Cki7hdzZRnEEqdc2dJt+SAGotyFg/SvZMk=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
@@ -259,6 +323,8 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
+github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
+github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
@@ -275,6 +341,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -297,6 +365,8 @@ github.com/wenlng/go-captcha-assets v1.0.1 h1:AdjRFMKmadPRWRTv0XEYfjDvcaayZ2yExI
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=
github.com/wenlng/go-captcha/v2 v2.0.2/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
@@ -309,37 +379,72 @@ github.com/yitter/idgenerator-go v1.3.3 h1:i6rzmpbCL0vlmr/tuW5+lSQzNuDG9vYBjIYRv
github.com/yitter/idgenerator-go v1.3.3/go.mod h1:VVjbqFjGUsIkaXVkXEdmx1LiXUL3K1NvyxWPJBPbBpE=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
+github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zeromicro/go-zero v1.7.6 h1:SArK4xecdrpVY3ZFJcbc0IZCx+NuWyHNjCv9f1+Gwrc=
github.com/zeromicro/go-zero v1.7.6/go.mod h1:SmGykRm5e0Z4CGNj+GaSKDffaHzQV56fel0FkymTLlE=
github.com/zmexing/go-sensitive-word v1.3.0 h1:dB9S9kNklksOODGLLAov0RaVCwC2w9Kwxz6NZMdM6rk=
github.com/zmexing/go-sensitive-word v1.3.0/go.mod h1:wkNIpkq1iPOe3l7l83zvnnV5mm20jfj2x8V8kjOTsUM=
+go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk=
+go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM=
+go.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=
+go.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=
+go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA=
+go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU=
+go.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=
+go.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=
+go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4=
+go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU=
+go.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=
+go.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=
go.mongodb.org/mongo-driver/v2 v2.0.0 h1:Jfd7XpdZa9yk3eY774bO7SWVb30noLSirL9nKTpavhI=
go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0 h1:jBpDk4HAUsrnVO1FsfCfCOTEc/MkInJmvfCHYLFiT80=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.34.0/go.mod h1:H9LUIM1daaeZaz91vZcfeM0fejXPmgCYE8ZhzqfJuiU=
go.opentelemetry.io/otel/exporters/zipkin v1.33.0 h1:aFexjEJIw5kVz6vQwnsqCG/nTV/UpsZh7MtQwGmH1eI=
go.opentelemetry.io/otel/exporters/zipkin v1.33.0/go.mod h1:aYsOzr/SZwZXJM6DJmSP/ST2P7MYxuc0R9RewkFVp9s=
+go.opentelemetry.io/otel/exporters/zipkin v1.34.0 h1:GSjCkoYqsnvUMCjxF18j2tCWH8fhGZYjH3iYgechPTI=
+go.opentelemetry.io/otel/exporters/zipkin v1.34.0/go.mod h1:h830hluwAqgSNnZbxL2rJhmAlE7/0SF9esoHVLU04Gc=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
@@ -353,6 +458,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
@@ -368,14 +475,21 @@ golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbD
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
@@ -388,13 +502,24 @@ 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/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
+golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -420,6 +545,8 @@ golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
+golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -436,28 +563,39 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+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/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=
-google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
-google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
+google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
-google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
-google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
+google.golang.org/protobuf v1.36.3/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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -490,29 +628,63 @@ 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.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw=
+k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
+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.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q=
+k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y=
+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.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg=
+k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0=
+k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU=
+k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg=
+k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
+k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
+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-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
+k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
+k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg=
+k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
+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.10 h1:DnDZT/H6TtoJvQmVf7d8W+lVqEZpIJY/+0ENFh1LIHE=
-modernc.org/ccgo/v4 v4.23.10/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0=
+modernc.org/ccgo/v4 v4.23.13 h1:PFiaemQwE/jdwi8XEHyEV+qYWoIuikLP3T4rvDeJb00=
+modernc.org/ccgo/v4 v4.23.13/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0=
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.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8=
modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
-modernc.org/libc v1.61.7 h1:exz8rasFniviSgh3dH7QBnQHqYh9lolA5hVYfsiwkfo=
-modernc.org/libc v1.61.7/go.mod h1:xspSrXRNVSfWfcfqgvZDVe/Hw5kv4FVC6IRfoms5v/0=
+modernc.org/libc v1.61.9 h1:PLSBXVkifXGELtJ5BOnBUyAHr7lsatNwFU/RRo4kfJM=
+modernc.org/libc v1.61.9/go.mod h1:61xrnzk/aR8gr5bR7Uj/lLFLuXu2/zMpIjcry63Eumk=
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.1 h1:HS1HRg1jEohnuONobEq2WrLEhLyw8+J42yLFTnllm2A=
-modernc.org/memory v1.8.1/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
+modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
+modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
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.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
-modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
+modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
+modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
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=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
+sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
+sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk=
+sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=