This commit is contained in:
ivamp 2024-11-10 00:11:00 +08:00
parent a60608bdbc
commit d6fabedb23
27 changed files with 1645 additions and 42 deletions

View File

@ -33,6 +33,7 @@ import (
"leafdev.top/Ecosystem/recommender/internal/service/jwks" "leafdev.top/Ecosystem/recommender/internal/service/jwks"
"leafdev.top/Ecosystem/recommender/internal/service/post" "leafdev.top/Ecosystem/recommender/internal/service/post"
"leafdev.top/Ecosystem/recommender/internal/service/stream" "leafdev.top/Ecosystem/recommender/internal/service/stream"
"leafdev.top/Ecosystem/recommender/internal/service/user"
) )
// Injectors from wire.go: // Injectors from wire.go:
@ -52,7 +53,10 @@ func CreateApp() (*base.Application, error) {
categoryService := category.NewService(query) categoryService := category.NewService(query)
postController := application_v1.NewPostController(authService, applicationService, postService, categoryService) postController := application_v1.NewPostController(authService, applicationService, postService, categoryService)
categoryController := application_v1.NewCategoryController(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) api := router.NewApiRoute(handlers)
swaggerRouter := router.NewSwaggerRoute() swaggerRouter := router.NewSwaggerRoute()
ginLoggerMiddleware := middleware.NewGinLoggerMiddleware(loggerLogger) ginLoggerMiddleware := middleware.NewGinLoggerMiddleware(loggerLogger)
@ -67,8 +71,7 @@ func CreateApp() (*base.Application, error) {
grpcInterceptor := grpc.NewInterceptor(interceptorAuth, interceptorLogger) grpcInterceptor := grpc.NewInterceptor(interceptorAuth, interceptorLogger)
grpcHandlers := grpc.NewHandler(documentService, grpcInterceptor) grpcHandlers := grpc.NewHandler(documentService, grpcInterceptor)
handlerHandler := handler.NewHandler(grpcHandlers, handlers) handlerHandler := handler.NewHandler(grpcHandlers, handlers)
serviceService := service.NewService(loggerLogger, jwksJWKS, streamService, authService, applicationService, postService, categoryService) serviceService := service.NewService(loggerLogger, jwksJWKS, streamService, authService, applicationService, postService, categoryService, userService)
redisRedis := redis.NewRedis(config)
batchBatch := batch.NewBatch(loggerLogger) batchBatch := batch.NewBatch(loggerLogger)
s3S3 := s3.NewS3(config) s3S3 := s3.NewS3(config)
baseApplication := base.NewApplication(config, httpServer, handlerHandler, loggerLogger, serviceService, httpMiddleware, redisRedis, batchBatch, s3S3, db, query) baseApplication := base.NewApplication(config, httpServer, handlerHandler, loggerLogger, serviceService, httpMiddleware, redisRedis, batchBatch, s3S3, db, query)

View File

@ -22,6 +22,7 @@ redis:
port: 6379 port: 6379
password: "" password: ""
db: 0 db: 0
prefix: "recommender_"
jwks: jwks:
url: "" url: ""

View File

@ -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": { "definitions": {
@ -900,6 +1014,17 @@ const docTemplate = `{
} }
} }
}, },
"request.UserLikePost": {
"type": "object",
"properties": {
"external_user_id": {
"type": "string"
},
"post_id": {
"type": "integer"
}
}
},
"response.ResponseBody": { "response.ResponseBody": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -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": { "definitions": {
@ -891,6 +1005,17 @@
} }
} }
}, },
"request.UserLikePost": {
"type": "object",
"properties": {
"external_user_id": {
"type": "string"
},
"post_id": {
"type": "integer"
}
}
},
"response.ResponseBody": { "response.ResponseBody": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -133,6 +133,13 @@ definitions:
- target_id - target_id
- title - title
type: object type: object
request.UserLikePost:
properties:
external_user_id:
type: string
post_id:
type: integer
type: object
response.ResponseBody: response.ResponseBody:
properties: properties:
data: {} data: {}
@ -541,6 +548,72 @@ paths:
summary: 新建资源 summary: 新建资源
tags: tags:
- application_api - 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: securityDefinitions:
ApiKeyAuth: ApiKeyAuth:
in: header in: header

View File

@ -34,6 +34,7 @@ func main() {
entity.ApplicationToken{}, entity.ApplicationToken{},
entity.UserTagScore{}, entity.UserTagScore{},
entity.Category{}, entity.Category{},
entity.ExternalUser{},
) )
// Generate Type Safe API with Dynamic SQL defined on Querier interface for `model.User` and `model.Company` // Generate Type Safe API with Dynamic SQL defined on Querier interface for `model.User` and `model.Company`

View File

@ -53,6 +53,7 @@ type Redis struct {
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
Password string `yaml:"password"` Password string `yaml:"password"`
Prefix string `yaml:"prefix"`
DB int `yaml:"db"` DB int `yaml:"db"`
} }

View File

@ -12,6 +12,7 @@ import (
type Redis struct { type Redis struct {
Client *redis.Client Client *redis.Client
Locker *redislock.Client Locker *redislock.Client
config *conf.Config
} }
func NewRedis(c *conf.Config) *Redis { func NewRedis(c *conf.Config) *Redis {
@ -32,7 +33,12 @@ func NewRedis(c *conf.Config) *Redis {
var r = &Redis{ var r = &Redis{
Client: client, Client: client,
Locker: locker, Locker: locker,
config: c,
} }
return r return r
} }
func (r *Redis) Prefix(key string) string {
return fmt.Sprintf("%s:%s", r.config.Redis.Prefix, key)
}

View File

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

View File

@ -20,6 +20,7 @@ var (
Application *application Application *application
ApplicationToken *applicationToken ApplicationToken *applicationToken
Category *category Category *category
ExternalUser *externalUser
Post *post Post *post
PostTag *postTag PostTag *postTag
Tag *tag Tag *tag
@ -33,6 +34,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
Application = &Q.Application Application = &Q.Application
ApplicationToken = &Q.ApplicationToken ApplicationToken = &Q.ApplicationToken
Category = &Q.Category Category = &Q.Category
ExternalUser = &Q.ExternalUser
Post = &Q.Post Post = &Q.Post
PostTag = &Q.PostTag PostTag = &Q.PostTag
Tag = &Q.Tag Tag = &Q.Tag
@ -47,6 +49,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
Application: newApplication(db, opts...), Application: newApplication(db, opts...),
ApplicationToken: newApplicationToken(db, opts...), ApplicationToken: newApplicationToken(db, opts...),
Category: newCategory(db, opts...), Category: newCategory(db, opts...),
ExternalUser: newExternalUser(db, opts...),
Post: newPost(db, opts...), Post: newPost(db, opts...),
PostTag: newPostTag(db, opts...), PostTag: newPostTag(db, opts...),
Tag: newTag(db, opts...), Tag: newTag(db, opts...),
@ -62,6 +65,7 @@ type Query struct {
Application application Application application
ApplicationToken applicationToken ApplicationToken applicationToken
Category category Category category
ExternalUser externalUser
Post post Post post
PostTag postTag PostTag postTag
Tag tag Tag tag
@ -78,6 +82,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
Application: q.Application.clone(db), Application: q.Application.clone(db),
ApplicationToken: q.ApplicationToken.clone(db), ApplicationToken: q.ApplicationToken.clone(db),
Category: q.Category.clone(db), Category: q.Category.clone(db),
ExternalUser: q.ExternalUser.clone(db),
Post: q.Post.clone(db), Post: q.Post.clone(db),
PostTag: q.PostTag.clone(db), PostTag: q.PostTag.clone(db),
Tag: q.Tag.clone(db), Tag: q.Tag.clone(db),
@ -101,6 +106,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
Application: q.Application.replaceDB(db), Application: q.Application.replaceDB(db),
ApplicationToken: q.ApplicationToken.replaceDB(db), ApplicationToken: q.ApplicationToken.replaceDB(db),
Category: q.Category.replaceDB(db), Category: q.Category.replaceDB(db),
ExternalUser: q.ExternalUser.replaceDB(db),
Post: q.Post.replaceDB(db), Post: q.Post.replaceDB(db),
PostTag: q.PostTag.replaceDB(db), PostTag: q.PostTag.replaceDB(db),
Tag: q.Tag.replaceDB(db), Tag: q.Tag.replaceDB(db),
@ -114,6 +120,7 @@ type queryCtx struct {
Application IApplicationDo Application IApplicationDo
ApplicationToken IApplicationTokenDo ApplicationToken IApplicationTokenDo
Category ICategoryDo Category ICategoryDo
ExternalUser IExternalUserDo
Post IPostDo Post IPostDo
PostTag IPostTagDo PostTag IPostTagDo
Tag ITagDo Tag ITagDo
@ -127,6 +134,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
Application: q.Application.WithContext(ctx), Application: q.Application.WithContext(ctx),
ApplicationToken: q.ApplicationToken.WithContext(ctx), ApplicationToken: q.ApplicationToken.WithContext(ctx),
Category: q.Category.WithContext(ctx), Category: q.Category.WithContext(ctx),
ExternalUser: q.ExternalUser.WithContext(ctx),
Post: q.Post.WithContext(ctx), Post: q.Post.WithContext(ctx),
PostTag: q.PostTag.WithContext(ctx), PostTag: q.PostTag.WithContext(ctx),
Tag: q.Tag.WithContext(ctx), Tag: q.Tag.WithContext(ctx),

View File

@ -27,9 +27,15 @@ func newUserLike(db *gorm.DB, opts ...gen.DOOption) userLike {
tableName := _userLike.userLikeDo.TableName() tableName := _userLike.userLikeDo.TableName()
_userLike.ALL = field.NewAsterisk(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.PostId = field.NewUint(tableName, "post_id")
_userLike.Type = field.NewString(tableName, "type") _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() _userLike.fillFieldMap()
@ -39,10 +45,12 @@ func newUserLike(db *gorm.DB, opts ...gen.DOOption) userLike {
type userLike struct { type userLike struct {
userLikeDo userLikeDo
ALL field.Asterisk ALL field.Asterisk
UserId field.String ExternalUserId field.Uint
PostId field.Uint PostId field.Uint
Type field.String Type field.String
ApplicationId field.Uint
Application userLikeBelongsToApplication
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@ -59,9 +67,10 @@ func (u userLike) As(alias string) *userLike {
func (u *userLike) updateTableName(table string) *userLike { func (u *userLike) updateTableName(table string) *userLike {
u.ALL = field.NewAsterisk(table) 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.PostId = field.NewUint(table, "post_id")
u.Type = field.NewString(table, "type") u.Type = field.NewString(table, "type")
u.ApplicationId = field.NewUint(table, "application_id")
u.fillFieldMap() u.fillFieldMap()
@ -78,10 +87,12 @@ func (u *userLike) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (u *userLike) fillFieldMap() { func (u *userLike) fillFieldMap() {
u.fieldMap = make(map[string]field.Expr, 3) u.fieldMap = make(map[string]field.Expr, 5)
u.fieldMap["user_id"] = u.UserId u.fieldMap["external_user_id"] = u.ExternalUserId
u.fieldMap["post_id"] = u.PostId u.fieldMap["post_id"] = u.PostId
u.fieldMap["type"] = u.Type u.fieldMap["type"] = u.Type
u.fieldMap["application_id"] = u.ApplicationId
} }
func (u userLike) clone(db *gorm.DB) userLike { func (u userLike) clone(db *gorm.DB) userLike {
@ -94,6 +105,77 @@ func (u userLike) replaceDB(db *gorm.DB) userLike {
return u 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 userLikeDo struct{ gen.DO }
type IUserLikeDo interface { type IUserLikeDo interface {

View File

@ -27,9 +27,10 @@ func newUserTagScore(db *gorm.DB, opts ...gen.DOOption) userTagScore {
tableName := _userTagScore.userTagScoreDo.TableName() tableName := _userTagScore.userTagScoreDo.TableName()
_userTagScore.ALL = field.NewAsterisk(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.TagId = field.NewUint(tableName, "tag_id")
_userTagScore.Score = field.NewInt(tableName, "score") _userTagScore.Score = field.NewInt(tableName, "score")
_userTagScore.ApplicationId = field.NewUint(tableName, "application_id")
_userTagScore.Tag = userTagScoreBelongsToTag{ _userTagScore.Tag = userTagScoreBelongsToTag{
db: db.Session(&gorm.Session{}), 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() _userTagScore.fillFieldMap()
return _userTagScore return _userTagScore
@ -49,11 +56,14 @@ func newUserTagScore(db *gorm.DB, opts ...gen.DOOption) userTagScore {
type userTagScore struct { type userTagScore struct {
userTagScoreDo userTagScoreDo
ALL field.Asterisk ALL field.Asterisk
UserId field.String ExternalUserId field.Uint
TagId field.Uint TagId field.Uint
Score field.Int Score field.Int
Tag userTagScoreBelongsToTag ApplicationId field.Uint
Tag userTagScoreBelongsToTag
Application userTagScoreBelongsToApplication
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@ -70,9 +80,10 @@ func (u userTagScore) As(alias string) *userTagScore {
func (u *userTagScore) updateTableName(table string) *userTagScore { func (u *userTagScore) updateTableName(table string) *userTagScore {
u.ALL = field.NewAsterisk(table) 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.TagId = field.NewUint(table, "tag_id")
u.Score = field.NewInt(table, "score") u.Score = field.NewInt(table, "score")
u.ApplicationId = field.NewUint(table, "application_id")
u.fillFieldMap() u.fillFieldMap()
@ -89,10 +100,11 @@ func (u *userTagScore) GetFieldByName(fieldName string) (field.OrderExpr, bool)
} }
func (u *userTagScore) fillFieldMap() { func (u *userTagScore) fillFieldMap() {
u.fieldMap = make(map[string]field.Expr, 4) u.fieldMap = make(map[string]field.Expr, 6)
u.fieldMap["user_id"] = u.UserId u.fieldMap["external_user_id"] = u.ExternalUserId
u.fieldMap["tag_id"] = u.TagId u.fieldMap["tag_id"] = u.TagId
u.fieldMap["score"] = u.Score u.fieldMap["score"] = u.Score
u.fieldMap["application_id"] = u.ApplicationId
} }
@ -181,6 +193,77 @@ func (a userTagScoreBelongsToTagTx) Count() int64 {
return a.tx.Count() 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 userTagScoreDo struct{ gen.DO }
type IUserTagScoreDo interface { type IUserTagScoreDo interface {

View File

@ -11,10 +11,27 @@ func (u *User) TableName() string {
return "users" 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 { type UserLike struct {
UserId schema.UserId `gorm:"primarykey" json:"user_id"` ExternalUserId schema.EntityId `gorm:"primarykey" json:"external_user_id"`
PostId schema.EntityId `gorm:"primarykey" json:"post_id"` PostId schema.EntityId `gorm:"primarykey" json:"post_id"`
Type schema.UserLikeType `json:"type"` Type schema.UserLikeType `json:"type"`
Application *Application
ApplicationId schema.EntityId `json:"application_id"`
} }
func (u *UserLike) TableName() string { func (u *UserLike) TableName() string {
@ -22,10 +39,12 @@ func (u *UserLike) TableName() string {
} }
type UserTagScore struct { type UserTagScore struct {
UserId schema.UserId `gorm:"primarykey" json:"user_id"` ExternalUserId schema.EntityId `gorm:"primarykey" json:"external_user_id"`
TagId schema.EntityId `gorm:"primarykey" json:"tag_id"` TagId schema.EntityId `gorm:"primarykey" json:"tag_id"`
Tag *Tag Tag *Tag
Score int `json:"score"` Score int `json:"score"`
Application *Application
ApplicationId schema.EntityId `json:"application_id"`
} }
func (u *UserTagScore) TableName() string { func (u *UserTagScore) TableName() string {

View File

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

View File

@ -17,6 +17,7 @@ var ProviderSet = wire.NewSet(
application_v1.NewApplicationController, application_v1.NewApplicationController,
application_v1.NewPostController, application_v1.NewPostController,
application_v1.NewCategoryController, application_v1.NewCategoryController,
application_v1.NewUserController,
NewHandler, NewHandler,
) )
@ -46,6 +47,7 @@ type Handlers struct {
ApplicationApi *application_v1.ApplicationController ApplicationApi *application_v1.ApplicationController
ApplicationPostApi *application_v1.PostController ApplicationPostApi *application_v1.PostController
ApplicationCategoryApi *application_v1.CategoryController ApplicationCategoryApi *application_v1.CategoryController
ApplicationUserApi *application_v1.UserController
} }
func NewHandler( func NewHandler(
@ -53,11 +55,14 @@ func NewHandler(
applicationApiController *application_v1.ApplicationController, applicationApiController *application_v1.ApplicationController,
applicationPostApi *application_v1.PostController, applicationPostApi *application_v1.PostController,
applicationCategoryApi *application_v1.CategoryController, applicationCategoryApi *application_v1.CategoryController,
applicationUserApi *application_v1.UserController,
) *Handlers { ) *Handlers {
return &Handlers{ return &Handlers{
ApplicationController: applicationController, ApplicationController: applicationController,
ApplicationApi: applicationApiController, ApplicationApi: applicationApiController,
ApplicationPostApi: applicationPostApi, ApplicationPostApi: applicationPostApi,
ApplicationCategoryApi: applicationCategoryApi, ApplicationCategoryApi: applicationCategoryApi,
ApplicationUserApi: applicationUserApi,
} }
} }

View File

@ -9,3 +9,13 @@ type ApplicationId struct {
type ApplicationCreateRequest struct { type ApplicationCreateRequest struct {
Name string `json:"name"` 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"`
}

View File

@ -23,6 +23,22 @@ CREATE TABLE `application_tokens`
foreign key (application_id) references applications (id) on delete cascade 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` CREATE TABLE `categories`
( (
id serial NOT NULL, id serial NOT NULL,
@ -88,27 +104,29 @@ CREATE TABLE `post_tags`
-- user tag scores -- user tag scores
CREATE TABLE `user_tag_scores` CREATE TABLE `user_tag_scores`
( (
user_id varchar(255) NOT NULL, external_user_id bigint unsigned NOT NULL,
tag_id bigint unsigned NOT NULL, tag_id bigint unsigned NOT NULL,
score int NOT NULL, score int NOT NULL,
application_id bigint unsigned NOT NULL, application_id bigint unsigned NOT NULL,
primary key (user_id, tag_id), primary key (external_user_id, tag_id),
index (score, application_id), index (score, application_id),
foreign key (tag_id) references tags (id) on delete cascade, 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 -- user likes
CREATE TABLE `user_likes` CREATE TABLE `user_likes`
( (
user_id varchar(255) NOT NULL, external_user_id bigint unsigned NOT NULL,
post_id bigint unsigned NOT NULL, post_id bigint unsigned NOT NULL,
type enum ('like', 'dislike') NOT NULL, type enum ('like', 'dislike') NOT NULL,
application_id bigint unsigned NOT NULL, application_id bigint unsigned NOT NULL,
primary key (user_id, post_id), primary key (external_user_id, post_id),
index (type, application_id), index (type, application_id),
foreign key (post_id) references posts (id) on delete cascade, 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 `categories`;
DROP TABLE IF EXISTS `application_tokens`; DROP TABLE IF EXISTS `application_tokens`;
DROP TABLE IF EXISTS `applications`; DROP TABLE IF EXISTS `applications`;
DROP TABLE IF EXISTS `external_users`;

View File

@ -45,4 +45,7 @@ func (a *Api) InitApplicationApi(r *gin.RouterGroup) {
r.POST("/categories", a.h.ApplicationCategoryApi.Save) r.POST("/categories", a.h.ApplicationCategoryApi.Save)
r.GET("/categories/:category_id", a.h.ApplicationCategoryApi.Get) r.GET("/categories/:category_id", a.h.ApplicationCategoryApi.Get)
r.DELETE("/categories/:category_id", a.h.ApplicationCategoryApi.Delete) 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)
} }

View File

@ -30,6 +30,7 @@ type User struct {
} }
type UserId string type UserId string
type ExternalUserId uint
func (u UserId) String() string { func (u UserId) String() string {
return string(u) return string(u)

View File

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

View File

@ -28,7 +28,6 @@ func (s *Service) GetTag(c context.Context, name string, applicationEntity *enti
FirstOrCreate() FirstOrCreate()
} }
// Has Bind
func (s *Service) HasBindTag(c context.Context, post *entity.Post, tagName string) (bool, error) { func (s *Service) HasBindTag(c context.Context, post *entity.Post, tagName string) (bool, error) {
tag, err := s.GetTag(c, tagName, post.Application) tag, err := s.GetTag(c, tagName, post.Application)
if err != nil { 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 { func (s *Service) BindTag(c context.Context, post *entity.Post, tagName string) error {
tag, err := s.GetTag(c, tagName, post.Application) tag, err := s.GetTag(c, tagName, post.Application)
if err != nil { if err != nil {
return err return err
} }
bind, err := s.HasBindTag(c, post, tag.Name) bind, err := s.HasBindTag(c, post, tag.Name)
if err != nil {
return err
}
if !bind { if !bind {
err = s.dao.WithContext(c).PostTag.Create(&entity.PostTag{ err = s.dao.WithContext(c).PostTag.Create(&entity.PostTag{
PostId: &post.Id, 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 { func (s *Service) MarkAsProcessed(c context.Context, post *entity.Post) error {

View File

@ -8,6 +8,7 @@ import (
"leafdev.top/Ecosystem/recommender/internal/service/jwks" "leafdev.top/Ecosystem/recommender/internal/service/jwks"
"leafdev.top/Ecosystem/recommender/internal/service/post" "leafdev.top/Ecosystem/recommender/internal/service/post"
"leafdev.top/Ecosystem/recommender/internal/service/stream" "leafdev.top/Ecosystem/recommender/internal/service/stream"
"leafdev.top/Ecosystem/recommender/internal/service/user"
"github.com/google/wire" "github.com/google/wire"
) )
@ -20,6 +21,7 @@ type Service struct {
Application *application.Service Application *application.Service
Post *post.Service Post *post.Service
Category *category.Service Category *category.Service
User *user.Service
} }
var Provider = wire.NewSet( var Provider = wire.NewSet(
@ -29,6 +31,7 @@ var Provider = wire.NewSet(
application.NewService, application.NewService,
post.NewService, post.NewService,
category.NewService, category.NewService,
user.NewService,
NewService, NewService,
) )
@ -40,6 +43,7 @@ func NewService(
application *application.Service, application *application.Service,
post *post.Service, post *post.Service,
category *category.Service, category *category.Service,
user *user.Service,
) *Service { ) *Service {
return &Service{ return &Service{
logger, logger,
@ -49,5 +53,6 @@ func NewService(
application, application,
post, post,
category, category,
user,
} }
} }

View File

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

View File

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

View File

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

View File

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

7
pkg/consts/posts.go Normal file
View File

@ -0,0 +1,7 @@
package consts
import "errors"
var (
ErrAnotherOperationInProgress = errors.New("another operation in progress")
)