Echo/docs/290-热帖排行.md

6.9 KiB
Raw Blame History

热帖排行


每隔一段时间就计算帖子的分数,需要使用分布式定时任务:

Spring Quartz 导包

<!--Spring Quartz-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

由于 Spring Quartz 依赖于数据库,所以我们需要提前在数据库中创建 Quartz 需要的表

运行 init_quartz.sql 文件

分数计算设计:

分数计算

每次发生点赞(给帖子点赞)、评论(给帖子评论)、加精的时候,就将这些帖子存入缓存 Redis 中,然后通过分布式的定时任务,每隔一段时间就从缓存中取出这些帖子进行计算分数。

RedisKeyUtil

/**
 * 帖子分数
 * @return
 */
public static String getPostScoreKey() {
    return PREFIX_POST + SPLIT + "score";
}

Controller

以点赞操作为例:

Job

/**
 * 帖子分数计算刷新
 */
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

/**
 * 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;
    }
}

根据分数进行排行显示

重构下分页查询帖子的方法:

/**
 * 分页查询讨论帖信息
 *
 * @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 也需要做相应修改

/**
 * 分页查询讨论帖信息
 *
 * @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

<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>