add face recognition

This commit is contained in:
2025-01-22 10:36:28 +08:00
parent eab806fb9b
commit c6af9a0461
47 changed files with 3621 additions and 454 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
"gorm.io/gorm"
)
const 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
}

View File

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

View File

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

View File

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

39
app/aisvc/rpc/aisvc.go Normal file
View File

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

18
app/aisvc/rpc/aisvc.proto Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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")) // 格式:<baseSavePath>/<userID>/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
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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$
Password: $LDQ20020618xxx$
Map:
# 高德地图API Key
Key: 54823a494909959a9c8cd8af101bbc32

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,24 +14,29 @@ const TableNameScaStorageInfo = "sca_storage_info"
// ScaStorageInfo mapped from table <sca_storage_info>
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

View File

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