2025-01-21 14:37:34 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
import (
|
2025-01-21 14:59:15 +08:00
|
|
|
|
"math"
|
2025-01-21 14:37:34 +08:00
|
|
|
|
"sort"
|
|
|
|
|
"strconv"
|
|
|
|
|
"yitao/ecode"
|
|
|
|
|
"yitao/model"
|
2025-01-21 14:59:15 +08:00
|
|
|
|
"yitao/util"
|
2025-01-21 14:37:34 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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))
|
2025-01-21 14:59:15 +08:00
|
|
|
|
// x 为 5天 一个单位
|
2025-01-21 14:37:34 +08:00
|
|
|
|
// 类型推荐程度 = (类型总体热度*0.4 + 用户喜爱程度*0.6) * Wtime
|
|
|
|
|
// 将类型推荐程度进行拟合,得到一个商品符合类型多的商品
|
|
|
|
|
// 然后在对所有商品对于所有用户的浏览量进行排序,采用类型拟合优先,类型完全一致的商品采用浏览量排序
|
|
|
|
|
|
|
|
|
|
// 因为用户会一直刷新列表
|
|
|
|
|
// 在前15名中随机选取5个商品,在15-27随机选取4个商品,在27-30中随机选取1个商品
|
|
|
|
|
// 维持三轮,如果用户三轮都不满意,那么判定用户是来找茬的,建议前端直接跳个鬼图
|
|
|
|
|
// 这样做可以也可以保证热门商品可能被重复推荐,让用户狠狠购买!
|
|
|
|
|
|
2025-01-21 14:59:15 +08:00
|
|
|
|
// 该算法运行过程中计算与数据库负荷较大,用户喜爱程度缓存60秒,商品总体热度缓存600秒,单一商品浏览量缓存60秒,Wtime缓存 600 秒
|
2025-01-21 14:37:34 +08:00
|
|
|
|
// @TODO: 采用布隆过滤器,降低推荐过的商品的概率
|
2025-01-21 14:59:15 +08:00
|
|
|
|
// @TODO[IMPORTANT]: 优化 Wtime 算法!!! IMPORTANT
|
2025-01-21 14:37:34 +08:00
|
|
|
|
|
|
|
|
|
//缓存机制,每次用户的记录都会隔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
|
2025-01-21 14:59:15 +08:00
|
|
|
|
var allItemsWtime = make(map[uint]float64)
|
2025-01-21 14:37:34 +08:00
|
|
|
|
model.DB.Find(&allItems)
|
2025-01-21 14:59:15 +08:00
|
|
|
|
|
|
|
|
|
// 冷启动部分:不知道可不可用,反正先用着
|
|
|
|
|
// 计算 Wtime
|
|
|
|
|
// 1/(1+e^(-x))
|
|
|
|
|
model.RedisRememberRet("all_items_wtime", 600, func() interface{} {
|
|
|
|
|
now := util.GetTime()
|
|
|
|
|
for _, v := range allItems {
|
|
|
|
|
allItemsWtime[v.ID] = 1 / (1 + math.Pow(2.7181, -float64((now-v.CreatedAt.Unix())/86400/5)))
|
|
|
|
|
}
|
|
|
|
|
return allItemsWtime
|
|
|
|
|
}, &allItemsWtime)
|
|
|
|
|
|
2025-01-21 14:37:34 +08:00
|
|
|
|
// 2. 计算商品的类型匹配度
|
|
|
|
|
var itemMatch = make(map[uint]int)
|
|
|
|
|
for _, item := range allItems {
|
|
|
|
|
for _, t := range item.Types {
|
|
|
|
|
itemMatch[item.ID] += allTypes[t]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-21 14:59:15 +08:00
|
|
|
|
for k, v := range itemMatch {
|
|
|
|
|
itemMatch[k] = int(float64(v) * allItemsWtime[k])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 计算冷启动问题
|
|
|
|
|
|
2025-01-21 14:37:34 +08:00
|
|
|
|
// 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 {
|
2025-01-21 14:59:15 +08:00
|
|
|
|
// 在前15名中随机选取5个商品
|
|
|
|
|
for _, i := range util.RandomSample(0, 15, 5) {
|
|
|
|
|
result = append(result, items[i])
|
|
|
|
|
}
|
|
|
|
|
// 在15-27随机选取4个商品
|
|
|
|
|
for _, i := range util.RandomSample(15, 27, 4) {
|
2025-01-21 14:37:34 +08:00
|
|
|
|
result = append(result, items[i])
|
|
|
|
|
}
|
2025-01-21 14:59:15 +08:00
|
|
|
|
// 在27-30中随机选取1个商品
|
|
|
|
|
for _, i := range util.RandomSample(27, 30, 1) {
|
2025-01-21 14:37:34 +08:00
|
|
|
|
result = append(result, items[i])
|
|
|
|
|
}
|
|
|
|
|
} 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
|
|
|
|
|
}
|