0%

golang推荐系统的代码实现

1. 推荐系统

一个完整的推荐系统通常包含以下步骤:

  1. 数据源准备
  2. 召回 (Recall): 从海量物品库中快速筛选出可能与用户相关的候选项。
  3. 过滤 (Filtering)
  4. 粗排 (Pre-ranking):使用简单模型快速过滤掉大量不相关的物品,保留少量物品进入精排。
  5. 精排 (Fine-ranking): 使用更复杂的模型(例如机器学习模型)精确预测用户对物品的偏好,并进行排序。
  6. 重排序 (Re-ranking): 非必选阶段,增加推荐结果的多样性和新颖性,改善用户体验,避免推荐同质化内容。
  7. 存储与反馈

2. 伪代码

2.1 数据源 cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package localcache

import (
"context"
"lw/internal/svc"
"time"

"github.com/robfig/cron/v3"

"github.com/zeromicro/go-zero/core/logx"
)

type FeedLocalCache struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}

func NewFeedLocalCache(ctx context.Context, svcCtx *svc.ServiceContext) *FeedLocalCache {
return &FeedLocalCache{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}

func (l *FeedLocalCache) Init() {
l.loadVideoTemplate()
l.calcHotVideoTemplate()
c := cron.New(cron.WithSeconds())
c.AddFunc("@every 10m", func() { l.loadVideoTemplate() }) // 每10分钟更新一次
c.AddFunc("@every 30m", func() { l.calcHotVideoTemplate() }) // 每30分钟更新一次
c.Start()
}

func (l *FeedLocalCache) TimeTrack(ctx context.Context, start time.Time, name string) {
elapsed := time.Since(start)
layout := "2006-01-02 15:04:05"
l.Infof("[%s] startTime: %s, endTime: %s, cost: %s", name, start.Format(layout), time.Now().Format(layout), elapsed)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package localcache

import (
"lw/dao/model"
"lw/enums"
"lw/internal/feeds/recommend/common"
"slices"
"sync"
"time"

"github.com/samber/lo"
)

const (
FEED_LIMIT = 20000
NEW_VIDEO_DAYS = 60
HOT_VIDEO_DAYS = 3
)

var (
mu sync.RWMutex // 读写锁
// 模板池数据
cachedAllVideoMap map[string]common.FeedVideo // 所有模板
cachedNewVideoMap map[string]common.FeedVideo // 最新模板
cachedHotCalcVideoMap map[string]common.FeedVideo // 热门模板, 算出来的
)

func (l *FeedLocalCache) loadVideoTemplate() {
start := time.Now()
l.Infof("[loadVideoTemplate] start, time: %v", start.Format("2006-01-02 15:04:05"))
// 获取所有模板
var allVideoTemplates []model.ConfigsVideo
query := l.svcCtx.DB.Model(&model.ConfigsVideo{}).Where("status = 1")
err := query.Find(&allVideoTemplates).Limit(FEED_LIMIT).Error
if err != nil {
l.Errorf("loadVideoTemplate error: %v", err)
return
}

// 获取标签
var sexTags []*model.VideoTag
l.svcCtx.DB.Model(&model.VideoTag{}).Where("tag_type = ?", "sex").Find(&sexTags).Limit(FEED_LIMIT)
generateType2SexMap := make(map[string]string)
for _, sexTag := range sexTags {
generateType2SexMap[sexTag.GenerateType] = sexTag.TagName
}

// 所有模板
allVideoTemplatesMap := make(map[string]common.FeedVideo)
for _, videoTemplate := range allVideoTemplates {
feedTags := make(map[string]bool)
if sexTag != "" {
feedTags[sexTag] = true
}
allVideoTemplatesMap[videoTemplate.GenerateType] = common.FeedVideo{
ConfigsVideo: videoTemplate,
FeedTags: feedTags,
}
}

newVideoTemplatesMap := make(map[string]common.FeedVideo)
for _, videoTemplate := range allVideoTemplatesMap {
if videoTemplate.PublishTime.AddDate(0, 0, NEW_VIDEO_DAYS).After(time.Now()) {
newVideoTemplatesMap[videoTemplate.GenerateType] = videoTemplate
}
}

mu.Lock()
cachedAllVideoMap = allVideoTemplatesMap
cachedNewVideoMap = newVideoTemplatesMap
mu.Unlock()

l.Infof("[loadVideoTemplate] finish, cost: %v, allVideoTemplatesMap: %v, hotVideoTemplatesMap: %v, newVideoTemplatesMap: %v, sex3Videoes20: %v, sex4Videoes20: %v",
time.Since(start),
len(cachedAllVideoMap),
len(cachedNewVideoMap),
)
}

func GetAllVideoCache() []common.FeedVideo {
mu.RLock()
defer mu.RUnlock()
allVideoMap := make([]common.FeedVideo, 0, len(cachedAllVideoMap))
for _, video := range cachedAllVideoMap {
allVideoMap = append(allVideoMap, video)
}
return allVideoMap
}

2.2 ctx 信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
type VideoRecommendCtx struct {
Ctx context.Context
SvcCtx *svc.ServiceContext
DebugInfo *DebugInfo

// 控制逻辑
FilteredVideos map[string]bool // 过滤
FeedReq types.FeedsVideoListReq

// 用户数据
User *model.User
UserBehaviorCount map[string]int64
UserABTestGroup map[string]int32
UserExt model.UserExt
}

func NewVideoRecommendCtx(ctx context.Context, svcCtx *svc.ServiceContext, user *model.User, opts ...Option) *VideoRecommendCtx {
rctx := &VideoRecommendCtx{
Ctx: ctx,
SvcCtx: svcCtx,
DebugInfo: &DebugInfo{Recaller: make(map[string]any), Filters: make(map[string]any), Ranker: make(map[string]any), Result: make(map[string]any)},
FilteredVideos: make(map[string]bool),
User: user,
UserBehaviorCount: make(map[string]int64),
UserABTestGroup: make(map[string]int32),
}
for _, opt := range opts {
opt(rctx)
}
return rctx
}

func (ctx *VideoRecommendCtx) GetDebugInfo() *DebugInfo {
return ctx.DebugInfo
}

type Option func(*VideoRecommendCtx)

// 用户扩展信息,等级设置信息
func WithUserExt(userExt model.UserExt) Option {
return func(rctx *VideoRecommendCtx) {
rctx.UserExt = userExt
}
}

// 用户AB测试组
func WithUserABTestGroup(userABTestGroup map[string]int32) Option {
return func(rctx *VideoRecommendCtx) {
rctx.UserABTestGroup = userABTestGroup
}
}

// 用户行为数据,用来判断等级
func WithUserBehaviorCount(userBehaviorCount map[string]int64) Option {
return func(rctx *VideoRecommendCtx) {
rctx.UserBehaviorCount = userBehaviorCount
}
}

// 需要过滤的
func WithFilteredVideos(filteredVideos []string) Option {
return func(rctx *VideoRecommendCtx) {
for _, video := range filteredVideos {
rctx.FilteredVideos[video] = true
}
}
}

// feed请求数据
func WithFeedReq(req types.FeedsVideoListReq) Option {
return func(rctx *VideoRecommendCtx) {
rctx.FeedReq = req
}
}

func TransformGenerateTypes(videoData []FeedVideo) []string {
generateTypes := make([]string, 0, len(videoData))
for _, video := range videoData {
generateTypes = append(generateTypes, video.GenerateType)
}
return generateTypes
}

func TransformHotScore(videoData []FeedVideo) map[string]float64 {
hotScores := make(map[string]float64, len(videoData))
for _, video := range videoData {
hotScores[video.GenerateType] = video.HotScore
}
return hotScores
}

type DebugInfo struct {
Recaller map[string]any
Filters map[string]any
Ranker map[string]any
Result map[string]any
}

func (d *DebugInfo) AddRecaller(name string, info map[string]any) {
if d.Recaller == nil {
d.Recaller = make(map[string]any)
}
d.Recaller[name] = info
}

func (d *DebugInfo) AddFilter(name string, info map[string]any) {
if d.Filters == nil {
d.Filters = make(map[string]any)
}
d.Filters[name] = info
}

func (d *DebugInfo) AddRanker(name string, info map[string]any) {
if d.Ranker == nil {
d.Ranker = make(map[string]any)
}
d.Ranker[name] = info
}

func (d *DebugInfo) AddResult(info map[string]any) {
if d.Result == nil {
d.Result = make(map[string]any)
}
d.Result = info
}

2.3 过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package filter

import (
"fmt"
"lw/configs/cache"
"lw/enums"
"lw/internal/feeds/localcache"
"lw/internal/feeds/recommend/common"
"time"
)

type VideoFilter interface {
Filter(rctx *common.VideoRecommendCtx, videoData []common.FeedVideo) ([]common.FeedVideo, []string)
Name() string
}

// 通用过滤器: 不同模板池重复, videoType 过滤
type VideoFilterCommon struct {
}

func (f *VideoFilterCommon) Filter(rctx *common.VideoRecommendCtx, videoData []common.FeedVideo) ([]common.FeedVideo, []string) {
var okVideos, filteredVideos []common.FeedVideo
for _, video := range videoData {
if _, ok := rctx.FilteredVideos[video.GenerateType]; ok {
filteredVideos = append(filteredVideos, video)
continue
}
if rctx.FeedReq.VideoType != "" && rctx.FeedReq.VideoType != video.VideoType {
filteredVideos = append(filteredVideos, video)
continue
}
okVideos = append(okVideos, video)
}
return okVideos, common.TransformGenerateTypes(filteredVideos)
}

func (f *VideoFilterCommon) Name() string {
return "filter_common"
}

// 记录过滤器: 过滤推荐列表重复
type VideoFilterRecord struct {
}

func (f *VideoFilterRecord) Filter(rctx *common.VideoRecommendCtx, videoData []common.FeedVideo) ([]common.FeedVideo, []string) {
if rctx.FeedReq.RefreshMethod == string(common.VideoRefreshMethodFirstLaunch) {
return videoData, nil
}

key := common.GetRecommendListKey(common.FeedScene(rctx.FeedReq.FeedScene), rctx.FeedReq.VideoType, rctx.User.ID)
results, err := rctx.SvcCtx.Redis.ZRevRangeWithScores(key, 0, common.RECOMMEND_LIST_RECORD_COUNT-1)
if err != nil {
return videoData, nil
}
recordMap := make(map[string]bool)
for _, result := range results {
recordMap[result.Key] = true
}
var okVideos, filteredVideos []common.FeedVideo
for _, video := range videoData {
if _, ok := recordMap[video.GenerateType]; ok {
filteredVideos = append(filteredVideos, video)
} else {
okVideos = append(okVideos, video)
}
}
return okVideos, common.TransformGenerateTypes(filteredVideos)
}

func (f *VideoFilterRecord) Name() string {
return "filter_record"
}

2.4 排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package ranker

import (
"lw/internal/feeds/recommend/common"
"lw/utils"
"sort"
)

type VideoRanker interface {
Rank(rctx *common.VideoRecommendCtx, videoData []common.FeedVideo) []common.FeedVideo
Name() string
}

type VideoRankerBase struct {
}

func (r *VideoRankerBase) Rank(rctx *common.VideoRecommendCtx, videoData []common.FeedVideo) []common.FeedVideo {
return videoData
}

func (r *VideoRankerBase) Name() string {
return "rank_base"
}

type VideoRankerHotHighScore struct {
}

func (r *VideoRankerHotHighScore) Rank(rctx *common.VideoRecommendCtx, videoData []common.FeedVideo) []common.FeedVideo {
sort.Slice(videoData, func(i, j int) bool {
return videoData[i].HotScore > videoData[j].HotScore
})
utils.ShuffleFirstN(videoData, 5)
return videoData
}

func (r *VideoRankerHotHighScore) Name() string {
return "rank_hot_high_score"
}

2.5 召回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package recaller

import (
"lw/internal/feeds/localcache"
"lw/internal/feeds/recommend/common"
)

type VideoRecaller interface {
Recall(rctx *common.VideoRecommendCtx) ([]common.FeedVideo, error)
Name() string
}

type HotVideoRecaller struct {
}

func NewHotVideoRecaller() *HotVideoRecaller {
return &HotVideoRecaller{}
}

func (r *HotVideoRecaller) Recall(rctx *common.VideoRecommendCtx) ([]common.FeedVideo, error) {
return localcache.GetHotCalcVideoCache(), nil
}

func (r *HotVideoRecaller) Name() string {
return "recall_hot_video"
}

2.6 策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package recommender

import (
"errors"
"lw/internal/feeds/recommend/common"
"lw/internal/feeds/recommend/filter"
"lw/internal/feeds/recommend/ranker"
"lw/internal/feeds/recommend/recaller"
"time"
)

type VideoRecommender struct {
recaller recaller.VideoRecaller
filter []filter.VideoFilter
ranker ranker.VideoRanker
}

func (r *VideoRecommender) Recommend(rctx *common.VideoRecommendCtx, limit int) (data []common.FeedVideo, err error) {
startTime := time.Now()
if limit <= 0 {
return nil, nil
}
if r.recaller == nil {
return nil, errors.New("recaller is nil")
}
if rctx.DebugInfo == nil {
rctx.DebugInfo = &common.DebugInfo{}
}

videoData, err := r.recaller.Recall(rctx)
if err != nil {
return nil, err
}
rctx.DebugInfo.AddRecaller(r.recaller.Name(), map[string]any{
"recall_count": len(videoData),
})

for _, filter := range r.filter {
var filtered []string
beforeCount := len(videoData)
videoData, filtered = filter.Filter(rctx, videoData)
values := map[string]any{
"before_count": beforeCount,
"filtered_count": len(filtered),
}
rctx.DebugInfo.AddFilter(filter.Name(), values)
}

if r.ranker != nil {
videoData = r.ranker.Rank(rctx, videoData)
rctx.DebugInfo.AddRanker(r.ranker.Name(), map[string]any{
"sorted_data": len(videoData),
})
}

if len(videoData) > limit {
videoData = videoData[:limit]
}
rctx.DebugInfo.AddResult(map[string]any{
"limit_count": limit,
"cost_time": time.Since(startTime).String(),
"ok_count": len(videoData),
"ok_data": common.TransformGenerateTypes(videoData),
})
return videoData, nil
}


// 新
func NewNewVideoRecommender() *VideoRecommender {
return &VideoRecommender{
recaller: recaller.NewNewVideoRecaller(),
filter: []filter.VideoFilter{
&filter.VideoFilterCommon{},
&filter.VideoFilterSex{},
&filter.VideoFilterRecord{},
&filter.VideoFilterAbtest{},
},
ranker: &ranker.VideoRankerMasterTemplate{},
}
}

// 全部
func NewAllVideoRecommender() *VideoRecommender {
return &VideoRecommender{
recaller: recaller.NewAllVideoRecaller(),
filter: []filter.VideoFilter{
&filter.VideoFilterCommon{},
&filter.VideoFilterSex{},
&filter.VideoFilterRecord{},
&filter.VideoFilterAbtest{},
},
ranker: &ranker.VideoRankerMasterTemplate{},
}
}

// ---- 仅用于计算能看到的数量 ----
func NewAllVideoWithoutRecordRecommender() *VideoRecommender {
return &VideoRecommender{
recaller: recaller.NewAllVideoRecaller(),
filter: []filter.VideoFilter{
&filter.VideoFilterCommon{},
&filter.VideoFilterSex{},
&filter.VideoFilterAbtest{},
},
ranker: nil,
}
}

2.7 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
func (l *FeedsLogic) GetVideoList(ctx context.Context, req types.FeedsVideoListReq) (data []common.FeedVideo, debugInfos map[string]any, err error) {
startTime := time.Now()
userId := middleware.GetAccountId(l.ctx)
if userId <= 0 {
return nil, nil, errors.New("user not found")
}
l.Infow("GetVideoList start", logx.Field("user_id", userId), logx.Field("req", utils.ToJsonString(req)))

// 获取用户相关数据
var user *model.User
if err = l.svcCtx.DB.Model(&model.User{}).Where("id = ?", userId).First(&user).Error; err != nil {
l.Errorf("GetVideoList get user error: %v", err)
return
}
var userBehaviorStats []model.UserBehaviorStat
l.svcCtx.DB.Model(&model.UserBehaviorStat{}).Where("user_id = ?", userId).Find(&userBehaviorStats)
userBehaviorCount := make(map[string]int64)
for _, v := range userBehaviorStats {
userBehaviorCount[v.BehaviorType] = v.BehaviorCount
}

userABTestGroups := make(map[string]int32)
abtestId2Video, _ := utils.GetUserABTestGroup(l.ctx, l.svcCtx, userId, cache.AbtestId2Video)
userABTestGroups[cache.AbtestId2Video] = abtestId2Video
var userExt model.UserExt
l.svcCtx.DB.Where("user_id = ?", userId).First(&userExt)
if userExt.VideoHideList != "" { // 过滤隐藏列表
videoHideList := []string{}
_ = json.Unmarshal([]byte(userExt.VideoHideList), &videoHideList)
filteredVideos = append(filteredVideos, videoHideList...)
}
getUserEndTime := time.Now()

// 先处理一下用户历史推荐记录
_, userSeeData := l.execRecommend(recommender.NewAllVideoWithoutRecordRecommender(), user, userBehaviorCount, userABTestGroups, userExt, req, []string{}, math.MaxInt64)
err = l.prepareRecommendList(userId, req, len(userSeeData)-len(filteredVideos))
if err != nil {
l.Errorf("GetVideoList prepareRecommendList: %v", err)
}
prepareEndTime := time.Now()
l.Infow("GetVideoList prepare recommend",
logx.Field("user_id", userId),
logx.Field("userBehaviorCount", utils.ToJsonString(userBehaviorCount)),
logx.Field("userABTestGroups", utils.ToJsonString(userABTestGroups)),
logx.Field("userExt", utils.ToJsonString(userExt)),
logx.Field("userSeeData", len(userSeeData)),
logx.Field("filteredVideos", filteredVideos),
)
// 开始依次获取各类模板
lowHotctx, lowHotVideos := l.execRecommend(recommender.NewLowHotVideoRecommender(), user, userBehaviorCount, userABTestGroups, userExt, req, filteredVideos, baseCount)
filteredVideos = append(filteredVideos, common.TransformGenerateTypes(lowHotVideos)...)

newctx, newVideos := l.execRecommend(recommender.NewNewVideoRecommender(), user, userBehaviorCount, userABTestGroups, userExt, req, filteredVideos, baseCount)
filteredVideos = append(filteredVideos, common.TransformGenerateTypes(newVideos)...)

highHotctx, highHotVideos := l.execRecommend(recommender.NewHighHotVideoRecommender(), user, userBehaviorCount, userABTestGroups, userExt, req, filteredVideos, baseCount)
filteredVideos = append(filteredVideos, common.TransformGenerateTypes(highHotVideos)...)

// 计算剩余需要的数量
otherctx, otherData := l.execRecommend(recommender.NewAllVideoRecommender(), user, userBehaviorCount, userABTestGroups, userExt, req, filteredVideos, int(req.VideoCount)-len(lowHotVideos)-len(newVideos)-len(highHotVideos))

// 重排序数据
data = append(data, lowHotVideos...)
data = append(data, newVideos...)
data = append(data, highHotVideos...)
data = append(data, otherData...)
data = l.ReOrderVideo(data)

// 构建 debug 信息
debugInfos = make(map[string]any)
debugInfos["runtime_info"] = map[string]any{
"req_count": req.VideoCount,
"resp_count": len(data),
"user_see_count": len(userSeeData),
"time1_getuser": getUserEndTime.Sub(startTime).String(),
"time2_prepare": prepareEndTime.Sub(getUserEndTime).String(),
"time3_recommend": time.Since(prepareEndTime).String(),
"time4_total": time.Since(startTime).String(),
}
debugInfos["user_info"] = map[string]any{
"user_id": userId,
"user_behavior_stat": userBehaviorCount,
}
debugInfos["recommend_debug"] = map[string]*common.DebugInfo{
"1_LOW_HOT": lowHotctx.GetDebugInfo(),
"2_NEW": newctx.GetDebugInfo(),
"3_HIGH_HOT": highHotctx.GetDebugInfo(),
"4_OTHER": otherctx.GetDebugInfo(),
}
l.Infow("GetVideoList finish", logx.Field("user_id", userId), logx.Field("debug", debugInfos))
if l.svcCtx.Config.EnvTag == enums.EnvOnline {
debugInfos = nil // 线上不返回 debug 信息
}

threading.GoSafe(func() {
l.AddRecommendList(common.FeedScene(req.FeedScene), req.VideoType, userId, common.TransformGenerateTypes(data))
})
return
}


func (l *FeedsLogic) prepareRecommendList(userId int64, req types.FeedsVideoListReq, userSeeCount int) error {
feedScene := common.FeedScene(req.FeedScene)
if feedScene == common.FeedSceneHomepage || feedScene == common.FeedSceneTrending {
switch req.RefreshMethod {
case string(common.VideoRefreshMethodFirstLaunch): // 首次启动,清理数据
return l.DelRecommendList(feedScene, req.VideoType, userId, common.RECOMMEND_LIST_RECORD_COUNT)
case string(common.VideoRefreshMethodPullRefresh): // 下拉刷新,也做清理操作
return l.DelRecommendList(feedScene, req.VideoType, userId, common.RECOMMEND_LIST_RECORD_COUNT)
}
}
return nil
}


func (l *FeedsLogic) AddRecommendList(feedScene common.FeedScene, videoType string, userId int64, list []string) error {
if len(list) == 0 {
return nil
}
now := time.Now().Unix()
var values []redis.Pair
for _, v := range list {
values = append(values, redis.Pair{Key: v, Score: now})
}
key := common.GetRecommendListKey(feedScene, videoType, userId)
_, err := l.svcCtx.Redis.Zadds(key, values...)
if err != nil {
return err
}

_err := l.DelRecommendList(feedScene, videoType, userId, -common.RECOMMEND_LIST_RECORD_COUNT) // 负数是保留分数最高的 N 个元素
if _err != nil {
l.Errorf("AddRecommendList DelRecommendList: %v", _err)
}

_err = l.svcCtx.Redis.Expire(key, common.RECOMMEND_LIST_EXPIRE)
if _err != nil {
l.Errorf("AddRecommendList Expire: %v", _err)
}
return nil
}

func (l *FeedsLogic) DelRecommendList(feedScene common.FeedScene, videoType string, userId int64, count int64) error {
key := common.GetRecommendListKey(feedScene, videoType, userId)
_, err := l.svcCtx.Redis.Zremrangebyrank(key, 0, count-1) // 删除分数最低的 N 个元素
if err != nil {
l.Errorf("DelRecommendList Zremrangebyrank: %v", err)
}
return err
}
可以加首页作者微信,咨询相关问题!