问答平台(7),热帖排行
问题背景
将分数变化的帖子放入 Redis 缓存中,每5分钟定时任务刷新帖子的分数。
1 |
|
工具类
- RedisKeyUtil: 添加内容
1
2
3
4
5
6private static final String PREFIX_POST = "post";
// 帖子分数
public static String getPostScoreKey() {
return PREFIX_POST + SPLIT + "score";
}
表现层
- DiscussPostController: 修改(计算帖子分数)
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@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(path = "/add", method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title, String content) {
User user = hostHolder.getUser();
if (user == null) {
return CommunityUtil.getJSONString(403, "还没有登录!");
}
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
discussPostService.addDiscussPost(post);
// 触发发帖事件
Event event = new Event()
.setTopic(TOPIC_PUBLISH)
.setUserId(user.getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(post.getId());
eventProducer.fireEvent(event);
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, post.getId());
// 报错的情况,将来统一处理
return CommunityUtil.getJSONString(0, "发布成功!");
}
// 加精
@RequestMapping(path = "/wonderful", method = RequestMethod.POST)
@ResponseBody
public String setWonderful(int id) {
discussPostService.updateStatus(id, 1);
// 触发发帖事件
Event event = new Event()
.setTopic(TOPIC_PUBLISH)
.setUserId(hostHolder.getUser().getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(id);
eventProducer.fireEvent(event);
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, id);
return CommunityUtil.getJSONString(0);
} - CommentController: 修改
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@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
comment.setUserId(hostHolder.getUser().getId());
comment.setStatus(0);
comment.setCreateTime(new Date());
commentService.addComment(comment);
// 触发评论事件
Event event = new Event()
.setTopic(TOPIC_COMMENT)
.setUserId(hostHolder.getUser().getId())
.setEntityType(comment.getEntityType())
.setEntityId(comment.getEntityId())
.setData("postId", discussPostId);
if (comment.getEntityType() == ENTITY_TYPE_POST) {
DiscussPost target = discussPostService.findDiscussPostById(comment.getEntityId());
event.setEntityUserId(target.getUserId());
} else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) {
Comment target = commentService.findCommentById(comment.getEntityId());
event.setEntityUserId(target.getUserId());
}
eventProducer.fireEvent(event);
if (comment.getEntityType() == ENTITY_TYPE_POST) {
// 触发发帖事件
event = new Event()
.setTopic(TOPIC_PUBLISH)
.setUserId(comment.getUserId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(discussPostId);
eventProducer.fireEvent(event);
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, discussPostId);
}
return "redirect:/discuss/detail/" + discussPostId;
} - LikeController: 修改
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@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(path = "/like", method = RequestMethod.POST)
@ResponseBody
public String like(int entityType, int entityId, int entityUserId, int postId) {
User user = hostHolder.getUser();
// 点赞
likeService.like(user.getId(), entityType, entityId, entityUserId);
// 数量
long likeCount = likeService.findEntityLikeCount(entityType, entityId);
// 状态
int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
// 返回的结果
Map<String, Object> map = new HashMap<>();
map.put("likeCount", likeCount);
map.put("likeStatus", likeStatus);
// 触发点赞事件
if (likeStatus == 1) {
Event event = new Event()
.setTopic(TOPIC_LIKE)
.setUserId(hostHolder.getUser().getId())
.setEntityType(entityType)
.setEntityId(entityId)
.setEntityUserId(entityUserId)
.setData("postId", postId);
eventProducer.fireEvent(event);
}
if (entityType == ENTITY_TYPE_POST) {
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, postId);
}
return CommunityUtil.getJSONString(0, null, map);
}
quartz
- PostScoreRefreshJob: 新增
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// 定时任务
public class PostScoreRefreshJob implements Job, CommunityConstant {
private static final Logger logger = LoggerFactory.getLogger(PostScoreRefreshJob.class);
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private DiscussPostService discussPostService;
@Autowired
private LikeService likeService;
@Autowired
private ElasticsearchService elasticsearchService;
// 牛客纪元
private static final Date epoch;
static {
try {
epoch = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-08-01 00:00:00");
} catch (ParseException e) {
throw new RuntimeException("初始化牛客纪元失败!", e);
}
}
@Override
public void execute(JobExecutionContext context) 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("[任务结束] 帖子分数刷新完毕!");
}
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;
// 分数 = 帖子权重 + 距离天数
double score = Math.log10(Math.max(w, 1))
+ (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 60 * 60 * 24);
// 更新帖子分数
discussPostService.updateScore(postId, score);
// 同步搜索数据
post.setScore(score);
elasticsearchService.saveDiscussPost(post);
}
}
config
- QuartzConfig: 添加内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 刷新帖子分数任务- 将分数变化的帖子丢到Redis缓存中,每5分钟计算一次帖子的分数。
@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: 重构 selectDiscussPosts
- orderMode: 排序模式(0默认,1热度)
1
2int updateScore(int id, double score);
List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit, int orderMode); - discusspost-mapper.xml: 重构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<update id="updateScore">
update discuss_post set score = #{score} where id = #{id}
</update>
<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
</if>
limit #{offset}, #{limit}
</select>
业务层
- DiscussPostService: 重构 findDiscussPosts
1
2
3
4
5
6
7
8
9
10
11
12public int updateScore(int id, double score) {
return discussPostMapper.updateScore(id, score);
}
public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit, int orderMode) {
if (userId == 0 && orderMode == 1) {
return postListCache.get(offset + ":" + limit);
}
logger.debug("load post list from DB.");
return discussPostMapper.selectDiscussPosts(userId, offset, limit, orderMode);
}
表现层
- HomeController: 重构 getIndexPage
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@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model, Page page,
@RequestParam(name = "orderMode", defaultValue = "0") int orderMode) {
// 方法调用前,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: 修改
1
2
3
4
5
6
7
8<!-- 内容 -->
<!-- 筛选条件 -->
<li class="nav-item">
<a th:class="|nav-link ${orderMode==0?'active':''}|" th:href="@{/index(orderMode=0)}">最新</a>
</li>
<li class="nav-item">
<a th:class="|nav-link ${orderMode==1?'active':''}|" th:href="@{/index(orderMode=1)}">最热</a>
</li>
结果展示
问答平台(7),热帖排行
https://lcf163.github.io/2020/06/17/问答平台(7),热帖排行/