This commit is contained in:
parent
367db36c7a
commit
735aad8cc1
@ -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
|
||||
|
73
docs/docs.go
73
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": "搜索商品",
|
||||
|
@ -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": "搜索商品",
|
||||
|
@ -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:
|
||||
|
15
model/buyment_model.go
Normal file
15
model/buyment_model.go
Normal file
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
157
service/recomment_service.go
Normal file
157
service/recomment_service.go
Normal file
@ -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
|
||||
}
|
@ -7,6 +7,7 @@ type Service struct {
|
||||
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user