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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
package com.tanpu.community.service;
import com.tanpu.biz.common.enums.clue.PageEnum;
import com.tanpu.biz.common.enums.community.CollectionTypeEnum;
import com.tanpu.biz.common.enums.community.TopicStatusEnum;
import com.tanpu.community.api.beans.qo.ThemeAnalysDO;
import com.tanpu.community.api.beans.qo.TopicRankQo;
import com.tanpu.community.api.beans.vo.feign.fatools.UserInfoResp;
import com.tanpu.community.cache.RedisCache;
import com.tanpu.community.dao.entity.community.ThemeEntity;
import com.tanpu.community.dao.entity.community.TopicEntity;
import com.tanpu.community.util.BizUtils;
import com.tanpu.community.util.ConvertUtil;
import com.tanpu.community.util.TimeUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.tanpu.community.api.constants.RedisKeyConstant.CACHE_FEIGN_USER_INFO;
@Service
public class RankService {
@Value("${rank.theme.viewRate:0.1}")
public Double viewRate;
@Value("${rank.theme.forwardRate:3}")
public Double forwardRate;
@Value("${rank.theme.commentRate:2}")
public Double commentRate;
@Value("${rank.theme.likeRaten:3}")
public Double likeRaten;
@Value("${rank.theme.collectRate:3}")
public Double collectRate;
//用户质量权重
@Value("${rank.theme.userWeightRate:0.9}")
public Double userWeightRate;
//初始质量
@Value("${rank.theme.initialWeight:1.0}")
public Double initialWeight;
//时间系数
@Value("${rank.theme.timeRate:0.2}")
public Double timeRate;
@Value("${rank.topic.viewRate:1}")
public Double topicViewRate;
@Value("${rank.topic.discussRate:3}")
public Double topicDiscussRate;
@Value("${rank.topic.themeRate:1}")
public Double topicThemeRate;
//时间系数
@Value("${rank.theme.timeRate:1}")
public Double topicTimeRate;
@Autowired
private ThemeService themeService;
@Autowired
private CollectionService collectionService;
@Autowired
private CommentService commentService;
@Autowired
private TopicService topicService;
@Autowired
private VisitLogService visitLogService;
@Autowired
private FeignService feignService;
@Autowired
private RedisCache redisCache;
@Resource
private RankLogService rankLogService;
//最热
private List<ThemeAnalysDO> hotestThemes = new ArrayList<>();
private List<TopicRankQo> rankTopicList = new ArrayList<>();
private List<TopicRankQo> rankTopicListTop4 = new ArrayList<>();
/**
* 计算主题热度排行
*/
public void rankThemes() {
LocalDateTime start = LocalDateTime.now();
//7天内所有主题进行热度值排序
List<ThemeEntity> themeEntities = themeService.queryRecentdays(7);
if (CollectionUtils.isEmpty(themeEntities)) {
return;
}
List<ThemeAnalysDO> themeAnalysDOS = ConvertUtil.themeEntityToAnalysDOs(themeEntities);
//批量查询
List<String> themeIds = themeAnalysDOS.stream().map(ThemeAnalysDO::getThemeId).collect(Collectors.toList());
Map<String, Integer> likeCountMap = collectionService.getCountMapByType(themeIds, CollectionTypeEnum.LIKE_THEME);
Map<String, Integer> bookCountMap = collectionService.getCountMapByType(themeIds, CollectionTypeEnum.COLLECT_THEME);
Map<String, Integer> commentCountMap = commentService.getCountMapByThemeIds(themeIds);
Map<String, Integer> forwardCountMap = themeService.getForwardCountMap(themeIds);
Map<String, Integer> visitCountMap = visitLogService.getCountMapByTargetIds(themeIds, PageEnum.COMM_VISIT_THEME.getId());
for (ThemeAnalysDO theme : themeAnalysDOS) {
String themeId = theme.getThemeId();
theme.setCommentCount(commentCountMap.getOrDefault(themeId, 0));
theme.setLikeCount(likeCountMap.getOrDefault(themeId, 0));
theme.setForwardCount(forwardCountMap.getOrDefault(themeId, 0));
theme.setCollectCount(bookCountMap.getOrDefault(themeId, 0));
theme.setViewCount(visitCountMap.getOrDefault(themeId, 0));
//查询用户质量
String authorId = theme.getAuthorId();
UserInfoResp authorInfo = redisCache.getObject(StringUtils.joinWith("_", CACHE_FEIGN_USER_INFO, authorId),
60, () -> feignService.getUserInfoById(authorId), UserInfoResp.class);
if (authorInfo == null || authorInfo.getLevelGrade() == null) {
theme.setUserWeight(0.0);
} else {
// 设置用户权重
theme.setUserWeight(authorInfo.getLevelGrade() * 1.0);
}
//打分
this.calculateThemeScore(theme);
}
//排序
Map<ThemeAnalysDO, Double> map = themeAnalysDOS.stream().collect(Collectors.toMap(o -> o, ThemeAnalysDO::getScore));
//排序
hotestThemes = map.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.map(Map.Entry::getKey).collect(Collectors.toList());
//落库
rankLogService.logThemeRank(hotestThemes, start, TimeUtils.calMillisTillNow(start));
}
/**
* 计算话题热度
*
* @return
*/
public void rankTopics() {
LocalDateTime start = LocalDateTime.now();
List<TopicEntity> topicEntities = topicService.queryAll();
if (CollectionUtils.isEmpty(topicEntities)) {
this.rankTopicList = new ArrayList<>();
this.rankTopicListTop4 = new ArrayList<>();
return;
}
List<TopicRankQo> topicRankQos = ConvertUtil.topicEntityToHotQos(topicEntities);
List<String> topicIds = topicRankQos.stream().map(TopicRankQo::getTopicId).collect(Collectors.toList());
Map<String, Integer> topicViewMap = visitLogService.getCountMapByTargetIds(topicIds, PageEnum.COMM_VISIT_TOPIC_DETAIL.getId());
for (TopicRankQo topic : topicRankQos) {
List<String> themeIds = themeService.queryThemeIdsByTopic(topic.getTopicId());
if (CollectionUtils.isEmpty(themeIds)) {
topic.setViewCount(topicViewMap.getOrDefault(topic.getTopicId(), 0));
topic.setDisscussCount(0);
topic.setThemeWeight(0.0);
} else {
// 浏览量
Integer topicPV = topicViewMap.getOrDefault(topic.getTopicId(), 0);
Integer themePV = visitLogService.queryThemeVisit(themeIds);
topic.setViewCount(topicPV + themePV + topic.getViewCntAdjust());
//讨论数=发布主题贴数+回复总数
Integer commentCount = commentService.getTotalCountByThemeIds(themeIds);
topic.setDisscussCount(themeIds.size() + commentCount);
//帖子权重,求和
double themeSum = getHotestThemes().stream().filter(o -> topic.getTopicId().equals(o.getTopicId()))
.mapToDouble(ThemeAnalysDO::getScore)
.sum();
topic.setThemeWeight(themeSum);
}
// 打分
calculateTopicScore(topic);
// 格式化浏览量、讨论量
topic.setFormatViewCount(BizUtils.formatCountNumber(topic.getViewCount()));
topic.setFormatDisscussCount(BizUtils.formatCountNumber(topic.getDisscussCount()));
}
// 排序
Map<TopicRankQo, Double> map = topicRankQos.stream().collect(Collectors.toMap(o -> o, TopicRankQo::getScore));
List<TopicRankQo> rankList = map.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
rankList.get(0).setType(TopicStatusEnum.HOTTEST.getCode());
this.rankTopicList = rankList;
this.rankTopicListTop4 = rankList.stream().limit(4).collect(Collectors.toList());
//落库
rankLogService.logTopicRank(rankList, start, TimeUtils.calMillisTillNow(start));
return;
}
/**
* 从排序列表中返回话题详情
*
* @param topicId 话题Id
* @return
*/
public TopicRankQo getTopicDetail(String topicId) {
if (this.rankTopicList.size() == 0) {
rankTopics();
}
List<TopicRankQo> matchTopic = this.rankTopicList.stream().filter(o -> topicId.equals(o.getTopicId())).limit(1).collect(Collectors.toList());
matchTopic.add(new TopicRankQo());
return matchTopic.get(0);
}
public List<TopicRankQo> getRankTopicList(String keyword) {
if (this.rankTopicList.size() == 0) {
this.rankTopics();
}
if (StringUtils.isEmpty(keyword)) {
return rankTopicList;
} else {
//过滤关键字
return this.rankTopicList.stream().filter(o -> o.getTopicTitle().contains(keyword)).collect(Collectors.toList());
}
}
public List<TopicRankQo> getRankTopicListTop4() {
if (this.rankTopicList.size() == 0) {
this.rankTopics();
}
return rankTopicListTop4;
}
public List<ThemeAnalysDO> getHotestThemes() {
if (this.hotestThemes.size() == 0) {
rankThemes();
}
return hotestThemes;
}
public List<String> getRankThemeListByTopic(String topicId, List<String> excludeIds) {
if (this.hotestThemes.size() == 0) {
this.rankThemes();
}
return hotestThemes.stream()
.filter(o -> topicId.equals(o.getTopicId()) && !excludeIds.contains(o.getThemeId()))
.map(ThemeAnalysDO::getThemeId)
.collect(Collectors.toList());
}
// todo 这里用户层面只考虑了用户的gradelevel,后续可以为用户的历史贴子打分。根据用户发表的历史帖子的质量,为用户评分。例如,历史帖子的关注度,用户的粉丝数量。
// todo 可以考虑一下话题的热度。
private void calculateThemeScore(ThemeAnalysDO theme) {
// 质量=帖子质量+用户质量
double w = theme.getViewCount() * viewRate
+ theme.getForwardCount() * forwardRate
+ theme.getCommentCount() * commentRate
+ theme.getLikeCount() * likeRaten
+ theme.getCollectCount() * collectRate
+ Math.pow(theme.getUserWeight(), userWeightRate);
double score = (w + initialWeight) / Math.pow(theme.getMinutesTillNow() + 1, timeRate);
theme.setScore(score);
}
public void calculateTopicScore(TopicRankQo topic) {
//顶置话题
if (topic.getIsTop() > 0) {
topic.setScore(Double.MAX_VALUE);
}
Double socre = ((topic.getDisscussCount() * topicDiscussRate + topic.getViewCount() * topicViewRate)
/ Math.pow(topic.getMinutesTillNow() + 1, topicTimeRate))
+ topic.getThemeWeight() * topicThemeRate;
topic.setScore(socre);
}
}