234 lines
6.9 KiB
Markdown
234 lines
6.9 KiB
Markdown
![]() |
# 热帖排行
|
|||
|
|
|||
|
---
|
|||
|
|
|||
|
每隔一段时间就计算帖子的分数,需要使用分布式定时任务:
|
|||
|
|
|||
|

|
|||
|
|
|||
|

|
|||
|
|
|||
|
Spring Quartz 导包
|
|||
|
|
|||
|
```xml
|
|||
|
<!--Spring Quartz-->
|
|||
|
<dependency>
|
|||
|
<groupId>org.springframework.boot</groupId>
|
|||
|
<artifactId>spring-boot-starter-quartz</artifactId>
|
|||
|
</dependency>
|
|||
|
```
|
|||
|
|
|||
|
由于 Spring Quartz 依赖于数据库,所以我们需要提前在数据库中创建 Quartz 需要的表
|
|||
|
|
|||
|
运行 init_quartz.sql 文件
|
|||
|
|
|||
|
<img src="https://gitee.com/veal98/images/raw/master/img/20210131155131.png" style="zoom: 67%;" />
|
|||
|
|
|||
|
分数计算设计:
|
|||
|
|
|||
|

|
|||
|
|
|||
|
|
|||
|
|
|||
|
## 分数计算
|
|||
|
|
|||
|
每次发生点赞(给帖子点赞)、评论(给帖子评论)、加精的时候,就将这些帖子存入缓存 Redis 中,然后通过分布式的定时任务,每隔一段时间就从缓存中取出这些帖子进行计算分数。
|
|||
|
|
|||
|
RedisKeyUtil
|
|||
|
|
|||
|
```java
|
|||
|
/**
|
|||
|
* 帖子分数
|
|||
|
* @return
|
|||
|
*/
|
|||
|
public static String getPostScoreKey() {
|
|||
|
return PREFIX_POST + SPLIT + "score";
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### Controller
|
|||
|
|
|||
|
以点赞操作为例:
|
|||
|
|
|||
|
<img src="https://gitee.com/veal98/images/raw/master/img/20210131173755.png" style="zoom: 50%;" />
|
|||
|
|
|||
|
### Job
|
|||
|
|
|||
|
```java
|
|||
|
/**
|
|||
|
* 帖子分数计算刷新
|
|||
|
*/
|
|||
|
public class PostScoreRefreshJob implements Job, CommunityConstant {
|
|||
|
|
|||
|
private static final Logger logger = LoggerFactory.getLogger(PostScoreRefreshJob.class);
|
|||
|
|
|||
|
@Autowired
|
|||
|
private RedisTemplate redisTemplate;
|
|||
|
|
|||
|
@Autowired
|
|||
|
private DiscussPostSerivce discussPostSerivce;
|
|||
|
|
|||
|
@Autowired
|
|||
|
private LikeService likeService;
|
|||
|
|
|||
|
@Autowired
|
|||
|
private ElasticsearchService elasticsearchService;
|
|||
|
|
|||
|
// Epoch 纪元
|
|||
|
private static final Date epoch;
|
|||
|
|
|||
|
static {
|
|||
|
try {
|
|||
|
epoch = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-01-01 00:00:00");
|
|||
|
} catch (ParseException e) {
|
|||
|
throw new RuntimeException("初始化 Epoch 纪元失败", e);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
@Override
|
|||
|
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
|
|||
|
String redisKey = RedisKeyUtil.getPostScoreKey();
|
|||
|
BoundSetOperations operations = redisTemplate.boundSetOps(redisKey);
|
|||
|
|
|||
|
if (operations.size() == 0) {
|
|||
|
logger.info("[任务取消] 没有需要刷新的帖子");
|
|||
|
return ;
|
|||
|
}
|
|||
|
|
|||
|
logger.info("[任务开始] 正在刷新帖子分数: " + operations.size());
|
|||
|
while (operations.size() > 0) {
|
|||
|
this.refresh((Integer) operations.pop());
|
|||
|
}
|
|||
|
logger.info("[任务结束] 帖子分数刷新完毕");
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 刷新帖子分数
|
|||
|
* @param postId
|
|||
|
*/
|
|||
|
private void refresh(int postId) {
|
|||
|
DiscussPost post = discussPostSerivce.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;
|
|||
|
// 分数 = 权重 + 发帖距离天数
|
|||
|
double score = Math.log10(Math.max(w, 1))
|
|||
|
+ (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 3600 * 24);
|
|||
|
// 更新帖子分数
|
|||
|
discussPostSerivce.updateScore(postId, score);
|
|||
|
// 同步更新搜索数据
|
|||
|
post.setScore(score);
|
|||
|
elasticsearchService.saveDiscusspost(post);
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
注意同步更新一下搜索 Elasticsearch 服务器的数据
|
|||
|
|
|||
|
### Quarz Config
|
|||
|
|
|||
|
```java
|
|||
|
/**
|
|||
|
* Spring Quartz 配置类,用于将数据存入数据库,以后直接从数据库中调用数据
|
|||
|
*/
|
|||
|
@Configuration
|
|||
|
public class QuartzConfig {
|
|||
|
|
|||
|
/**
|
|||
|
* 刷新帖子分数任务
|
|||
|
* @return
|
|||
|
*/
|
|||
|
@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;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 刷新帖子分数触发器
|
|||
|
* @return
|
|||
|
*/
|
|||
|
@Bean
|
|||
|
public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail) {
|
|||
|
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
|
|||
|
factoryBean.setJobDetail(postScoreRefreshJobDetail);
|
|||
|
factoryBean.setName("postScoreRefreshTrigger");
|
|||
|
factoryBean.setGroup("communityTriggerGroup");
|
|||
|
factoryBean.setRepeatInterval(1000 * 60 * 5); // 5分钟刷新一次
|
|||
|
factoryBean.setJobDataMap(new JobDataMap());
|
|||
|
return factoryBean;
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
## 根据分数进行排行显示
|
|||
|
|
|||
|
重构下分页查询帖子的方法:
|
|||
|
|
|||
|
```java
|
|||
|
/**
|
|||
|
* 分页查询讨论帖信息
|
|||
|
*
|
|||
|
* @param userId 当传入的 userId = 0 时查找所有用户的帖子
|
|||
|
* 当传入的 userId != 0 时,查找该指定用户的帖子
|
|||
|
* @param offset 每页的起始索引
|
|||
|
* @param limit 每页显示多少条数据
|
|||
|
* @param orderMode 排行模式(若传入 1, 则按照热度来排序)
|
|||
|
* @return
|
|||
|
*/
|
|||
|
List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit, int orderMode);
|
|||
|
```
|
|||
|
|
|||
|
添加一个 orderMode,若传入 1, 则按照热度来排序,传入 0(默认)则按照最新来排序。当然,置顶的帖子不受影响
|
|||
|
|
|||
|
对应的 service 也需要做相应修改
|
|||
|
|
|||
|
```java
|
|||
|
/**
|
|||
|
* 分页查询讨论帖信息
|
|||
|
*
|
|||
|
* @param userId 当传入的 userId = 0 时查找所有用户的帖子
|
|||
|
* 当传入的 userId != 0 时,查找该指定用户的帖子
|
|||
|
* @param offset 每页的起始索引
|
|||
|
* @param limit 每页显示多少条数据
|
|||
|
* @param orderMode 排行模式(若传入 1, 则按照热度来排序)
|
|||
|
* @return
|
|||
|
*/
|
|||
|
public List<DiscussPost> findDiscussPosts (int userId, int offset, int limit, int orderMode) {
|
|||
|
return discussPostMapper.selectDiscussPosts(userId, offset, limit, orderMode);
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
在修改对应的 HomeController
|
|||
|
|
|||
|

|
|||
|
|
|||
|
Get 方法:前端是通过 ? 来传递参数的:比如说`/index?1`,前端 `th:href="@{/index(orderMode=0)}"`
|
|||
|
|
|||
|
而通过请求体来传递参数使用的是 Post 请求 :比如说`/add/postid`,前端 `th:href="@{|/index/${post.id}|}"`
|
|||
|
|
|||
|
修改一下 index.html
|
|||
|
|
|||
|
```html
|
|||
|
<a th:class="|nav-link ${orderMode==0 ? 'active' : ''}|" th:href="@{/index(orderMode=0)}"><i class="bi bi-lightning"></i> 最新</a>
|
|||
|
|
|||
|
<a th:class="|nav-link ${orderMode==1 ? 'active' : ''}|" th:href="@{/index(orderMode=1)}"><i class="bi bi-hand-thumbs-up"></i> 最热</a>
|
|||
|
```
|