From d6fabedb234dcc6a72038aa0682000b36411e18d Mon Sep 17 00:00:00 2001 From: ivamp Date: Sun, 10 Nov 2024 00:11:00 +0800 Subject: [PATCH] update --- cmd/wire_gen.go | 9 +- configs/config.yaml | 1 + docs/docs.go | 125 +++++ docs/swagger.json | 125 +++++ docs/swagger.yaml | 73 +++ hack/gorm-gen/gorm.go | 1 + internal/base/conf/conf.go | 1 + internal/base/redis/provide.go | 6 + internal/dao/external_users.gen.go | 486 ++++++++++++++++++ internal/dao/gen.go | 8 + internal/dao/user_likes.gen.go | 98 +++- internal/dao/user_tag_scores.gen.go | 101 +++- internal/entity/User.go | 33 +- .../http/controller/application_v1/users.go | 199 +++++++ internal/handler/http/provider.go | 5 + internal/handler/http/request/applications.go | 10 + internal/migrations/1_setup.sql | 43 +- internal/router/api.go | 3 + internal/schema/user.go | 1 + internal/service/post/posts.go | 17 + internal/service/post/tags.go | 8 +- internal/service/provider.go | 5 + internal/service/user/external.go | 35 ++ internal/service/user/posts.go | 148 ++++++ internal/service/user/provider.go | 25 + internal/service/user/tags.go | 114 ++++ pkg/consts/posts.go | 7 + 27 files changed, 1645 insertions(+), 42 deletions(-) create mode 100644 internal/dao/external_users.gen.go create mode 100644 internal/handler/http/controller/application_v1/users.go create mode 100644 internal/service/user/external.go create mode 100644 internal/service/user/posts.go create mode 100644 internal/service/user/provider.go create mode 100644 internal/service/user/tags.go create mode 100644 pkg/consts/posts.go diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 0c7ade9..b41b9e0 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -33,6 +33,7 @@ import ( "leafdev.top/Ecosystem/recommender/internal/service/jwks" "leafdev.top/Ecosystem/recommender/internal/service/post" "leafdev.top/Ecosystem/recommender/internal/service/stream" + "leafdev.top/Ecosystem/recommender/internal/service/user" ) // Injectors from wire.go: @@ -52,7 +53,10 @@ func CreateApp() (*base.Application, error) { categoryService := category.NewService(query) postController := application_v1.NewPostController(authService, applicationService, postService, categoryService) categoryController := application_v1.NewCategoryController(authService, applicationService, postService, categoryService) - handlers := http.NewHandler(applicationController, application_v1ApplicationController, postController, categoryController) + userService := user.NewService(query, postService, loggerLogger) + redisRedis := redis.NewRedis(config) + userController := application_v1.NewUserController(authService, applicationService, userService, postService, loggerLogger, redisRedis) + handlers := http.NewHandler(applicationController, application_v1ApplicationController, postController, categoryController, userController) api := router.NewApiRoute(handlers) swaggerRouter := router.NewSwaggerRoute() ginLoggerMiddleware := middleware.NewGinLoggerMiddleware(loggerLogger) @@ -67,8 +71,7 @@ func CreateApp() (*base.Application, error) { grpcInterceptor := grpc.NewInterceptor(interceptorAuth, interceptorLogger) grpcHandlers := grpc.NewHandler(documentService, grpcInterceptor) handlerHandler := handler.NewHandler(grpcHandlers, handlers) - serviceService := service.NewService(loggerLogger, jwksJWKS, streamService, authService, applicationService, postService, categoryService) - redisRedis := redis.NewRedis(config) + serviceService := service.NewService(loggerLogger, jwksJWKS, streamService, authService, applicationService, postService, categoryService, userService) batchBatch := batch.NewBatch(loggerLogger) s3S3 := s3.NewS3(config) baseApplication := base.NewApplication(config, httpServer, handlerHandler, loggerLogger, serviceService, httpMiddleware, redisRedis, batchBatch, s3S3, db, query) diff --git a/configs/config.yaml b/configs/config.yaml index 09a9386..cb1935e 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -22,6 +22,7 @@ redis: port: 6379 password: "" db: 0 + prefix: "recommender_" jwks: url: "" diff --git a/docs/docs.go b/docs/docs.go index 2b87ba2..259706d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -697,6 +697,120 @@ const docTemplate = `{ } } } + }, + "/applications/v1/users/_dislike": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从用户的标签喜好中移除内容", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "application_api" + ], + "summary": "Dislike", + "parameters": [ + { + "description": "UserLikePost", + "name": "UserLikePost", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UserLikePost" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.ResponseBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.Category" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ResponseBody" + } + } + } + } + }, + "/applications/v1/users/_like": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "将标签附加到用户名", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "application_api" + ], + "summary": "Like", + "parameters": [ + { + "description": "UserLikePost", + "name": "UserLikePost", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UserLikePost" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.ResponseBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.Category" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ResponseBody" + } + } + } + } } }, "definitions": { @@ -900,6 +1014,17 @@ const docTemplate = `{ } } }, + "request.UserLikePost": { + "type": "object", + "properties": { + "external_user_id": { + "type": "string" + }, + "post_id": { + "type": "integer" + } + } + }, "response.ResponseBody": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 6d3fe2d..71f0963 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -688,6 +688,120 @@ } } } + }, + "/applications/v1/users/_dislike": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "从用户的标签喜好中移除内容", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "application_api" + ], + "summary": "Dislike", + "parameters": [ + { + "description": "UserLikePost", + "name": "UserLikePost", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UserLikePost" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.ResponseBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.Category" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ResponseBody" + } + } + } + } + }, + "/applications/v1/users/_like": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "将标签附加到用户名", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "application_api" + ], + "summary": "Like", + "parameters": [ + { + "description": "UserLikePost", + "name": "UserLikePost", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.UserLikePost" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.ResponseBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entity.Category" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.ResponseBody" + } + } + } + } } }, "definitions": { @@ -891,6 +1005,17 @@ } } }, + "request.UserLikePost": { + "type": "object", + "properties": { + "external_user_id": { + "type": "string" + }, + "post_id": { + "type": "integer" + } + } + }, "response.ResponseBody": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 3e938a5..0c30b05 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -133,6 +133,13 @@ definitions: - target_id - title type: object + request.UserLikePost: + properties: + external_user_id: + type: string + post_id: + type: integer + type: object response.ResponseBody: properties: data: {} @@ -541,6 +548,72 @@ paths: summary: 新建资源 tags: - application_api + /applications/v1/users/_dislike: + post: + consumes: + - application/json + description: 从用户的标签喜好中移除内容 + parameters: + - description: UserLikePost + in: body + name: UserLikePost + required: true + schema: + $ref: '#/definitions/request.UserLikePost' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.ResponseBody' + - properties: + data: + $ref: '#/definitions/entity.Category' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.ResponseBody' + security: + - ApiKeyAuth: [] + summary: Dislike + tags: + - application_api + /applications/v1/users/_like: + post: + consumes: + - application/json + description: 将标签附加到用户名 + parameters: + - description: UserLikePost + in: body + name: UserLikePost + required: true + schema: + $ref: '#/definitions/request.UserLikePost' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.ResponseBody' + - properties: + data: + $ref: '#/definitions/entity.Category' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.ResponseBody' + security: + - ApiKeyAuth: [] + summary: Like + tags: + - application_api securityDefinitions: ApiKeyAuth: in: header diff --git a/hack/gorm-gen/gorm.go b/hack/gorm-gen/gorm.go index a96dff3..9359888 100644 --- a/hack/gorm-gen/gorm.go +++ b/hack/gorm-gen/gorm.go @@ -34,6 +34,7 @@ func main() { entity.ApplicationToken{}, entity.UserTagScore{}, entity.Category{}, + entity.ExternalUser{}, ) // Generate Type Safe API with Dynamic SQL defined on Querier interface for `model.User` and `model.Company` diff --git a/internal/base/conf/conf.go b/internal/base/conf/conf.go index 0e8b1db..9e069c1 100644 --- a/internal/base/conf/conf.go +++ b/internal/base/conf/conf.go @@ -53,6 +53,7 @@ type Redis struct { Host string `yaml:"host"` Port int `yaml:"port"` Password string `yaml:"password"` + Prefix string `yaml:"prefix"` DB int `yaml:"db"` } diff --git a/internal/base/redis/provide.go b/internal/base/redis/provide.go index ac9e253..f28f0ed 100644 --- a/internal/base/redis/provide.go +++ b/internal/base/redis/provide.go @@ -12,6 +12,7 @@ import ( type Redis struct { Client *redis.Client Locker *redislock.Client + config *conf.Config } func NewRedis(c *conf.Config) *Redis { @@ -32,7 +33,12 @@ func NewRedis(c *conf.Config) *Redis { var r = &Redis{ Client: client, Locker: locker, + config: c, } return r } + +func (r *Redis) Prefix(key string) string { + return fmt.Sprintf("%s:%s", r.config.Redis.Prefix, key) +} diff --git a/internal/dao/external_users.gen.go b/internal/dao/external_users.gen.go new file mode 100644 index 0000000..a15ff3d --- /dev/null +++ b/internal/dao/external_users.gen.go @@ -0,0 +1,486 @@ +// 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 dao + +import ( + "context" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" + + "gorm.io/gen" + "gorm.io/gen/field" + + "gorm.io/plugin/dbresolver" + + "leafdev.top/Ecosystem/recommender/internal/entity" +) + +func newExternalUser(db *gorm.DB, opts ...gen.DOOption) externalUser { + _externalUser := externalUser{} + + _externalUser.externalUserDo.UseDB(db, opts...) + _externalUser.externalUserDo.UseModel(&entity.ExternalUser{}) + + tableName := _externalUser.externalUserDo.TableName() + _externalUser.ALL = field.NewAsterisk(tableName) + _externalUser.Id = field.NewUint(tableName, "id") + _externalUser.CreatedAt = field.NewTime(tableName, "created_at") + _externalUser.UpdatedAt = field.NewTime(tableName, "updated_at") + _externalUser.Name = field.NewString(tableName, "name") + _externalUser.Email = field.NewString(tableName, "email") + _externalUser.ExternalId = field.NewString(tableName, "external_id") + _externalUser.Summary = field.NewString(tableName, "summary") + _externalUser.ApplicationId = field.NewUint(tableName, "application_id") + _externalUser.Application = externalUserBelongsToApplication{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Application", "entity.Application"), + } + + _externalUser.fillFieldMap() + + return _externalUser +} + +type externalUser struct { + externalUserDo + + ALL field.Asterisk + Id field.Uint + CreatedAt field.Time + UpdatedAt field.Time + Name field.String + Email field.String + ExternalId field.String + Summary field.String + ApplicationId field.Uint + Application externalUserBelongsToApplication + + fieldMap map[string]field.Expr +} + +func (e externalUser) Table(newTableName string) *externalUser { + e.externalUserDo.UseTable(newTableName) + return e.updateTableName(newTableName) +} + +func (e externalUser) As(alias string) *externalUser { + e.externalUserDo.DO = *(e.externalUserDo.As(alias).(*gen.DO)) + return e.updateTableName(alias) +} + +func (e *externalUser) updateTableName(table string) *externalUser { + e.ALL = field.NewAsterisk(table) + e.Id = field.NewUint(table, "id") + e.CreatedAt = field.NewTime(table, "created_at") + e.UpdatedAt = field.NewTime(table, "updated_at") + e.Name = field.NewString(table, "name") + e.Email = field.NewString(table, "email") + e.ExternalId = field.NewString(table, "external_id") + e.Summary = field.NewString(table, "summary") + e.ApplicationId = field.NewUint(table, "application_id") + + e.fillFieldMap() + + return e +} + +func (e *externalUser) GetFieldByName(fieldName string) (field.OrderExpr, bool) { + _f, ok := e.fieldMap[fieldName] + if !ok || _f == nil { + return nil, false + } + _oe, ok := _f.(field.OrderExpr) + return _oe, ok +} + +func (e *externalUser) fillFieldMap() { + e.fieldMap = make(map[string]field.Expr, 9) + e.fieldMap["id"] = e.Id + e.fieldMap["created_at"] = e.CreatedAt + e.fieldMap["updated_at"] = e.UpdatedAt + e.fieldMap["name"] = e.Name + e.fieldMap["email"] = e.Email + e.fieldMap["external_id"] = e.ExternalId + e.fieldMap["summary"] = e.Summary + e.fieldMap["application_id"] = e.ApplicationId + +} + +func (e externalUser) clone(db *gorm.DB) externalUser { + e.externalUserDo.ReplaceConnPool(db.Statement.ConnPool) + return e +} + +func (e externalUser) replaceDB(db *gorm.DB) externalUser { + e.externalUserDo.ReplaceDB(db) + return e +} + +type externalUserBelongsToApplication struct { + db *gorm.DB + + field.RelationField +} + +func (a externalUserBelongsToApplication) Where(conds ...field.Expr) *externalUserBelongsToApplication { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a externalUserBelongsToApplication) WithContext(ctx context.Context) *externalUserBelongsToApplication { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a externalUserBelongsToApplication) Session(session *gorm.Session) *externalUserBelongsToApplication { + a.db = a.db.Session(session) + return &a +} + +func (a externalUserBelongsToApplication) Model(m *entity.ExternalUser) *externalUserBelongsToApplicationTx { + return &externalUserBelongsToApplicationTx{a.db.Model(m).Association(a.Name())} +} + +type externalUserBelongsToApplicationTx struct{ tx *gorm.Association } + +func (a externalUserBelongsToApplicationTx) Find() (result *entity.Application, err error) { + return result, a.tx.Find(&result) +} + +func (a externalUserBelongsToApplicationTx) Append(values ...*entity.Application) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a externalUserBelongsToApplicationTx) Replace(values ...*entity.Application) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a externalUserBelongsToApplicationTx) Delete(values ...*entity.Application) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a externalUserBelongsToApplicationTx) Clear() error { + return a.tx.Clear() +} + +func (a externalUserBelongsToApplicationTx) Count() int64 { + return a.tx.Count() +} + +type externalUserDo struct{ gen.DO } + +type IExternalUserDo interface { + gen.SubQuery + Debug() IExternalUserDo + WithContext(ctx context.Context) IExternalUserDo + WithResult(fc func(tx gen.Dao)) gen.ResultInfo + ReplaceDB(db *gorm.DB) + ReadDB() IExternalUserDo + WriteDB() IExternalUserDo + As(alias string) gen.Dao + Session(config *gorm.Session) IExternalUserDo + Columns(cols ...field.Expr) gen.Columns + Clauses(conds ...clause.Expression) IExternalUserDo + Not(conds ...gen.Condition) IExternalUserDo + Or(conds ...gen.Condition) IExternalUserDo + Select(conds ...field.Expr) IExternalUserDo + Where(conds ...gen.Condition) IExternalUserDo + Order(conds ...field.Expr) IExternalUserDo + Distinct(cols ...field.Expr) IExternalUserDo + Omit(cols ...field.Expr) IExternalUserDo + Join(table schema.Tabler, on ...field.Expr) IExternalUserDo + LeftJoin(table schema.Tabler, on ...field.Expr) IExternalUserDo + RightJoin(table schema.Tabler, on ...field.Expr) IExternalUserDo + Group(cols ...field.Expr) IExternalUserDo + Having(conds ...gen.Condition) IExternalUserDo + Limit(limit int) IExternalUserDo + Offset(offset int) IExternalUserDo + Count() (count int64, err error) + Scopes(funcs ...func(gen.Dao) gen.Dao) IExternalUserDo + Unscoped() IExternalUserDo + Create(values ...*entity.ExternalUser) error + CreateInBatches(values []*entity.ExternalUser, batchSize int) error + Save(values ...*entity.ExternalUser) error + First() (*entity.ExternalUser, error) + Take() (*entity.ExternalUser, error) + Last() (*entity.ExternalUser, error) + Find() ([]*entity.ExternalUser, error) + FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*entity.ExternalUser, err error) + FindInBatches(result *[]*entity.ExternalUser, batchSize int, fc func(tx gen.Dao, batch int) error) error + Pluck(column field.Expr, dest interface{}) error + Delete(...*entity.ExternalUser) (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) IExternalUserDo + Assign(attrs ...field.AssignExpr) IExternalUserDo + Joins(fields ...field.RelationField) IExternalUserDo + Preload(fields ...field.RelationField) IExternalUserDo + FirstOrInit() (*entity.ExternalUser, error) + FirstOrCreate() (*entity.ExternalUser, error) + FindByPage(offset int, limit int) (result []*entity.ExternalUser, 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) IExternalUserDo + UnderlyingDB() *gorm.DB + schema.Tabler +} + +func (e externalUserDo) Debug() IExternalUserDo { + return e.withDO(e.DO.Debug()) +} + +func (e externalUserDo) WithContext(ctx context.Context) IExternalUserDo { + return e.withDO(e.DO.WithContext(ctx)) +} + +func (e externalUserDo) ReadDB() IExternalUserDo { + return e.Clauses(dbresolver.Read) +} + +func (e externalUserDo) WriteDB() IExternalUserDo { + return e.Clauses(dbresolver.Write) +} + +func (e externalUserDo) Session(config *gorm.Session) IExternalUserDo { + return e.withDO(e.DO.Session(config)) +} + +func (e externalUserDo) Clauses(conds ...clause.Expression) IExternalUserDo { + return e.withDO(e.DO.Clauses(conds...)) +} + +func (e externalUserDo) Returning(value interface{}, columns ...string) IExternalUserDo { + return e.withDO(e.DO.Returning(value, columns...)) +} + +func (e externalUserDo) Not(conds ...gen.Condition) IExternalUserDo { + return e.withDO(e.DO.Not(conds...)) +} + +func (e externalUserDo) Or(conds ...gen.Condition) IExternalUserDo { + return e.withDO(e.DO.Or(conds...)) +} + +func (e externalUserDo) Select(conds ...field.Expr) IExternalUserDo { + return e.withDO(e.DO.Select(conds...)) +} + +func (e externalUserDo) Where(conds ...gen.Condition) IExternalUserDo { + return e.withDO(e.DO.Where(conds...)) +} + +func (e externalUserDo) Order(conds ...field.Expr) IExternalUserDo { + return e.withDO(e.DO.Order(conds...)) +} + +func (e externalUserDo) Distinct(cols ...field.Expr) IExternalUserDo { + return e.withDO(e.DO.Distinct(cols...)) +} + +func (e externalUserDo) Omit(cols ...field.Expr) IExternalUserDo { + return e.withDO(e.DO.Omit(cols...)) +} + +func (e externalUserDo) Join(table schema.Tabler, on ...field.Expr) IExternalUserDo { + return e.withDO(e.DO.Join(table, on...)) +} + +func (e externalUserDo) LeftJoin(table schema.Tabler, on ...field.Expr) IExternalUserDo { + return e.withDO(e.DO.LeftJoin(table, on...)) +} + +func (e externalUserDo) RightJoin(table schema.Tabler, on ...field.Expr) IExternalUserDo { + return e.withDO(e.DO.RightJoin(table, on...)) +} + +func (e externalUserDo) Group(cols ...field.Expr) IExternalUserDo { + return e.withDO(e.DO.Group(cols...)) +} + +func (e externalUserDo) Having(conds ...gen.Condition) IExternalUserDo { + return e.withDO(e.DO.Having(conds...)) +} + +func (e externalUserDo) Limit(limit int) IExternalUserDo { + return e.withDO(e.DO.Limit(limit)) +} + +func (e externalUserDo) Offset(offset int) IExternalUserDo { + return e.withDO(e.DO.Offset(offset)) +} + +func (e externalUserDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IExternalUserDo { + return e.withDO(e.DO.Scopes(funcs...)) +} + +func (e externalUserDo) Unscoped() IExternalUserDo { + return e.withDO(e.DO.Unscoped()) +} + +func (e externalUserDo) Create(values ...*entity.ExternalUser) error { + if len(values) == 0 { + return nil + } + return e.DO.Create(values) +} + +func (e externalUserDo) CreateInBatches(values []*entity.ExternalUser, batchSize int) error { + return e.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 (e externalUserDo) Save(values ...*entity.ExternalUser) error { + if len(values) == 0 { + return nil + } + return e.DO.Save(values) +} + +func (e externalUserDo) First() (*entity.ExternalUser, error) { + if result, err := e.DO.First(); err != nil { + return nil, err + } else { + return result.(*entity.ExternalUser), nil + } +} + +func (e externalUserDo) Take() (*entity.ExternalUser, error) { + if result, err := e.DO.Take(); err != nil { + return nil, err + } else { + return result.(*entity.ExternalUser), nil + } +} + +func (e externalUserDo) Last() (*entity.ExternalUser, error) { + if result, err := e.DO.Last(); err != nil { + return nil, err + } else { + return result.(*entity.ExternalUser), nil + } +} + +func (e externalUserDo) Find() ([]*entity.ExternalUser, error) { + result, err := e.DO.Find() + return result.([]*entity.ExternalUser), err +} + +func (e externalUserDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*entity.ExternalUser, err error) { + buf := make([]*entity.ExternalUser, 0, batchSize) + err = e.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 (e externalUserDo) FindInBatches(result *[]*entity.ExternalUser, batchSize int, fc func(tx gen.Dao, batch int) error) error { + return e.DO.FindInBatches(result, batchSize, fc) +} + +func (e externalUserDo) Attrs(attrs ...field.AssignExpr) IExternalUserDo { + return e.withDO(e.DO.Attrs(attrs...)) +} + +func (e externalUserDo) Assign(attrs ...field.AssignExpr) IExternalUserDo { + return e.withDO(e.DO.Assign(attrs...)) +} + +func (e externalUserDo) Joins(fields ...field.RelationField) IExternalUserDo { + for _, _f := range fields { + e = *e.withDO(e.DO.Joins(_f)) + } + return &e +} + +func (e externalUserDo) Preload(fields ...field.RelationField) IExternalUserDo { + for _, _f := range fields { + e = *e.withDO(e.DO.Preload(_f)) + } + return &e +} + +func (e externalUserDo) FirstOrInit() (*entity.ExternalUser, error) { + if result, err := e.DO.FirstOrInit(); err != nil { + return nil, err + } else { + return result.(*entity.ExternalUser), nil + } +} + +func (e externalUserDo) FirstOrCreate() (*entity.ExternalUser, error) { + if result, err := e.DO.FirstOrCreate(); err != nil { + return nil, err + } else { + return result.(*entity.ExternalUser), nil + } +} + +func (e externalUserDo) FindByPage(offset int, limit int) (result []*entity.ExternalUser, count int64, err error) { + result, err = e.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 = e.Offset(-1).Limit(-1).Count() + return +} + +func (e externalUserDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { + count, err = e.Count() + if err != nil { + return + } + + err = e.Offset(offset).Limit(limit).Scan(result) + return +} + +func (e externalUserDo) Scan(result interface{}) (err error) { + return e.DO.Scan(result) +} + +func (e externalUserDo) Delete(models ...*entity.ExternalUser) (result gen.ResultInfo, err error) { + return e.DO.Delete(models) +} + +func (e *externalUserDo) withDO(do gen.Dao) *externalUserDo { + e.DO = *do.(*gen.DO) + return e +} diff --git a/internal/dao/gen.go b/internal/dao/gen.go index 7e2d280..04affe3 100644 --- a/internal/dao/gen.go +++ b/internal/dao/gen.go @@ -20,6 +20,7 @@ var ( Application *application ApplicationToken *applicationToken Category *category + ExternalUser *externalUser Post *post PostTag *postTag Tag *tag @@ -33,6 +34,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) { Application = &Q.Application ApplicationToken = &Q.ApplicationToken Category = &Q.Category + ExternalUser = &Q.ExternalUser Post = &Q.Post PostTag = &Q.PostTag Tag = &Q.Tag @@ -47,6 +49,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query { Application: newApplication(db, opts...), ApplicationToken: newApplicationToken(db, opts...), Category: newCategory(db, opts...), + ExternalUser: newExternalUser(db, opts...), Post: newPost(db, opts...), PostTag: newPostTag(db, opts...), Tag: newTag(db, opts...), @@ -62,6 +65,7 @@ type Query struct { Application application ApplicationToken applicationToken Category category + ExternalUser externalUser Post post PostTag postTag Tag tag @@ -78,6 +82,7 @@ func (q *Query) clone(db *gorm.DB) *Query { Application: q.Application.clone(db), ApplicationToken: q.ApplicationToken.clone(db), Category: q.Category.clone(db), + ExternalUser: q.ExternalUser.clone(db), Post: q.Post.clone(db), PostTag: q.PostTag.clone(db), Tag: q.Tag.clone(db), @@ -101,6 +106,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query { Application: q.Application.replaceDB(db), ApplicationToken: q.ApplicationToken.replaceDB(db), Category: q.Category.replaceDB(db), + ExternalUser: q.ExternalUser.replaceDB(db), Post: q.Post.replaceDB(db), PostTag: q.PostTag.replaceDB(db), Tag: q.Tag.replaceDB(db), @@ -114,6 +120,7 @@ type queryCtx struct { Application IApplicationDo ApplicationToken IApplicationTokenDo Category ICategoryDo + ExternalUser IExternalUserDo Post IPostDo PostTag IPostTagDo Tag ITagDo @@ -127,6 +134,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx { Application: q.Application.WithContext(ctx), ApplicationToken: q.ApplicationToken.WithContext(ctx), Category: q.Category.WithContext(ctx), + ExternalUser: q.ExternalUser.WithContext(ctx), Post: q.Post.WithContext(ctx), PostTag: q.PostTag.WithContext(ctx), Tag: q.Tag.WithContext(ctx), diff --git a/internal/dao/user_likes.gen.go b/internal/dao/user_likes.gen.go index 837b6f1..f51147f 100644 --- a/internal/dao/user_likes.gen.go +++ b/internal/dao/user_likes.gen.go @@ -27,9 +27,15 @@ func newUserLike(db *gorm.DB, opts ...gen.DOOption) userLike { tableName := _userLike.userLikeDo.TableName() _userLike.ALL = field.NewAsterisk(tableName) - _userLike.UserId = field.NewString(tableName, "user_id") + _userLike.ExternalUserId = field.NewUint(tableName, "external_user_id") _userLike.PostId = field.NewUint(tableName, "post_id") _userLike.Type = field.NewString(tableName, "type") + _userLike.ApplicationId = field.NewUint(tableName, "application_id") + _userLike.Application = userLikeBelongsToApplication{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Application", "entity.Application"), + } _userLike.fillFieldMap() @@ -39,10 +45,12 @@ func newUserLike(db *gorm.DB, opts ...gen.DOOption) userLike { type userLike struct { userLikeDo - ALL field.Asterisk - UserId field.String - PostId field.Uint - Type field.String + ALL field.Asterisk + ExternalUserId field.Uint + PostId field.Uint + Type field.String + ApplicationId field.Uint + Application userLikeBelongsToApplication fieldMap map[string]field.Expr } @@ -59,9 +67,10 @@ func (u userLike) As(alias string) *userLike { func (u *userLike) updateTableName(table string) *userLike { u.ALL = field.NewAsterisk(table) - u.UserId = field.NewString(table, "user_id") + u.ExternalUserId = field.NewUint(table, "external_user_id") u.PostId = field.NewUint(table, "post_id") u.Type = field.NewString(table, "type") + u.ApplicationId = field.NewUint(table, "application_id") u.fillFieldMap() @@ -78,10 +87,12 @@ func (u *userLike) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (u *userLike) fillFieldMap() { - u.fieldMap = make(map[string]field.Expr, 3) - u.fieldMap["user_id"] = u.UserId + u.fieldMap = make(map[string]field.Expr, 5) + u.fieldMap["external_user_id"] = u.ExternalUserId u.fieldMap["post_id"] = u.PostId u.fieldMap["type"] = u.Type + u.fieldMap["application_id"] = u.ApplicationId + } func (u userLike) clone(db *gorm.DB) userLike { @@ -94,6 +105,77 @@ func (u userLike) replaceDB(db *gorm.DB) userLike { return u } +type userLikeBelongsToApplication struct { + db *gorm.DB + + field.RelationField +} + +func (a userLikeBelongsToApplication) Where(conds ...field.Expr) *userLikeBelongsToApplication { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a userLikeBelongsToApplication) WithContext(ctx context.Context) *userLikeBelongsToApplication { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a userLikeBelongsToApplication) Session(session *gorm.Session) *userLikeBelongsToApplication { + a.db = a.db.Session(session) + return &a +} + +func (a userLikeBelongsToApplication) Model(m *entity.UserLike) *userLikeBelongsToApplicationTx { + return &userLikeBelongsToApplicationTx{a.db.Model(m).Association(a.Name())} +} + +type userLikeBelongsToApplicationTx struct{ tx *gorm.Association } + +func (a userLikeBelongsToApplicationTx) Find() (result *entity.Application, err error) { + return result, a.tx.Find(&result) +} + +func (a userLikeBelongsToApplicationTx) Append(values ...*entity.Application) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a userLikeBelongsToApplicationTx) Replace(values ...*entity.Application) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a userLikeBelongsToApplicationTx) Delete(values ...*entity.Application) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a userLikeBelongsToApplicationTx) Clear() error { + return a.tx.Clear() +} + +func (a userLikeBelongsToApplicationTx) Count() int64 { + return a.tx.Count() +} + type userLikeDo struct{ gen.DO } type IUserLikeDo interface { diff --git a/internal/dao/user_tag_scores.gen.go b/internal/dao/user_tag_scores.gen.go index 352a377..0c3d33b 100644 --- a/internal/dao/user_tag_scores.gen.go +++ b/internal/dao/user_tag_scores.gen.go @@ -27,9 +27,10 @@ func newUserTagScore(db *gorm.DB, opts ...gen.DOOption) userTagScore { tableName := _userTagScore.userTagScoreDo.TableName() _userTagScore.ALL = field.NewAsterisk(tableName) - _userTagScore.UserId = field.NewString(tableName, "user_id") + _userTagScore.ExternalUserId = field.NewUint(tableName, "external_user_id") _userTagScore.TagId = field.NewUint(tableName, "tag_id") _userTagScore.Score = field.NewInt(tableName, "score") + _userTagScore.ApplicationId = field.NewUint(tableName, "application_id") _userTagScore.Tag = userTagScoreBelongsToTag{ db: db.Session(&gorm.Session{}), @@ -41,6 +42,12 @@ func newUserTagScore(db *gorm.DB, opts ...gen.DOOption) userTagScore { }, } + _userTagScore.Application = userTagScoreBelongsToApplication{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Application", "entity.Application"), + } + _userTagScore.fillFieldMap() return _userTagScore @@ -49,11 +56,14 @@ func newUserTagScore(db *gorm.DB, opts ...gen.DOOption) userTagScore { type userTagScore struct { userTagScoreDo - ALL field.Asterisk - UserId field.String - TagId field.Uint - Score field.Int - Tag userTagScoreBelongsToTag + ALL field.Asterisk + ExternalUserId field.Uint + TagId field.Uint + Score field.Int + ApplicationId field.Uint + Tag userTagScoreBelongsToTag + + Application userTagScoreBelongsToApplication fieldMap map[string]field.Expr } @@ -70,9 +80,10 @@ func (u userTagScore) As(alias string) *userTagScore { func (u *userTagScore) updateTableName(table string) *userTagScore { u.ALL = field.NewAsterisk(table) - u.UserId = field.NewString(table, "user_id") + u.ExternalUserId = field.NewUint(table, "external_user_id") u.TagId = field.NewUint(table, "tag_id") u.Score = field.NewInt(table, "score") + u.ApplicationId = field.NewUint(table, "application_id") u.fillFieldMap() @@ -89,10 +100,11 @@ func (u *userTagScore) GetFieldByName(fieldName string) (field.OrderExpr, bool) } func (u *userTagScore) fillFieldMap() { - u.fieldMap = make(map[string]field.Expr, 4) - u.fieldMap["user_id"] = u.UserId + u.fieldMap = make(map[string]field.Expr, 6) + u.fieldMap["external_user_id"] = u.ExternalUserId u.fieldMap["tag_id"] = u.TagId u.fieldMap["score"] = u.Score + u.fieldMap["application_id"] = u.ApplicationId } @@ -181,6 +193,77 @@ func (a userTagScoreBelongsToTagTx) Count() int64 { return a.tx.Count() } +type userTagScoreBelongsToApplication struct { + db *gorm.DB + + field.RelationField +} + +func (a userTagScoreBelongsToApplication) Where(conds ...field.Expr) *userTagScoreBelongsToApplication { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a userTagScoreBelongsToApplication) WithContext(ctx context.Context) *userTagScoreBelongsToApplication { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a userTagScoreBelongsToApplication) Session(session *gorm.Session) *userTagScoreBelongsToApplication { + a.db = a.db.Session(session) + return &a +} + +func (a userTagScoreBelongsToApplication) Model(m *entity.UserTagScore) *userTagScoreBelongsToApplicationTx { + return &userTagScoreBelongsToApplicationTx{a.db.Model(m).Association(a.Name())} +} + +type userTagScoreBelongsToApplicationTx struct{ tx *gorm.Association } + +func (a userTagScoreBelongsToApplicationTx) Find() (result *entity.Application, err error) { + return result, a.tx.Find(&result) +} + +func (a userTagScoreBelongsToApplicationTx) Append(values ...*entity.Application) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a userTagScoreBelongsToApplicationTx) Replace(values ...*entity.Application) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a userTagScoreBelongsToApplicationTx) Delete(values ...*entity.Application) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a userTagScoreBelongsToApplicationTx) Clear() error { + return a.tx.Clear() +} + +func (a userTagScoreBelongsToApplicationTx) Count() int64 { + return a.tx.Count() +} + type userTagScoreDo struct{ gen.DO } type IUserTagScoreDo interface { diff --git a/internal/entity/User.go b/internal/entity/User.go index e778291..3fec441 100644 --- a/internal/entity/User.go +++ b/internal/entity/User.go @@ -11,10 +11,27 @@ func (u *User) TableName() string { return "users" } +type ExternalUser struct { + Model + Id schema.EntityId `gorm:"primarykey" json:"id"` + Name string `json:"name"` + Email string `json:"email"` + ExternalId string `json:"external_id"` + Summary string `json:"summary"` + ApplicationId schema.EntityId `json:"application_id"` + Application *Application +} + +func (u *ExternalUser) TableName() string { + return "external_users" +} + type UserLike struct { - UserId schema.UserId `gorm:"primarykey" json:"user_id"` - PostId schema.EntityId `gorm:"primarykey" json:"post_id"` - Type schema.UserLikeType `json:"type"` + ExternalUserId schema.EntityId `gorm:"primarykey" json:"external_user_id"` + PostId schema.EntityId `gorm:"primarykey" json:"post_id"` + Type schema.UserLikeType `json:"type"` + Application *Application + ApplicationId schema.EntityId `json:"application_id"` } func (u *UserLike) TableName() string { @@ -22,10 +39,12 @@ func (u *UserLike) TableName() string { } type UserTagScore struct { - UserId schema.UserId `gorm:"primarykey" json:"user_id"` - TagId schema.EntityId `gorm:"primarykey" json:"tag_id"` - Tag *Tag - Score int `json:"score"` + ExternalUserId schema.EntityId `gorm:"primarykey" json:"external_user_id"` + TagId schema.EntityId `gorm:"primarykey" json:"tag_id"` + Tag *Tag + Score int `json:"score"` + Application *Application + ApplicationId schema.EntityId `json:"application_id"` } func (u *UserTagScore) TableName() string { diff --git a/internal/handler/http/controller/application_v1/users.go b/internal/handler/http/controller/application_v1/users.go new file mode 100644 index 0000000..e0a4a72 --- /dev/null +++ b/internal/handler/http/controller/application_v1/users.go @@ -0,0 +1,199 @@ +package application_v1 + +import ( + "github.com/gin-gonic/gin" + "leafdev.top/Ecosystem/recommender/internal/base/logger" + "leafdev.top/Ecosystem/recommender/internal/base/redis" + "leafdev.top/Ecosystem/recommender/internal/handler/http/request" + "leafdev.top/Ecosystem/recommender/internal/handler/http/response" + "leafdev.top/Ecosystem/recommender/internal/service/application" + "leafdev.top/Ecosystem/recommender/internal/service/auth" + "leafdev.top/Ecosystem/recommender/internal/service/post" + "leafdev.top/Ecosystem/recommender/internal/service/user" + "leafdev.top/Ecosystem/recommender/pkg/consts" + "net/http" + "time" +) + +const TaskProcessing = "user_likes" + +var LockTTL = time.Minute * 10 + +type UserController struct { + authService *auth.Service + applicationService *application.Service + userService *user.Service + postService *post.Service + logger *logger.Logger + redis *redis.Redis +} + +func NewUserController( + authService *auth.Service, + applicationService *application.Service, + userService *user.Service, + postService *post.Service, + logger *logger.Logger, + redis *redis.Redis, + +) *UserController { + return &UserController{ + authService: authService, + applicationService: applicationService, + userService: userService, + postService: postService, + logger: logger, + redis: redis, + } +} + +// Like godoc +// @Summary Like +// @Description 将标签附加到用户名 +// @Tags application_api +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param UserLikePost body request.UserLikePost true "UserLikePost" +// @Success 200 {object} response.ResponseBody{data=entity.Category} +// @Failure 400 {object} response.ResponseBody +// @Router /applications/v1/users/_like [post] +func (uc *UserController) Like(c *gin.Context) { + app, err := uc.authService.GetApplication(c) + if err != nil { + response.Ctx(c).Error(err).Status(http.StatusBadRequest).Send() + return + } + + var userLikePostRequest = &request.UserLikePost{} + + if err := c.ShouldBindJSON(userLikePostRequest); err != nil { + response.Ctx(c).Error(err).Status(http.StatusBadRequest).Send() + return + } + + externalUser, err := uc.userService.GetOrCreateExternalUser(c, userLikePostRequest.ExternalUserId, app) + if err != nil { + response.Ctx(c).Error(err).Status(http.StatusInternalServerError).Send() + return + } + + // posts + postEntity, err := uc.postService.GetPostById(c, userLikePostRequest.PostId) + if err != nil { + response.Ctx(c).Error(err).Status(http.StatusInternalServerError).Send() + return + } + + if postEntity.ApplicationId != app.Id { + response.Ctx(c).Status(http.StatusNotFound).Send() + return + } + + // 检测是否有 + var cacheKey = uc.redis.Prefix(TaskProcessing + ":" + userLikePostRequest.PostId.String()) + // if exists + exists, err := uc.redis.Client.Exists(c, cacheKey).Result() + if err != nil { + response.Ctx(c).Error(err).Status(http.StatusInternalServerError).Send() + return + } + + if exists > 0 { + response.Ctx(c).Status(http.StatusTooEarly).Error(consts.ErrAnotherOperationInProgress).Send() + return + } + + _, err = uc.redis.Client.Set(c, cacheKey, userLikePostRequest.ExternalUserId, LockTTL).Result() + + go func(prefix string) { + err = uc.userService.LikePost(c, externalUser, app, postEntity) + if err != nil { + response.Ctx(c).Error(err).Status(http.StatusInternalServerError).Send() + return + } + + _, err := uc.redis.Client.Del(c, prefix).Result() + if err != nil { + uc.logger.Sugar.Error(err) + } + }(cacheKey) + + response.Ctx(c).Status(http.StatusNoContent).Send() + return +} + +// Dislike godoc +// @Summary Dislike +// @Description 从用户的标签喜好中移除内容 +// @Tags application_api +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param UserLikePost body request.UserLikePost true "UserLikePost" +// @Success 200 {object} response.ResponseBody{data=entity.Category} +// @Failure 400 {object} response.ResponseBody +// @Router /applications/v1/users/_dislike [post] +func (uc *UserController) Dislike(c *gin.Context) { + app, err := uc.authService.GetApplication(c) + if err != nil { + response.Ctx(c).Error(err).Status(http.StatusBadRequest).Send() + return + } + + var userDislikePostRequest = &request.UserDislikePost{} + + if err := c.ShouldBindJSON(userDislikePostRequest); err != nil { + response.Ctx(c).Error(err).Status(http.StatusBadRequest).Send() + return + } + + externalUser, err := uc.userService.GetOrCreateExternalUser(c, userDislikePostRequest.ExternalUserId, app) + if err != nil { + response.Ctx(c).Error(err).Status(http.StatusInternalServerError).Send() + return + } + + // posts + postEntity, err := uc.postService.GetPostById(c, userDislikePostRequest.PostId) + if err != nil { + response.Ctx(c).Error(err).Status(http.StatusInternalServerError).Send() + return + } + + if postEntity.ApplicationId != app.Id { + response.Ctx(c).Status(http.StatusNotFound).Send() + return + } + + // 检测是否有 + var cacheKey = uc.redis.Prefix(TaskProcessing + ":" + userDislikePostRequest.PostId.String()) + exists, err := uc.redis.Client.Exists(c, cacheKey).Result() + if err != nil { + response.Ctx(c).Error(err).Status(http.StatusInternalServerError).Send() + return + } + + if exists > 0 { + response.Ctx(c).Status(http.StatusTooEarly).Error(consts.ErrAnotherOperationInProgress).Send() + return + } + + _, err = uc.redis.Client.Set(c, cacheKey, userDislikePostRequest.ExternalUserId, LockTTL).Result() + + go func(prefix string) { + err = uc.userService.DislikePost(c, externalUser, app, postEntity) + if err != nil { + response.Ctx(c).Error(err).Status(http.StatusInternalServerError).Send() + return + } + + _, err := uc.redis.Client.Del(c, prefix).Result() + if err != nil { + uc.logger.Sugar.Error(err) + } + }(cacheKey) + + response.Ctx(c).Status(http.StatusNoContent).Send() + return +} diff --git a/internal/handler/http/provider.go b/internal/handler/http/provider.go index b7b9541..d523bb0 100644 --- a/internal/handler/http/provider.go +++ b/internal/handler/http/provider.go @@ -17,6 +17,7 @@ var ProviderSet = wire.NewSet( application_v1.NewApplicationController, application_v1.NewPostController, application_v1.NewCategoryController, + application_v1.NewUserController, NewHandler, ) @@ -46,6 +47,7 @@ type Handlers struct { ApplicationApi *application_v1.ApplicationController ApplicationPostApi *application_v1.PostController ApplicationCategoryApi *application_v1.CategoryController + ApplicationUserApi *application_v1.UserController } func NewHandler( @@ -53,11 +55,14 @@ func NewHandler( applicationApiController *application_v1.ApplicationController, applicationPostApi *application_v1.PostController, applicationCategoryApi *application_v1.CategoryController, + applicationUserApi *application_v1.UserController, + ) *Handlers { return &Handlers{ ApplicationController: applicationController, ApplicationApi: applicationApiController, ApplicationPostApi: applicationPostApi, ApplicationCategoryApi: applicationCategoryApi, + ApplicationUserApi: applicationUserApi, } } diff --git a/internal/handler/http/request/applications.go b/internal/handler/http/request/applications.go index a9791c6..4caabe1 100644 --- a/internal/handler/http/request/applications.go +++ b/internal/handler/http/request/applications.go @@ -9,3 +9,13 @@ type ApplicationId struct { type ApplicationCreateRequest struct { Name string `json:"name"` } + +type UserLikePost struct { + PostId schema.EntityId `json:"post_id" uri:"post_id"` + ExternalUserId string `json:"external_user_id"` +} + +type UserDislikePost struct { + PostId schema.EntityId `json:"post_id" uri:"post_id"` + ExternalUserId string `json:"external_user_id"` +} diff --git a/internal/migrations/1_setup.sql b/internal/migrations/1_setup.sql index 3783b59..a5a5d7f 100644 --- a/internal/migrations/1_setup.sql +++ b/internal/migrations/1_setup.sql @@ -23,6 +23,22 @@ CREATE TABLE `application_tokens` foreign key (application_id) references applications (id) on delete cascade ); +CREATE TABLE `external_users` +( + id serial NOT NULL, + name varchar(255) DEFAULT NULL, + email varchar(255) DEFAULT NULL, + external_id varchar(255) NOT NULL, + summary varchar(255) DEFAULT NULL, + application_id bigint unsigned NOT NULL, + created_at timestamp DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp DEFAULT CURRENT_TIMESTAMP, + primary key (id), + index (external_id), + foreign key (application_id) references applications (id) on delete cascade +); + + CREATE TABLE `categories` ( id serial NOT NULL, @@ -88,27 +104,29 @@ CREATE TABLE `post_tags` -- user tag scores CREATE TABLE `user_tag_scores` ( - user_id varchar(255) NOT NULL, - tag_id bigint unsigned NOT NULL, - score int NOT NULL, - application_id bigint unsigned NOT NULL, - primary key (user_id, tag_id), + external_user_id bigint unsigned NOT NULL, + tag_id bigint unsigned NOT NULL, + score int NOT NULL, + application_id bigint unsigned NOT NULL, + primary key (external_user_id, tag_id), index (score, application_id), foreign key (tag_id) references tags (id) on delete cascade, - foreign key (application_id) references applications (id) on delete cascade + foreign key (application_id) references applications (id) on delete cascade, + foreign key (external_user_id) references external_users (id) on delete cascade ); -- user likes CREATE TABLE `user_likes` ( - user_id varchar(255) NOT NULL, - post_id bigint unsigned NOT NULL, - type enum ('like', 'dislike') NOT NULL, - application_id bigint unsigned NOT NULL, - primary key (user_id, post_id), + external_user_id bigint unsigned NOT NULL, + post_id bigint unsigned NOT NULL, + type enum ('like', 'dislike') NOT NULL, + application_id bigint unsigned NOT NULL, + primary key (external_user_id, post_id), index (type, application_id), foreign key (post_id) references posts (id) on delete cascade, - foreign key (application_id) references applications (id) on delete cascade + foreign key (application_id) references applications (id) on delete cascade, + foreign key (external_user_id) references external_users (id) on delete cascade ); @@ -122,3 +140,4 @@ DROP TABLE IF EXISTS `tags`; DROP TABLE IF EXISTS `categories`; DROP TABLE IF EXISTS `application_tokens`; DROP TABLE IF EXISTS `applications`; +DROP TABLE IF EXISTS `external_users`; diff --git a/internal/router/api.go b/internal/router/api.go index bd94401..8bce702 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -45,4 +45,7 @@ func (a *Api) InitApplicationApi(r *gin.RouterGroup) { r.POST("/categories", a.h.ApplicationCategoryApi.Save) r.GET("/categories/:category_id", a.h.ApplicationCategoryApi.Get) r.DELETE("/categories/:category_id", a.h.ApplicationCategoryApi.Delete) + + r.POST("/users/_like", a.h.ApplicationUserApi.Like) + r.POST("/users/_dislike", a.h.ApplicationUserApi.Dislike) } diff --git a/internal/schema/user.go b/internal/schema/user.go index 04f0728..e851acb 100644 --- a/internal/schema/user.go +++ b/internal/schema/user.go @@ -30,6 +30,7 @@ type User struct { } type UserId string +type ExternalUserId uint func (u UserId) String() string { return string(u) diff --git a/internal/service/post/posts.go b/internal/service/post/posts.go index 2086e25..5916a0e 100644 --- a/internal/service/post/posts.go +++ b/internal/service/post/posts.go @@ -51,3 +51,20 @@ func (s *Service) DeletePost(c context.Context, post *entity.Post) error { func (s *Service) GetPostById(c context.Context, postId schema.EntityId) (*entity.Post, error) { return s.dao.WithContext(c).Post.Preload(s.dao.Post.Application).Where(s.dao.Post.Id.Eq(postId.Uint())).First() } + +func (s *Service) GetPostTags(c context.Context, postEntity *entity.Post) ([]*entity.Tag, error) { + postTags, err := s.dao.WithContext(c).PostTag.Preload(s.dao.PostTag.Tag). + Where(s.dao.PostTag.PostId.Eq(postEntity.Id.Uint())).Find() + + if err != nil { + return nil, err + } + + var tags []*entity.Tag + + for _, postTag := range postTags { + tags = append(tags, postTag.Tag) + } + + return tags, err +} diff --git a/internal/service/post/tags.go b/internal/service/post/tags.go index 2fabfc0..f40dd63 100644 --- a/internal/service/post/tags.go +++ b/internal/service/post/tags.go @@ -28,7 +28,6 @@ func (s *Service) GetTag(c context.Context, name string, applicationEntity *enti FirstOrCreate() } -// Has Bind func (s *Service) HasBindTag(c context.Context, post *entity.Post, tagName string) (bool, error) { tag, err := s.GetTag(c, tagName, post.Application) if err != nil { @@ -46,12 +45,15 @@ func (s *Service) HasBindTag(c context.Context, post *entity.Post, tagName strin func (s *Service) BindTag(c context.Context, post *entity.Post, tagName string) error { tag, err := s.GetTag(c, tagName, post.Application) - if err != nil { return err } bind, err := s.HasBindTag(c, post, tag.Name) + if err != nil { + return err + } + if !bind { err = s.dao.WithContext(c).PostTag.Create(&entity.PostTag{ PostId: &post.Id, @@ -59,7 +61,7 @@ func (s *Service) BindTag(c context.Context, post *entity.Post, tagName string) }) } - return err + return nil } func (s *Service) MarkAsProcessed(c context.Context, post *entity.Post) error { diff --git a/internal/service/provider.go b/internal/service/provider.go index b3b3bc1..9dd090f 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -8,6 +8,7 @@ import ( "leafdev.top/Ecosystem/recommender/internal/service/jwks" "leafdev.top/Ecosystem/recommender/internal/service/post" "leafdev.top/Ecosystem/recommender/internal/service/stream" + "leafdev.top/Ecosystem/recommender/internal/service/user" "github.com/google/wire" ) @@ -20,6 +21,7 @@ type Service struct { Application *application.Service Post *post.Service Category *category.Service + User *user.Service } var Provider = wire.NewSet( @@ -29,6 +31,7 @@ var Provider = wire.NewSet( application.NewService, post.NewService, category.NewService, + user.NewService, NewService, ) @@ -40,6 +43,7 @@ func NewService( application *application.Service, post *post.Service, category *category.Service, + user *user.Service, ) *Service { return &Service{ logger, @@ -49,5 +53,6 @@ func NewService( application, post, category, + user, } } diff --git a/internal/service/user/external.go b/internal/service/user/external.go new file mode 100644 index 0000000..bb6f004 --- /dev/null +++ b/internal/service/user/external.go @@ -0,0 +1,35 @@ +package user + +import ( + "context" + "leafdev.top/Ecosystem/recommender/internal/entity" +) + +func (s *Service) GetOrCreateExternalUser(c context.Context, externalUserId string, applicationEntity *entity.Application) (*entity.ExternalUser, error) { + //Where(s.dao.UserTagScore.ExternalUserId.Eq(externalUserEntity.Id.Uint())). + count, err := s.dao.WithContext(c).ExternalUser. + Where(s.dao.ExternalUser.ApplicationId.Eq(applicationEntity.Id.Uint())). + Where(s.dao.ExternalUser.ExternalId.Eq(externalUserId)).Count() + if err != nil { + return nil, err + } + + if count > 0 { + eu, err := s.dao.WithContext(c).ExternalUser. + Where(s.dao.ExternalUser.ApplicationId.Eq(applicationEntity.Id.Uint())). + Where(s.dao.ExternalUser.ExternalId.Eq(externalUserId)).First() + if err != nil { + return nil, err + } + return eu, nil + } + + eu := &entity.ExternalUser{ + ApplicationId: applicationEntity.Id, + ExternalId: externalUserId, + } + + err = s.dao.WithContext(c).ExternalUser.Create(eu) + + return eu, err +} diff --git a/internal/service/user/posts.go b/internal/service/user/posts.go new file mode 100644 index 0000000..cf610e9 --- /dev/null +++ b/internal/service/user/posts.go @@ -0,0 +1,148 @@ +package user + +import ( + "context" + "leafdev.top/Ecosystem/recommender/internal/entity" + "leafdev.top/Ecosystem/recommender/internal/schema" + "time" +) + +const TaskProcessing = "user_likes" + +var LockTTL = time.Minute * 10 + +func (s *Service) HasLiked(c context.Context, externalUserEntity *entity.ExternalUser, applicationEntity *entity.Application, postEntity *entity.Post) (bool, error) { + count, err := s.dao.WithContext(c).UserLike.Where(s.dao.UserLike.ExternalUserId.Eq(externalUserEntity.Id.Uint())). + Where(s.dao.UserLike.PostId.Eq(postEntity.Id.Uint())). + Where(s.dao.UserLike.ApplicationId.Eq(applicationEntity.Id.Uint())).Count() + + if err != nil { + return false, err + } + + return count > 0, nil +} + +func (s *Service) UpdateLike(c context.Context, externalUserEntity *entity.ExternalUser, applicationEntity *entity.Application, postEntity *entity.Post, likeType schema.UserLikeType) error { + count, err := s.dao.WithContext(c).UserLike.Where(s.dao.UserLike.ExternalUserId.Eq(externalUserEntity.Id.Uint())). + Where(s.dao.UserLike.PostId.Eq(postEntity.Id.Uint())). + Where(s.dao.UserLike.ApplicationId.Eq(applicationEntity.Id.Uint())).Count() + + if err != nil { + return err + } + + if count > 0 { + // update + _, err = s.dao.WithContext(c).UserLike.Where(s.dao.UserLike.ExternalUserId.Eq(externalUserEntity.Id.Uint())). + Where(s.dao.UserLike.PostId.Eq(postEntity.Id.Uint())). + Where(s.dao.UserLike.ApplicationId.Eq(applicationEntity.Id.Uint())). + Update(s.dao.UserLike.Type, likeType) + return err + } + + var userLike = &entity.UserLike{ + ApplicationId: applicationEntity.Id, + ExternalUserId: externalUserEntity.Id, + PostId: postEntity.Id, + Type: likeType, + } + + err = s.dao.WithContext(c).UserLike.Create(userLike) + + return err +} + +func (s *Service) LikePost(c context.Context, externalUserEntity *entity.ExternalUser, applicationEntity *entity.Application, postEntity *entity.Post) error { + //// 检测是否有 + //var cacheKey = s.redis.Prefix(TaskProcessing) + //lock, err := s.redis.Locker.Obtain(c, cacheKey, LockTTL, nil) + //if err != nil { + // return err + //} + + //defer func(lock *redislock.Lock, ctx context.Context) { + // err := lock.Release(ctx) + // if err != nil { + // s.logger.Sugar.Error(err) + // } + //}(lock, c) + + // get tags + postTags, err := s.postService.GetPostTags(c, postEntity) + if err != nil { + return err + } + + hasLike, err := s.HasLiked(c, externalUserEntity, applicationEntity, postEntity) + if err != nil { + return err + } + + if hasLike { + return nil + } + + err = s.UpdateLike(c, externalUserEntity, applicationEntity, postEntity, schema.UserLikeTypeLike) + if err != nil { + return err + } + + err = s.BindTags(context.Background(), externalUserEntity, applicationEntity, postTags) + if err != nil { + s.logger.Sugar.Error(err) + } + + return nil +} + +func (s *Service) DislikePost(c context.Context, externalUserEntity *entity.ExternalUser, applicationEntity *entity.Application, postEntity *entity.Post) error { + // 检测是否有 + //var cacheKey = s.redis.Prefix(TaskProcessing) + //lock, err := s.redis.Locker.Obtain(c, cacheKey, LockTTL, nil) + //if err != nil { + // return err + //} + // + //defer func(lock *redislock.Lock, ctx context.Context) { + // err := lock.Release(ctx) + // if err != nil { + // s.logger.Sugar.Error(err) + // } + //}(lock, c) + + // get tags + postTags, err := s.postService.GetPostTags(c, postEntity) + if err != nil { + return err + } + + hasLike, err := s.HasLiked(c, externalUserEntity, applicationEntity, postEntity) + if err != nil { + return err + } + + if !hasLike { + return nil + } + + err = s.UpdateLike(c, externalUserEntity, applicationEntity, postEntity, schema.UserLikeTypeDislike) + if err != nil { + return err + } + + _, err = s.dao.WithContext(c).UserLike.Where(s.dao.UserLike.ExternalUserId.Eq(externalUserEntity.Id.Uint())). + Where(s.dao.UserLike.PostId.Eq(postEntity.Id.Uint())). + Where(s.dao.UserLike.ApplicationId.Eq(applicationEntity.Id.Uint())).Count() + + if err != nil { + return err + } + + err = s.RemoveTags(context.Background(), externalUserEntity, applicationEntity, postTags) + if err != nil { + s.logger.Sugar.Error(err) + } + + return nil +} diff --git a/internal/service/user/provider.go b/internal/service/user/provider.go new file mode 100644 index 0000000..5776dc6 --- /dev/null +++ b/internal/service/user/provider.go @@ -0,0 +1,25 @@ +package user + +import ( + "leafdev.top/Ecosystem/recommender/internal/base/logger" + "leafdev.top/Ecosystem/recommender/internal/dao" + "leafdev.top/Ecosystem/recommender/internal/service/post" +) + +type Service struct { + dao *dao.Query + postService *post.Service + logger *logger.Logger +} + +func NewService( + dao *dao.Query, + postService *post.Service, + logger *logger.Logger, +) *Service { + return &Service{ + dao: dao, + postService: postService, + logger: logger, + } +} diff --git a/internal/service/user/tags.go b/internal/service/user/tags.go new file mode 100644 index 0000000..430dd46 --- /dev/null +++ b/internal/service/user/tags.go @@ -0,0 +1,114 @@ +package user + +import ( + "context" + "leafdev.top/Ecosystem/recommender/internal/entity" +) + +func (s *Service) HasBindTag(c context.Context, externalUserEntity *entity.ExternalUser, applicationEntity *entity.Application, tagEntity *entity.Tag) (bool, error) { + count, err := s.dao.WithContext(c).UserTagScore. + Where(s.dao.UserTagScore.ExternalUserId.Eq(externalUserEntity.Id.Uint())). + Where(s.dao.UserTagScore.TagId.Eq(tagEntity.Id.Uint())). + Where(s.dao.UserTagScore.ApplicationId.Eq(applicationEntity.Id.Uint())).Count() + if err != nil { + return false, err + } + + return count > 0, nil +} + +func (s *Service) BindTags(c context.Context, externalUserEntity *entity.ExternalUser, applicationEntity *entity.Application, tags []*entity.Tag) error { + // 检测用户是否绑定 Tag,如果绑定则更新,否则创建 + for _, tag := range tags { + var uts *entity.UserTagScore + + hasBind, err := s.HasBindTag(c, externalUserEntity, applicationEntity, tag) + if err != nil { + return err + } + if hasBind { + // score + 1 + uts, err = s.dao.WithContext(c).UserTagScore. + Where(s.dao.UserTagScore.ExternalUserId.Eq(externalUserEntity.Id.Uint())). + Where(s.dao.UserTagScore.TagId.Eq(tag.Id.Uint())). + Where(s.dao.UserTagScore.ApplicationId.Eq(applicationEntity.Id.Uint())).First() + if err != nil { + return err + } + + uts.Score += 1 + + err = s.dao.WithContext(c).UserTagScore.Save(uts) + if err != nil { + return err + } + + continue + } + + uts = &entity.UserTagScore{ + ApplicationId: applicationEntity.Id, + TagId: tag.Id, + ExternalUserId: externalUserEntity.Id, + Score: 1, + } + + err = s.dao.WithContext(c).UserTagScore.Create(uts) + if err != nil { + return err + } + } + + return nil +} + +func (s *Service) RemoveTags(c context.Context, externalUserEntity *entity.ExternalUser, applicationEntity *entity.Application, tags []*entity.Tag) error { + for _, tag := range tags { + var uts *entity.UserTagScore + + hasBind, err := s.HasBindTag(c, externalUserEntity, applicationEntity, tag) + if err != nil { + return err + } + if hasBind { + // score - 1 + uts, err = s.dao.WithContext(c).UserTagScore. + Where(s.dao.UserTagScore.ExternalUserId.Eq(externalUserEntity.Id.Uint())). + Where(s.dao.UserTagScore.TagId.Eq(tag.Id.Uint())). + Where(s.dao.UserTagScore.ApplicationId.Eq(applicationEntity.Id.Uint())).First() + if err != nil { + return err + } + + //if uts.Score-1 < 0 { + // continue + //} + + uts.Score -= 1 + + _, err = s.dao.WithContext(c).UserTagScore. + Where(s.dao.UserTagScore.ExternalUserId.Eq(externalUserEntity.Id.Uint())). + Where(s.dao.UserTagScore.TagId.Eq(tag.Id.Uint())). + Where(s.dao.UserTagScore.ApplicationId.Eq(applicationEntity.Id.Uint())).Update(s.dao.UserTagScore.Score, uts.Score) + if err != nil { + return err + } + + continue + } else { + uts = &entity.UserTagScore{ + ApplicationId: applicationEntity.Id, + TagId: tag.Id, + ExternalUserId: externalUserEntity.Id, + Score: -1, + } + + err = s.dao.WithContext(c).UserTagScore.Create(uts) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/consts/posts.go b/pkg/consts/posts.go new file mode 100644 index 0000000..037e808 --- /dev/null +++ b/pkg/consts/posts.go @@ -0,0 +1,7 @@ +package consts + +import "errors" + +var ( + ErrAnotherOperationInProgress = errors.New("another operation in progress") +)