新增:历史记录与推荐接口
All checks were successful
Deploy on Tag / build-and-deploy (push) Successful in 38s

This commit is contained in:
wzj 2025-01-21 14:37:34 +08:00
parent 367db36c7a
commit 735aad8cc1
11 changed files with 440 additions and 11 deletions

View File

@ -15,7 +15,9 @@ type ItemController struct {
func (c *ItemController) BeforeActivation(b mvc.BeforeActivation) { func (c *ItemController) BeforeActivation(b mvc.BeforeActivation) {
b.Handle("GET", "/detail/{id:uint}", "Detail") 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", "/list/type/{ttype:int}", "GetListByType")
b.Handle("GET", "/search", "Search") b.Handle("GET", "/search", "Search")
@ -60,12 +62,33 @@ func (c *ItemController) Detail(id uint) mvc.Result {
// @Success 200 {object} map[string]interface{} "{"msg": "历史记录统计成功"}" // @Success 200 {object} map[string]interface{} "{"msg": "历史记录统计成功"}"
// @Failure 400 {object} map[string]interface{} "{"msg": "错误信息","code":0}" // @Failure 400 {object} map[string]interface{} "{"msg": "错误信息","code":0}"
// @Router /api/item/history/{id} [put] // @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) uid := GetUidFromCtx(c.Ctx)
e := c.Service.Item.PutHistory(uid, id) e := c.Service.Item.PutHistory(uid, id)
return e.Response() 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 // @Summary get item list api
// @Description 通过类型获取相关商品列表 // @Description 通过类型获取相关商品列表
// @Tags item api // @Tags item api

View File

@ -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}": { "/api/item/list/type/{type}": {
"get": { "get": {
"description": "通过类型获取相关商品列表", "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": { "/api/item/search": {
"get": { "get": {
"description": "搜索商品", "description": "搜索商品",

View File

@ -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}": { "/api/item/list/type/{type}": {
"get": { "get": {
"description": "通过类型获取相关商品列表", "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": { "/api/item/search": {
"get": { "get": {
"description": "搜索商品", "description": "搜索商品",

View File

@ -360,6 +360,33 @@ paths:
summary: get item api summary: get item api
tags: tags:
- item api - 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}: /api/item/list/type/{type}:
get: get:
consumes: consumes:
@ -457,6 +484,31 @@ paths:
summary: 删除商品 summary: 删除商品
tags: tags:
- item api - 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: /api/item/search:
get: get:
consumes: consumes:

15
model/buyment_model.go Normal file
View 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"
}

View File

@ -5,4 +5,11 @@ type HistoryModel struct {
UserId uint UserId uint
ItemId uint ItemId uint
Count int Count int
User UserModel `gorm:"foreignKey:UserId"`
Item ItemModel `gorm:"foreignKey:ItemId"`
}
func (HistoryModel) TableName() string {
return "history"
} }

View File

@ -21,7 +21,18 @@ func ConnectToDb(dsn string) *gorm.DB {
if err != nil { if err != nil {
panic(err) 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 return DB
} }

View File

@ -2,6 +2,8 @@ package model
import ( import (
"context" "context"
"encoding/json"
"fmt"
"time" "time"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
@ -25,8 +27,22 @@ func RedisRemember(key string, timeout int, funct func() interface{}) string {
ctx := context.Background() ctx := context.Background()
if RDB.Exists(ctx, key).Val() == 0 { if RDB.Exists(ctx, key).Val() == 0 {
data := funct() 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()
} }
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)
}

View File

@ -67,7 +67,7 @@ func (s *ItemService) PutHistory(uid uint, item_id uint) *ecode.Ecode {
h := new(model.HistoryModel) h := new(model.HistoryModel)
// 查看以前共有几条纪录 // 查看以前共有几条纪录
var c int64 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.UserId = uid
h.ItemId = item_id h.ItemId = item_id
h.Count = int(c) + 1 h.Count = int(c) + 1

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

View File

@ -1,12 +1,13 @@
package service package service
type Service struct { type Service struct {
User *UserService User *UserService
Captcha *CaptchaService Captcha *CaptchaService
LoginLog *LoginLogService LoginLog *LoginLogService
Common *CommonService Common *CommonService
Item *ItemService Item *ItemService
File *FileService File *FileService
Recomment *RecommentService
} }
func InitService() *Service { func InitService() *Service {
@ -18,6 +19,7 @@ func InitService() *Service {
s.Common = new(CommonService) s.Common = new(CommonService)
s.Item = new(ItemService) s.Item = new(ItemService)
s.File = new(FileService) s.File = new(FileService)
s.Recomment = new(RecommentService)
return s return s
} }