From 735aad8cc1f8c216316cd284c3df0ffa9bca6084 Mon Sep 17 00:00:00 2001 From: wzj Date: Tue, 21 Jan 2025 14:37:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E4=B8=8E=E6=8E=A8=E8=8D=90=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/item_controller.go | 27 +++++- docs/docs.go | 73 ++++++++++++++++ docs/swagger.json | 73 ++++++++++++++++ docs/swagger.yaml | 52 +++++++++++ model/buyment_model.go | 15 ++++ model/history_model.go | 7 ++ model/model.go | 13 ++- model/redis.go | 18 +++- service/item_service.go | 2 +- service/recomment_service.go | 157 ++++++++++++++++++++++++++++++++++ service/service.go | 14 +-- 11 files changed, 440 insertions(+), 11 deletions(-) create mode 100644 model/buyment_model.go create mode 100644 service/recomment_service.go diff --git a/controller/item_controller.go b/controller/item_controller.go index 9222589..33b81e9 100644 --- a/controller/item_controller.go +++ b/controller/item_controller.go @@ -15,7 +15,9 @@ type ItemController struct { func (c *ItemController) BeforeActivation(b mvc.BeforeActivation) { b.Handle("GET", "/detail/{id:uint}", "Detail") - b.Handle("PUT", "/history/{id:uint}", "PutHistory") + b.Handle("PUT", "/history/{id:uint}", "PutHistory", middleware.JwtMiddleware.Serve) + + b.Handle("GET", "/recommend", "GetRecommend", middleware.JwtMiddleware.Serve) b.Handle("GET", "/list/type/{ttype:int}", "GetListByType") b.Handle("GET", "/search", "Search") @@ -60,12 +62,33 @@ func (c *ItemController) Detail(id uint) mvc.Result { // @Success 200 {object} map[string]interface{} "{"msg": "历史记录统计成功"}" // @Failure 400 {object} map[string]interface{} "{"msg": "错误信息","code":0}" // @Router /api/item/history/{id} [put] -func (c *ItemController) DetailHistory(id uint) mvc.Result { +func (c *ItemController) PutHistory(id uint) mvc.Result { uid := GetUidFromCtx(c.Ctx) e := c.Service.Item.PutHistory(uid, id) return e.Response() } +// @Summary get recommend item api +// @Description 获取推荐商品列表(首页使用,每次返回十个) +// @Description 算法模型:用户浏览记录,用户购买记录,商品类型,商品热度 +// @Description 该接口为 BETA 接口,正在测试中,调用请谨慎 +// @Tags item api +// @Accept json +// @Produce json +// @Success 200 {object} []model.ItemModel "{["id":1],["id":2]}" +// @Failure 400 {object} map[string]interface{} "{"msg": "错误信息","code":0}" +// @Router /api/item/recommend [get] +func (c *ItemController) GetRecommend() mvc.Result { + uid := GetUidFromCtx(c.Ctx) + itemHot, e := c.Service.Recomment.GetPersonalHotItems(uid) + if e.Error() { + return e.Response() + } + return mvc.Response{ + Object: itemHot, + } +} + // @Summary get item list api // @Description 通过类型获取相关商品列表 // @Tags item api diff --git a/docs/docs.go b/docs/docs.go index f4a28de..59c910f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -304,6 +304,46 @@ const docTemplate = `{ } } }, + "/api/item/history/{id}": { + "put": { + "description": "上传用户浏览历史记录", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "item api" + ], + "summary": "上传用户浏览历史记录", + "parameters": [ + { + "type": "integer", + "description": "商品ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"msg\": \"历史记录统计成功\"}", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "{\"msg\": \"错误信息\",\"code\":0}", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/item/list/type/{type}": { "get": { "description": "通过类型获取相关商品列表", @@ -447,6 +487,39 @@ const docTemplate = `{ } } }, + "/api/item/recommend": { + "get": { + "description": "获取推荐商品列表(首页使用,每次返回十个)\n算法模型:用户浏览记录,用户购买记录,商品类型,商品热度\n该接口为 BETA 接口,正在测试中,调用请谨慎", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "item api" + ], + "summary": "get recommend item api", + "responses": { + "200": { + "description": "{[\"id\":1],[\"id\":2]}", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ItemModel" + } + } + }, + "400": { + "description": "{\"msg\": \"错误信息\",\"code\":0}", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/item/search": { "get": { "description": "搜索商品", diff --git a/docs/swagger.json b/docs/swagger.json index 9bec146..251294a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -298,6 +298,46 @@ } } }, + "/api/item/history/{id}": { + "put": { + "description": "上传用户浏览历史记录", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "item api" + ], + "summary": "上传用户浏览历史记录", + "parameters": [ + { + "type": "integer", + "description": "商品ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "{\"msg\": \"历史记录统计成功\"}", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "{\"msg\": \"错误信息\",\"code\":0}", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/item/list/type/{type}": { "get": { "description": "通过类型获取相关商品列表", @@ -441,6 +481,39 @@ } } }, + "/api/item/recommend": { + "get": { + "description": "获取推荐商品列表(首页使用,每次返回十个)\n算法模型:用户浏览记录,用户购买记录,商品类型,商品热度\n该接口为 BETA 接口,正在测试中,调用请谨慎", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "item api" + ], + "summary": "get recommend item api", + "responses": { + "200": { + "description": "{[\"id\":1],[\"id\":2]}", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ItemModel" + } + } + }, + "400": { + "description": "{\"msg\": \"错误信息\",\"code\":0}", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/api/item/search": { "get": { "description": "搜索商品", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index dff7760..266c7e6 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -360,6 +360,33 @@ paths: summary: get item api tags: - item api + /api/item/history/{id}: + put: + consumes: + - application/json + description: 上传用户浏览历史记录 + parameters: + - description: 商品ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: '{"msg": "历史记录统计成功"}' + schema: + additionalProperties: true + type: object + "400": + description: '{"msg": "错误信息","code":0}' + schema: + additionalProperties: true + type: object + summary: 上传用户浏览历史记录 + tags: + - item api /api/item/list/type/{type}: get: consumes: @@ -457,6 +484,31 @@ paths: summary: 删除商品 tags: - item api + /api/item/recommend: + get: + consumes: + - application/json + description: |- + 获取推荐商品列表(首页使用,每次返回十个) + 算法模型:用户浏览记录,用户购买记录,商品类型,商品热度 + 该接口为 BETA 接口,正在测试中,调用请谨慎 + produces: + - application/json + responses: + "200": + description: '{["id":1],["id":2]}' + schema: + items: + $ref: '#/definitions/model.ItemModel' + type: array + "400": + description: '{"msg": "错误信息","code":0}' + schema: + additionalProperties: true + type: object + summary: get recommend item api + tags: + - item api /api/item/search: get: consumes: diff --git a/model/buyment_model.go b/model/buyment_model.go new file mode 100644 index 0000000..f40fec6 --- /dev/null +++ b/model/buyment_model.go @@ -0,0 +1,15 @@ +package model + +type BuymentModel struct { + BaseModel + ItemId uint + UserId uint + Price int + + User UserModel `gorm:"foreignKey:UserId"` + Item ItemModel `gorm:"foreignKey:ItemId"` +} + +func (BuymentModel) TableName() string { + return "buyment" +} diff --git a/model/history_model.go b/model/history_model.go index 530d8fa..a4b0988 100644 --- a/model/history_model.go +++ b/model/history_model.go @@ -5,4 +5,11 @@ type HistoryModel struct { UserId uint ItemId uint Count int + + User UserModel `gorm:"foreignKey:UserId"` + Item ItemModel `gorm:"foreignKey:ItemId"` +} + +func (HistoryModel) TableName() string { + return "history" } diff --git a/model/model.go b/model/model.go index 4d5d84a..95b688c 100644 --- a/model/model.go +++ b/model/model.go @@ -21,7 +21,18 @@ func ConnectToDb(dsn string) *gorm.DB { if err != nil { panic(err) } - err = DB.AutoMigrate(&CaptchaModel{}, &UserModel{}, &ItemModel{}, &LoginLogModel{}, &FileModel{}, &TypeModel{}) + err = DB.AutoMigrate(&CaptchaModel{}, + &UserModel{}, + &ItemModel{}, + &LoginLogModel{}, + &FileModel{}, + &TypeModel{}, + &HistoryModel{}, + &BuymentModel{}, + ) + if err != nil { + panic(err) + } return DB } diff --git a/model/redis.go b/model/redis.go index 17fdea4..b537e7c 100644 --- a/model/redis.go +++ b/model/redis.go @@ -2,6 +2,8 @@ package model import ( "context" + "encoding/json" + "fmt" "time" "github.com/redis/go-redis/v9" @@ -25,8 +27,22 @@ func RedisRemember(key string, timeout int, funct func() interface{}) string { ctx := context.Background() if RDB.Exists(ctx, key).Val() == 0 { data := funct() - RDB.Set(ctx, key, data, time.Duration(timeout)*time.Second) + b, _ := json.Marshal(data) + s := RDB.Set(ctx, key, string(b), time.Duration(timeout)*time.Second) + if s.Err() != nil { + fmt.Printf("s.Err(): %v\n", s.Err()) + } return RDB.Get(ctx, key).Val() } return RDB.Get(ctx, key).Val() } + +func RedisForget(key string) { + ctx := context.Background() + RDB.Del(ctx, key) +} + +func RedisRememberRet(key string, timeout int, funct func() interface{}, ret interface{}) { + s := RedisRemember(key, timeout, funct) + json.Unmarshal([]byte(s), ret) +} diff --git a/service/item_service.go b/service/item_service.go index c4e944e..aa37e23 100644 --- a/service/item_service.go +++ b/service/item_service.go @@ -67,7 +67,7 @@ func (s *ItemService) PutHistory(uid uint, item_id uint) *ecode.Ecode { h := new(model.HistoryModel) // 查看以前共有几条纪录 var c int64 - model.DB.Model(h).Where("uid=? and item_id=?", uid, item_id).Count(&c) + model.DB.Model(h).Where("user_id=? and item_id=?", uid, item_id).Count(&c) h.UserId = uid h.ItemId = item_id h.Count = int(c) + 1 diff --git a/service/recomment_service.go b/service/recomment_service.go new file mode 100644 index 0000000..8d5ac4b --- /dev/null +++ b/service/recomment_service.go @@ -0,0 +1,157 @@ +package service + +import ( + "sort" + "strconv" + "yitao/ecode" + "yitao/model" +) + +type RecommentService struct{} + +// 推荐算法模块 +/*********************************************************************************/ + +// 获取用户个人推荐商品 +func (s *RecommentService) GetPersonalHotItems(uid uint) (items []model.ItemModel, e *ecode.Ecode) { + e = ecode.OK() + + // 在算法中处理的都是“类型”,而最后返回的是“商品” + + // 类型总体热度 = 该类商品在过去30天的浏览量*0.5 + 该类商品的总被购买量*0.5 + // 用户喜爱程度 = 用户对该类商品的浏览量*0.4 + 用户对该类商品的购买量*0.6 + // 时间指数:为解决商品冷启动问题:Wtime = 1/(1+e^(-x)) + // 类型推荐程度 = (类型总体热度*0.4 + 用户喜爱程度*0.6) * Wtime + // 将类型推荐程度进行拟合,得到一个商品符合类型多的商品 + // 然后在对所有商品对于所有用户的浏览量进行排序,采用类型拟合优先,类型完全一致的商品采用浏览量排序 + + // 因为用户会一直刷新列表 + // 在前15名中随机选取5个商品,在15-27随机选取4个商品,在27-30中随机选取1个商品 + // 维持三轮,如果用户三轮都不满意,那么判定用户是来找茬的,建议前端直接跳个鬼图 + // 这样做可以也可以保证热门商品可能被重复推荐,让用户狠狠购买! + + // 该算法运行过程中计算与数据库负荷较大,用户喜爱程度缓存60秒,商品总体热度缓存600秒,单一商品浏览量缓存60秒 + // @TODO: 采用布隆过滤器,降低推荐过的商品的概率 + + //缓存机制,每次用户的记录都会隔60秒更新 + // userHotTypes := model.RedisRemember(strconv.FormatUint(uint64(uid), 10)+"_hot_types", 60, func() interface{} { + // return s.LoadUserHotTypes(uid) + // }) + // allHotTypes := model.RedisRemember("all_hot_types", 600, func() interface{} { + // return s.LoadAllHotTypes() + // }) + + userHotTypes := new(map[int]int) + model.RedisRememberRet(strconv.FormatUint(uint64(uid), 10)+"_hot_types", 60, func() interface{} { + return s.LoadUserHotTypes(uid) + }, &userHotTypes) + allHotTypes := new(map[int]int) + model.RedisRememberRet("all_hot_types", 600, func() interface{} { + return s.LoadAllHotTypes() + }, &allHotTypes) + + // 整合 + allTypes := make(map[int]int) + for k, v := range *userHotTypes { + allTypes[k] += v + } + for k, v := range *allHotTypes { + allTypes[k] += v + } + + // 在所有商品中匹配符合类型多的 + // 1. 获取所有商品 + var allItems []model.ItemModel + model.DB.Find(&allItems) + // 2. 计算商品的类型匹配度 + var itemMatch = make(map[uint]int) + for _, item := range allItems { + for _, t := range item.Types { + itemMatch[item.ID] += allTypes[t] + } + } + + // 3. 对商品进行排序 + type kv struct { + Key uint + Value int + } + + var sortedItems []kv + for k, v := range itemMatch { + sortedItems = append(sortedItems, kv{k, v}) + } + + sort.Slice(sortedItems, func(i, j int) bool { + return sortedItems[i].Value > sortedItems[j].Value + }) + + // 4. 根据排序结果获取商品 + for _, kv := range sortedItems { + var item model.ItemModel + model.DB.First(&item, kv.Key) + items = append(items, item) + } + + // 5. 随机选取商品 + var result []model.ItemModel + if len(items) > 30 { + for i := 0; i < 5; i++ { + result = append(result, items[i]) + } + for i := 15; i < 27; i++ { + result = append(result, items[i]) + } + result = append(result, items[29]) + } else { + result = items + } + + return result, e +} + +func (s *RecommentService) LoadUserHotTypes(uid uint) map[int]int { + // 1. 获取用户的历史记录 + var history []model.HistoryModel + model.DB.Preload("Item").Where("user_id=?", uid).Find(&history) + // 提取对每种商品的类型 + var allTypes = make(map[int]int) + for _, h := range history { + for _, t := range h.Item.Types { + allTypes[t] += 4 + } + } + + // 2. 获取用户的购买记录 + var buyments []model.BuymentModel + model.DB.Preload("Item").Preload("User").Where("user_id=?", uid).Find(&buyments) + for _, b := range buyments { + for _, t := range b.Item.Types { + allTypes[t] += 6 + } + } + return allTypes +} + +func (s *RecommentService) LoadAllHotTypes() map[int]int { + // 获取全部的商品浏览记录 + var history []model.HistoryModel + model.DB.Preload("Item").Find(&history) + // 提取对每种商品的类型 + var allTypes = make(map[int]int) + for _, h := range history { + for _, t := range h.Item.Types { + allTypes[t] += 5 + } + } + + // 获取全部的商品购买记录 + var buyments []model.BuymentModel + model.DB.Preload("Item").Preload("User").Find(&buyments) + for _, b := range buyments { + for _, t := range b.Item.Types { + allTypes[t] += 5 + } + } + return allTypes +} diff --git a/service/service.go b/service/service.go index eb123e1..4dd250a 100644 --- a/service/service.go +++ b/service/service.go @@ -1,12 +1,13 @@ package service type Service struct { - User *UserService - Captcha *CaptchaService - LoginLog *LoginLogService - Common *CommonService - Item *ItemService - File *FileService + User *UserService + Captcha *CaptchaService + LoginLog *LoginLogService + Common *CommonService + Item *ItemService + File *FileService + Recomment *RecommentService } func InitService() *Service { @@ -18,6 +19,7 @@ func InitService() *Service { s.Common = new(CommonService) s.Item = new(ItemService) s.File = new(FileService) + s.Recomment = new(RecommentService) return s }