帖子的热度就是分数,
不同网站分数记录公式:
P是关注数,T是帖子发布到现在的时间间隔,按小时计。G是系数,1.5,1.8…
-1,+2这些都是经验值。
log的作用是使得前期的评论、赞、收藏 占比更高,而不是后期。
分数是和发布时间成反比的。
通常启动一个定时任务来算分数(半个小时、一个小时),算完一回,热门的帖子会保持这段时间内的稳定,过段时间会在刷新一遍。
先把评论、赞、收藏 产生变化 的帖子丢进缓存redis中,当时间到了,只计算产生变化的帖子的分数。
RedisKeyUtil
private static final String PREFIX_POST = "post";//存变化的帖子
发帖的时候
// 统计帖子分数
public static String getPostScoreKey() {
return PREFIX_POST + SPLIT + "score";
}
}
DiscussPostController中
@Autowired
private RedisTemplate redisTemplate;
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();//获取key
redisTemplate.opsForSet().add(redisKey, post.getId());//队列不允许重复数据出现?????我们要去重且不关注顺序 用set,set不允许重复数据存在,会将重复的数据剔除。
置顶不需要加分,只要置顶就是最顶端。
加精的时候需要
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, id);
删除的时候不需要
还需要处理 评论、点赞时候的处理。
在CommentController中
@Autowired
private RedisTemplate redisTemplate;
if (comment.getEntityType() == ENTITY_TYPE_POST) { 当评论类型是帖子
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, discussPostId);
}
在LikeController中,
对帖子点赞,才会计算分数
if(entityType == ENTITY_TYPE_POST) {
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, postId);
}
项目汇总但凡能影响帖子分数的地方 都把帖子id放入集合set
下面要做的是 每隔一段时间 来统计分数,需要用到上节课所讲的quarze
先写个job
新建PostScoreRefreshJob类。执行帖子分数刷新 的任务
public class PostScoreRefreshJob implements Job, CommunityConstant {
private static final Logger logger = LoggerFactory.getLogger(PostScoreRefreshJob.class);//实例化Logger
//注入一些常用的bean
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private DiscussPostService discussPostService;
@Autowired
private LikeService likeService;
@Autowired
private ElasticsearchService elasticsearchService;
// 牛客纪元(牛客成立的日子)
private static final Date epoch;//日期类型是Date
//初始化常量epoch
static {
try {
epoch = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-08-01 00:00:00");//SimpleDateFormat把字符串转为日期,并将其赋值给epoch
} catch (ParseException e) {
throw new RuntimeException("初始化牛客纪元失败!", e);
}
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
String redisKey = RedisKeyUtil.getPostScoreKey();//从redis中取值
BoundSetOperations operations = redisTemplate.boundSetOps(redisKey);//因为每一个key都要进行运算,即需要反复操作,所以需要BoundSetOperations
if (operations.size() == 0) {
logger.info("[任务取消] 没有需要刷新的帖子!");//只需记录日志,不做任何处理
return;
}
logger.info("[任务开始] 正在刷新帖子分数: " + operations.size());//operations.size()表示 要刷新多少个帖子
while (operations.size() > 0) {//只要redis中有数据,则开始计算
this.refresh((Integer) operations.pop());//operations是集合
}
logger.info("[任务结束] 帖子分数刷新完毕!");
}
private void refresh(int postId) {
DiscussPost post = discussPostService.findDiscussPostById(postId);
if (post == null) {
logger.error("该帖子不存在: id = " + postId);
return;
}
//取到公式中所用到的值
// 是否精华
boolean wonderful = post.getStatus() == 1;
// 评论数量
int commentCount = post.getCommentCount();
// 点赞数量
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId);
// 计算权重
double w = (wonderful ? 75 : 0) + commentCount * 10 + likeCount * 2;//(wonderful ? 75 : 0) 如果是精华,加75分;
// 分数 = 帖子权重 + 距离天数
double score = Math.log10(Math.max(w, 1))//因为log10()里面必须大于1,不然就是负数。
+ (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 3600 * 24);//.getTime()均得到一个毫秒值,方便相减
// 更新帖子分数
discussPostService.updateScore(postId, score);
// 同步搜索数据
post.setScore(score);
elasticsearchService.saveDiscussPost(post);
}
}
在DiscussPostService中,添加
public int updateScore(int id, double score) {
return discussPostMapper.updateScore(id, score);
}
在discussPostMapper.xml中
<update id="updateScore">
update discuss_post set score = #{score} where id = #{id}
</update>
该job若想正常运行,还得配置。
在QuartzConfig中:
// 刷新帖子分数任务
@Bean
public JobDetailFactoryBean postScoreRefreshJobDetail() {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(PostScoreRefreshJob.class);
factoryBean.setName("postScoreRefreshJob");
factoryBean.setGroup("communityJobGroup");
factoryBean.setDurability(true);
factoryBean.setRequestsRecovery(true);
return factoryBean;
}
@Bean
public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail) {
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(postScoreRefreshJobDetail);
factoryBean.setName("postScoreRefreshTrigger");
factoryBean.setGroup("communityTriggerGroup");
factoryBean.setRepeatInterval(1000 * 60 * 5);
factoryBean.setJobDataMap(new JobDataMap());
return factoryBean;
}
测试:
看看三个帖子的分数,因为做了对数运算,所以差距不大
接下来做一个展现,
最热 按照分数对帖子进行排列
因此要对之前的代码重构,使得其支持这种排序。
重构需要从数据访问层改起。
在DiscussPostMapper.XML中
<select id="selectDiscussPosts" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where status != 2
<if test="userId!=0">
and user_id = #{userId}
</if>
<if test="orderMode==0"><!-- 安照原来的方式来排序 -->
order by type desc, create_time desc
</if>
<if test="orderMode==1">
order by type desc, score desc, create_time desc <!-- 优先按照type,将置顶的放在前面。其次安装score倒序,其次安照创建时间倒序-->
</if>
limit #{offset}, #{limit}
</select>
我们搜一下,查询哪个地方要调用其
这里都要进行修改
都改为:
在HomeController中
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model, Page page,
@RequestParam(name = "orderMode", defaultValue = "0") int orderMode) {//点击"最热",则orderMode1,如果没有传进来,则默认为0
// 方法调用钱,SpringMVC会自动实例化Model和Page,并将Page注入Model.
// 所以,在thymeleaf中可以直接访问Page对象中的数据.
page.setRows(discussPostService.findDiscussPostRows(0));
page.setPath("/index?orderMode=" + orderMode);
List<DiscussPost> list = discussPostService
.findDiscussPosts(0, page.getOffset(), page.getLimit(), orderMode);
List<Map<String, Object>> discussPosts = new ArrayList<>();
if (list != null) {
for (DiscussPost post : list) {
Map<String, Object> map = new HashMap<>();
map.put("post", post);
User user = userService.findUserById(post.getUserId());
map.put("user", user);
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId());
map.put("likeCount", likeCount);
discussPosts.add(map);
}
}
model.addAttribute("discussPosts", discussPosts);
model.addAttribute("orderMode", orderMode);
return "/index";
}
在index.html中
<!-- 筛选条件 -->
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<a th:class="|nav-link ${orderMode==0?'active':''}|" th:href="@{/index(orderMode=0)}">最新</a><!-- orderMode==0时 则电亮,即加active,否则什么都不输出-->
</li>
<li class="nav-item">
<a th:class="|nav-link ${orderMode==1?'active':''}|" th:href="@{/index(orderMode=1)}">最热</a>
</li>
</ul>
测试:
默认 最新排序
点击最热: