优化评论返回模型,注意数据库表变更,需要重新运行sql脚本。完成评论和帖子的自定义热度排序

main
forely 4 weeks ago
parent e683171570
commit b006059b41

@ -85,7 +85,7 @@ public class RedisUtil {
if(typedTuples == null || typedTuples.isEmpty()){ if(typedTuples == null || typedTuples.isEmpty()){
return ScrollPageResponse.<T>builder().build(); return ScrollPageResponse.<T>builder().build();
} }
// 获取返回的offset与minTime // 获取返回的offset与minval
List<Long> ids = new ArrayList<>(); List<Long> ids = new ArrayList<>();
int returnOffset = 1; int returnOffset = 1;
long min = 0; long min = 0;
@ -109,6 +109,8 @@ public class RedisUtil {
.build(); .build();
} }
public void set(String key, Object value) { public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value, DEFAULT_TIMEOUT, DEFAULT_TIME_UNIT); redisTemplate.opsForValue().set(key, value, DEFAULT_TIMEOUT, DEFAULT_TIME_UNIT);
} }

@ -2,8 +2,10 @@ package com.luojia_channel;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@EnableScheduling
public class LuojiaChannelApplication { public class LuojiaChannelApplication {
public static void main(String[] args) { public static void main(String[] args) {

@ -32,7 +32,7 @@ public class ChatController {
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试") @ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
}) })
@GetMapping("/chat-list") @GetMapping("/chat-list")
public Result<ScrollPageResponse<ChatItemDTO>> getChatList(@RequestBody ChatPageQueryDTO chatPageQueryDTO) { public Result<ScrollPageResponse<ChatItemDTO>> getChatList(ChatPageQueryDTO chatPageQueryDTO) {
return Result.success(chatService.getChatList(chatPageQueryDTO)); return Result.success(chatService.getChatList(chatPageQueryDTO));
} }
@ -46,7 +46,7 @@ public class ChatController {
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试") @ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
}) })
@GetMapping("/history") @GetMapping("/history")
public Result<ScrollPageResponse<MessageResponse>> getChatHistory(@RequestBody ChatPageQueryDTO chatPageQueryDTO) { public Result<ScrollPageResponse<MessageResponse>> getChatHistory(ChatPageQueryDTO chatPageQueryDTO) {
return Result.success(chatService.getChatHistory(chatPageQueryDTO)); return Result.success(chatService.getChatHistory(chatPageQueryDTO));
} }
} }

@ -0,0 +1,86 @@
package com.luojia_channel.modules.post.algorithm;
import cn.hutool.core.bean.BeanUtil;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import com.luojia_channel.modules.post.entity.Comment;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.CommentMapper;
import com.luojia_channel.modules.post.mapper.PostMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.concurrent.TimeUnit;
@Component
@RequiredArgsConstructor
public class PostSelector {
private final PostMapper postMapper;
private final CommentMapper commentMapper;
private final RedisUtil redisUtil;
public void calculatePostScore(Post post){
long hours = calculateHoursSince(post.getCreateTime());
double likes = post.getLikeCount();
double comments = post.getCommentCount();
double favorites = post.getFavoriteCount();
double newScore = (1 + likes + comments*0.7 + favorites*0.5)
/ Math.pow(1 + hours/24.0, 1.8);
redisUtil.zAdd("post:hot:", post.getId(), newScore);
}
public void calculateCommentScore(Comment comment){
long hours = calculateHoursSince(comment.getCreateTime());
double likes = comment.getLikeCount();
double replies = comment.getReplyCount();
double newScore = (1 + likes + replies*0.7)
/ Math.pow(1 + hours/24.0, 1.8);
if(comment.getId().equals(comment.getTopId())) {
redisUtil.zAdd("post:comment_by_hot:" + comment.getPostId(), comment.getId(), newScore);
} else {
redisUtil.zAdd("comment:reply_by_hot:" + comment.getTopId(), comment.getId(), newScore);
}
}
// 更新帖子热度分数
public void updatePostScore(Long postId) {
Post post = postMapper.selectById(postId);
if (post == null) return;
calculatePostScore(post);
}
// 更新评论热度分数
public void updateCommentScore(Long commentId) {
Comment comment = commentMapper.selectById(commentId);
if(comment == null) return;
calculateCommentScore(comment);
}
// 计算从指定时间到现在经过的小时数
public long calculateHoursSince(LocalDateTime startTime) {
if (startTime == null) {
return 0;
}
Instant startInstant = startTime.atZone(ZoneId.systemDefault()).toInstant();
Instant nowInstant = Instant.now();
long millisDiff = Duration.between(startInstant, nowInstant).toMillis();
return TimeUnit.HOURS.convert(millisDiff, TimeUnit.MILLISECONDS);
}
// TODO 自定义首页帖子算法
}

@ -82,7 +82,7 @@ public class CommentController {
@ApiResponse(responseCode = "200", description = "获取成功"), @ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败帖子ID不合法") @ApiResponse(responseCode = "500", description = "获取失败帖子ID不合法")
}) })
public Result<ScrollPageResponse<CommentInfoDTO>> getCommentsByPostId(@RequestBody CommentPageQueryDTO commentPageQueryDTO) { public Result<ScrollPageResponse<CommentInfoDTO>> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO) {
ScrollPageResponse<CommentInfoDTO> commentList = commentService.getCommentsByPostId(commentPageQueryDTO); ScrollPageResponse<CommentInfoDTO> commentList = commentService.getCommentsByPostId(commentPageQueryDTO);
return Result.success(commentList); return Result.success(commentList);
} }
@ -98,7 +98,7 @@ public class CommentController {
@ApiResponse(responseCode = "200", description = "获取成功"), @ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败评论ID不合法") @ApiResponse(responseCode = "500", description = "获取失败评论ID不合法")
}) })
public Result<ScrollPageResponse<CommentInfoDTO>> getReplyById(@RequestBody CommentPageQueryDTO commentPageQueryDTO) { public Result<ScrollPageResponse<CommentInfoDTO>> getReplyById(CommentPageQueryDTO commentPageQueryDTO) {
ScrollPageResponse<CommentInfoDTO> commentInfoDTOList = commentService.getReplyById(commentPageQueryDTO); ScrollPageResponse<CommentInfoDTO> commentInfoDTOList = commentService.getReplyById(commentPageQueryDTO);
return Result.success(commentInfoDTOList); return Result.success(commentInfoDTOList);
} }

@ -107,7 +107,7 @@ public class PostController {
@ApiResponse(responseCode = "200", description = "获取成功"), @ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试") @ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
}) })
public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePost(@RequestBody PostPageQueryDTO postPageQueryDTO) { public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePost(PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePost(postPageQueryDTO)); return Result.success(postService.pagePost(postPageQueryDTO));
} }
@ -121,7 +121,7 @@ public class PostController {
@ApiResponse(responseCode = "200", description = "获取成功"), @ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试") @ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
}) })
public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePostOfUser(@RequestBody PostPageQueryDTO postPageQueryDTO) { public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePostOfUser(PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePostOfUser(postPageQueryDTO)); return Result.success(postService.pagePostOfUser(postPageQueryDTO));
} }

@ -14,7 +14,6 @@ public class CommentPageQueryDTO extends ScrollPageRequest {
@Schema(title = "评论ID") @Schema(title = "评论ID")
private Long parentCommentId; private Long parentCommentId;
private Boolean orderByTime = true; @Schema(title = "排序类型0表示按时间1表示按热度")
private Integer type = 0;
private Boolean orderByHot = false;
} }

@ -11,4 +11,7 @@ public class PostPageQueryDTO extends ScrollPageRequest {
description = "想要查看的用户的id输入空时为自己的id" description = "想要查看的用户的id输入空时为自己的id"
) )
private Long userId; private Long userId;
@Schema(title = "排序类型0表示按时间1表示按热度2表示自定义的推荐算法(暂未实现)")
private Integer type = 0;
} }

@ -74,6 +74,8 @@ public class CommentInfoDTO {
private String userName; private String userName;
private String userAvatar; private String userAvatar;
private String replyUserName;
@Schema( @Schema(
description = "子评论列表" description = "子评论列表"
) )

@ -21,6 +21,7 @@ public class Comment {
private Long userId; private Long userId;
private Long postId; private Long postId;
private Long parentCommentId; private Long parentCommentId;
private Long replyUserId;
private Long topId; private Long topId;
private LocalDateTime createTime; private LocalDateTime createTime;
private LocalDateTime updateTime; private LocalDateTime updateTime;

@ -14,6 +14,7 @@ import com.luojia_channel.common.utils.PageUtil;
import com.luojia_channel.common.utils.RedisUtil; import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext; import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.message.mq.domain.NotificationMessage; import com.luojia_channel.modules.message.mq.domain.NotificationMessage;
import com.luojia_channel.modules.post.algorithm.PostSelector;
import com.luojia_channel.modules.post.dto.req.CommentPageQueryDTO; import com.luojia_channel.modules.post.dto.req.CommentPageQueryDTO;
import com.luojia_channel.modules.post.dto.req.CommentSaveDTO; import com.luojia_channel.modules.post.dto.req.CommentSaveDTO;
import com.luojia_channel.modules.post.dto.resp.CommentInfoDTO; import com.luojia_channel.modules.post.dto.resp.CommentInfoDTO;
@ -35,10 +36,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
@ -52,6 +50,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
private final NotificationProducer notificationProducer; private final NotificationProducer notificationProducer;
private final RedisUtil redisUtil; private final RedisUtil redisUtil;
private final PostMapper postMapper; private final PostMapper postMapper;
private final PostSelector postSelector;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@ -66,6 +65,9 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
comment.setUserId(userId); comment.setUserId(userId);
comment.setCreateTime(LocalDateTime.now()); comment.setCreateTime(LocalDateTime.now());
comment.setUpdateTime(LocalDateTime.now()); comment.setUpdateTime(LocalDateTime.now());
comment.setReplyCount(0L);
comment.setLikeCount(0L);
if (!save(comment)) { if (!save(comment)) {
throw new PostException("创建评论失败"); throw new PostException("创建评论失败");
} }
@ -74,6 +76,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
if (comment.getParentCommentId() == null) { if (comment.getParentCommentId() == null) {
comment.setTopId(comment.getId()); comment.setTopId(comment.getId());
comment.setParentCommentId(0L); comment.setParentCommentId(0L);
comment.setReplyUserId(0L);
if (!updateById(comment)) { if (!updateById(comment)) {
throw new PostException("更新根级评论 topId 失败"); throw new PostException("更新根级评论 topId 失败");
} }
@ -81,8 +84,8 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
String key = "post:comment_by_time:" + comment.getPostId(); String key = "post:comment_by_time:" + comment.getPostId();
redisUtil.zAdd(key, comment.getId(), System.currentTimeMillis()); redisUtil.zAdd(key, comment.getId(), System.currentTimeMillis());
} }
// 帖子的回复数加一
// 帖子的回复数加一
Long postId = comment.getPostId(); Long postId = comment.getPostId();
Post post = postMapper.selectById(postId); Post post = postMapper.selectById(postId);
if (post == null) { if (post == null) {
@ -119,15 +122,15 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
} }
// 设置顶级评论id // 设置顶级评论id
comment.setTopId(parentComment.getTopId()); comment.setTopId(parentComment.getTopId());
comment.setReplyUserId(parentComment.getUserId());
updateById(comment); updateById(comment);
// 更新评论的zset回复列表 // 更新评论的zset回复列表
String buildKey = String.format("%d_%d", comment.getPostId(), comment.getParentCommentId()); String key = "comment:reply_by_time:" + comment.getTopId();
String key = "comment:reply_by_time:" + buildKey;
redisUtil.zAdd(key, comment.getId(), System.currentTimeMillis()); redisUtil.zAdd(key, comment.getId(), System.currentTimeMillis());
// 更新顶级评论回复数,当顶级评论与回复评论不同时 // 更新顶级评论回复数
LambdaUpdateWrapper<Comment> updateWrapper = Wrappers.lambdaUpdate(Comment.class) LambdaUpdateWrapper<Comment> updateWrapper = Wrappers.lambdaUpdate(Comment.class)
.eq(Comment::getId, comment.getTopId()) .eq(Comment::getId, comment.getTopId())
.setSql("reply_count = reply_count + 1"); .setSql("reply_count = reply_count + 1");
@ -136,20 +139,32 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
throw new PostException("回复顶级评论失败"); throw new PostException("回复顶级评论失败");
} }
// 更新父评论回复数
if(!parentComment.getTopId().equals(parentComment.getId())){
parentComment.setReplyCount(parentComment.getReplyCount()+1);
updateById(parentComment);
postSelector.calculateCommentScore(parentComment);
}
postSelector.updateCommentScore(comment.getTopId());
// 消息通知,回复评论 // 消息通知,回复评论
String content = String.format("%s 回复了你的评论: %s", if(!userId.equals(comment.getReplyUserId())){
UserContext.getUsername(), String content = String.format("%s 回复了你的评论: %s",
StringUtils.abbreviate(commentSaveDTO.getContent(), 20)); UserContext.getUsername(),
NotificationMessage notificationMessage = NotificationMessage.builder() StringUtils.abbreviate(commentSaveDTO.getContent(), 20));
.senderId(userId) NotificationMessage notificationMessage = NotificationMessage.builder()
.senderName(UserContext.getUsername()) .senderId(userId)
.senderAvatar(UserContext.getAvatar()) .senderName(UserContext.getUsername())
.receiverId(parentComment.getUserId()) .senderAvatar(UserContext.getAvatar())
.content(content) .receiverId(parentComment.getUserId())
.messageType(2) .content(content)
.build(); .messageType(2)
notificationProducer.sendMessage(notificationMessage); .build();
notificationProducer.sendMessage(notificationMessage);
}
} }
postSelector.calculateCommentScore(comment);
postSelector.calculatePostScore(post);
redisUtil.delete("post:detail:" + comment.getPostId()); redisUtil.delete("post:detail:" + comment.getPostId());
return comment.getId(); return comment.getId();
} }
@ -179,9 +194,12 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
Comment comment = commentMapper.selectById(id); Comment comment = commentMapper.selectById(id);
if(comment.getId().equals(comment.getTopId())) { if(comment.getId().equals(comment.getTopId())) {
redisUtil.zRemove("post:comment_by_time:" + comment.getPostId(), comment.getId()); redisUtil.zRemove("post:comment_by_time:" + comment.getPostId(), comment.getId());
redisUtil.zRemove("post:comment_by_hot:" + comment.getPostId(), comment.getId());
redisUtil.delete("comment:reply_by_time:" + comment.getTopId()); redisUtil.delete("comment:reply_by_time:" + comment.getTopId());
redisUtil.delete("comment:reply_by_hot:" + comment.getTopId());
}else{ }else{
redisUtil.zRemove("comment:reply_by_time:" + comment.getTopId(), comment.getId()); redisUtil.zRemove("comment:reply_by_time:" + comment.getTopId(), comment.getId());
redisUtil.zRemove("comment:reply_by_hot:" + comment.getTopId(), comment.getId());
} }
// TODO 如果根评论删除,那么其他评论怎么办,目前做法是删除其下所有的子评论 // TODO 如果根评论删除,那么其他评论怎么办,目前做法是删除其下所有的子评论
} }
@ -199,11 +217,12 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
.orderByDesc(Comment::getCreateTime); .orderByDesc(Comment::getCreateTime);
return getCommentInfoDTOPageResponse(commentPageQueryDTO, queryWrapper); return getCommentInfoDTOPageResponse(commentPageQueryDTO, queryWrapper);
*/ */
String key = "post:comment_by_time:" + commentPageQueryDTO.getPostId(); String condition = commentPageQueryDTO.getType().equals(0) ? "time:" : "hot:";
String key = "post:comment_by_" + condition + commentPageQueryDTO.getPostId();
return redisUtil.scrollPageQuery(key, CommentInfoDTO.class, commentPageQueryDTO, return redisUtil.scrollPageQuery(key, CommentInfoDTO.class, commentPageQueryDTO,
(commentIds) -> { (commentIds) -> {
List<Comment> comments = commentMapper.selectByIdsOrderByField(commentIds); List<Comment> comments = commentMapper.selectByIdsOrderByField(commentIds);
List<Long> userIds = new ArrayList<>(); Set<Long> userIds = new HashSet<>();
comments.forEach(comment -> userIds.add(comment.getUserId())); comments.forEach(comment -> userIds.add(comment.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds); List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream() Map<Long, User> userMap = users.stream()
@ -232,13 +251,17 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
.orderByDesc(Comment::getCreateTime); .orderByDesc(Comment::getCreateTime);
return getCommentInfoDTOPageResponse(commentPageQueryDTO, queryWrapper); return getCommentInfoDTOPageResponse(commentPageQueryDTO, queryWrapper);
*/ */
String buildKey = String.format("%d_%d", commentPageQueryDTO.getPostId(), commentPageQueryDTO.getParentCommentId()); String condition = commentPageQueryDTO.getType().equals(0) ? "time:" : "hot:";
String key = "comment:reply_by_time:" + buildKey; String key = "comment:reply_by_" + condition + commentPageQueryDTO.getParentCommentId();
return redisUtil.scrollPageQuery(key, CommentInfoDTO.class, commentPageQueryDTO, return redisUtil.scrollPageQuery(key, CommentInfoDTO.class, commentPageQueryDTO,
(commentIds) -> { (commentIds) -> {
List<Comment> comments = commentMapper.selectByIdsOrderByField(commentIds); List<Comment> comments = commentMapper.selectByIdsOrderByField(commentIds);
List<Long> userIds = new ArrayList<>(); Set<Long> userIds = new HashSet<>();
comments.forEach(comment -> userIds.add(comment.getUserId())); comments.forEach(comment -> userIds.add(comment.getUserId()));
for(Comment comment : comments){
userIds.add(comment.getUserId());
userIds.add(comment.getReplyUserId());
}
List<User> users = userMapper.selectBatchIds(userIds); List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream() Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user)); .collect(Collectors.toMap(User::getId, user -> user));
@ -249,6 +272,8 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
commentInfoDTO.setUserAvatar(user.getAvatar()); commentInfoDTO.setUserAvatar(user.getAvatar());
commentInfoDTO.setUserName(user.getUsername()); commentInfoDTO.setUserName(user.getUsername());
commentInfoDTO.setIsLike(isLikedComment(comment.getId())); commentInfoDTO.setIsLike(isLikedComment(comment.getId()));
User replyUser = userMap.getOrDefault(comment.getReplyUserId(), new User());
commentInfoDTO.setReplyUserName(replyUser.getUsername());
commentInfoDTOS.add(commentInfoDTO); commentInfoDTOS.add(commentInfoDTO);
} }
return commentInfoDTOS; return commentInfoDTOS;

@ -17,6 +17,7 @@ import com.luojia_channel.modules.interact.entity.Follow;
import com.luojia_channel.modules.interact.mapper.FollowMapper; import com.luojia_channel.modules.interact.mapper.FollowMapper;
import com.luojia_channel.modules.interact.service.impl.FollowServiceImpl; import com.luojia_channel.modules.interact.service.impl.FollowServiceImpl;
import com.luojia_channel.modules.message.mq.domain.NotificationMessage; import com.luojia_channel.modules.message.mq.domain.NotificationMessage;
import com.luojia_channel.modules.post.algorithm.PostSelector;
import com.luojia_channel.modules.post.dto.req.PostSaveDTO; import com.luojia_channel.modules.post.dto.req.PostSaveDTO;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO; import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO; import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
@ -49,10 +50,11 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
private final ValidatePostUtil validatePostUtil; private final ValidatePostUtil validatePostUtil;
private final RedisUtil redisUtil; private final RedisUtil redisUtil;
private final UserMapper userMapper; private final UserMapper userMapper;
private final PostSelector postSelector;
// 匿名用户名与匿名头像 // 匿名用户名与匿名头像
private static final String ANONYMOUS_NAME = "匿名用户"; public static final String ANONYMOUS_NAME = "匿名用户";
private static final String ANONYMOUS_AVATAR = ""; public static final String ANONYMOUS_AVATAR = "";
private final FollowMapper followMapper; private final FollowMapper followMapper;
private final FollowServiceImpl followService; private final FollowServiceImpl followService;
@ -68,6 +70,10 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
post.setUserId(UserContext.getUserId()); post.setUserId(UserContext.getUserId());
post.setCreateTime(LocalDateTime.now()); post.setCreateTime(LocalDateTime.now());
post.setUpdateTime(LocalDateTime.now()); post.setUpdateTime(LocalDateTime.now());
post.setCommentCount(0);
post.setLikeCount(0);
post.setViewCount(0);
post.setFavoriteCount(0);
if(!save(post)){ if(!save(post)){
throw new PostException("创建帖子失败"); throw new PostException("创建帖子失败");
} }
@ -84,6 +90,7 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
} }
// TODO 消息通知? // TODO 消息通知?
} }
postSelector.calculatePostScore(post);
redisUtil.zAdd("post:time:", post.getId(), System.currentTimeMillis()); redisUtil.zAdd("post:time:", post.getId(), System.currentTimeMillis());
redisUtil.zAdd("post:user:" + userId, post.getId(), System.currentTimeMillis()); redisUtil.zAdd("post:user:" + userId, post.getId(), System.currentTimeMillis());
return post.getId(); return post.getId();
@ -120,6 +127,7 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
// redisUtil.delete("post:of:user:" + UserContext.getUserId()); // redisUtil.delete("post:of:user:" + UserContext.getUserId());
redisUtil.delete("post:detail:" + id); redisUtil.delete("post:detail:" + id);
redisUtil.zRemove("post:time:", id); redisUtil.zRemove("post:time:", id);
redisUtil.zRemove("post:hot:", id);
redisUtil.zRemove("post:user:" + userId, id); redisUtil.zRemove("post:user:" + userId, id);
} }
@ -135,6 +143,7 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
if(post == null){ if(post == null){
throw new PostException("帖子不存在或被删除"); throw new PostException("帖子不存在或被删除");
} }
postSelector.calculatePostScore(post);
PostInfoDTO postInfoDTO = BeanUtil.copyProperties(post, PostInfoDTO.class); PostInfoDTO postInfoDTO = BeanUtil.copyProperties(post, PostInfoDTO.class);
User user = userMapper.selectOne(Wrappers.lambdaQuery(User.class) User user = userMapper.selectOne(Wrappers.lambdaQuery(User.class)
.eq(User::getId, post.getUserId())); .eq(User::getId, post.getUserId()));
@ -152,7 +161,7 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
@Override @Override
public ScrollPageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO) { public ScrollPageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO) {
// TODO 目前分页查询直接按照创建时间顺序排序了,未来考虑加入多种规则 // TODO 未来考虑加入多种规则
/* /*
LambdaQueryWrapper<Post> queryWrapper = Wrappers.lambdaQuery(Post.class) LambdaQueryWrapper<Post> queryWrapper = Wrappers.lambdaQuery(Post.class)
.orderByDesc(Post::getCreateTime); .orderByDesc(Post::getCreateTime);
@ -175,7 +184,7 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
return postBasicInfoDTO; return postBasicInfoDTO;
}); });
*/ */
String key = "post:time:"; String key = postPageQueryDTO.getType().equals(0) ? "post:time:" : "post:hot:";
return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO, return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO,
(postIds) -> { (postIds) -> {
List<Long> userIds = new ArrayList<>(); List<Long> userIds = new ArrayList<>();

@ -0,0 +1,91 @@
package com.luojia_channel.modules.post.task;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.post.algorithm.PostSelector;
import com.luojia_channel.modules.post.entity.Comment;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.CommentMapper;
import com.luojia_channel.modules.post.mapper.PostMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
@Component
@RequiredArgsConstructor
public class UpdateScoreTask {
private static final int BATCH_SIZE = 100;
// 每半小时更新热度
private static final long POST_UPDATE_INTERVAL = 30 * 60;
private static final long COMMENT_UPDATE_INTERVAL = 30 * 60;
private final PostSelector postSelector;
private final PostMapper postMapper;
private final CommentMapper commentMapper;
private final RedisUtil redisUtil;
// 定时更新帖子热度分数
@Scheduled(fixedRate = POST_UPDATE_INTERVAL * 1000)
public void updatePostScores() {
// 扩大更新范围到最近30天的帖子
LocalDateTime startTime = LocalDateTime.now().minusDays(30);
LambdaQueryWrapper<Post> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.ge(Post::getCreateTime, startTime);
long totalPosts = postMapper.selectCount(queryWrapper);
long batches = (totalPosts + BATCH_SIZE - 1) / BATCH_SIZE;
for (int i = 0; i < batches; i++) {
queryWrapper.last("LIMIT " + i * BATCH_SIZE + ", " + BATCH_SIZE);
List<Post> posts = postMapper.selectList(queryWrapper);
posts.forEach(post -> postSelector.updatePostScore(post.getId()));
}
}
// 定时更新评论热度分数
@Scheduled(fixedRate = COMMENT_UPDATE_INTERVAL * 1000)
public void updateCommentScores() {
// 扩大更新范围到最近15天的评论
LocalDateTime startTime = LocalDateTime.now().minusDays(15);
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.ge(Comment::getCreateTime, startTime);
long totalComments = commentMapper.selectCount(queryWrapper);
long batches = (totalComments + BATCH_SIZE - 1) / BATCH_SIZE;
for (int i = 0; i < batches; i++) {
queryWrapper.last("LIMIT " + i * BATCH_SIZE + ", " + BATCH_SIZE);
List<Comment> comments = commentMapper.selectList(queryWrapper);
comments.forEach(comment -> postSelector.updateCommentScore(comment.getId()));
}
}
// 定期清理过期的缓存数据
@Scheduled(cron = "0 0 4 * * ?") // 每天凌晨4点执行
public void cleanExpiredData() {
LocalDateTime expiredTime = LocalDateTime.now().minusDays(60);
// 清理过期的帖子缓存
LambdaQueryWrapper<Post> postQueryWrapper = new LambdaQueryWrapper<>();
postQueryWrapper.le(Post::getCreateTime, expiredTime);
List<Post> expiredPosts = postMapper.selectList(postQueryWrapper);
expiredPosts.forEach(post -> {
redisUtil.zRemove("post:hot:", post.getId());
redisUtil.zRemove("post:time:", post.getId());
});
// 清理过期的评论缓存
LambdaQueryWrapper<Comment> commentQueryWrapper = new LambdaQueryWrapper<>();
commentQueryWrapper.le(Comment::getCreateTime, expiredTime);
List<Comment> expiredComments = commentMapper.selectList(commentQueryWrapper);
expiredComments.forEach(comment -> {
// 评论相关缓存清理
});
}
}

@ -1,36 +1,36 @@
###本地开发环境 ##本地开发环境
# lj:
# db:
# host: localhost
# password: 123456
# redis:
# host: localhost
# port: 6379
# password: 123456
# rabbitmq:
# host: localhost
# port: 5672
# username: root
# password: 123456
# minio:
# endpoint: http://localhost:9000
# accessKey: root
# secretKey: 12345678
lj: lj:
db: db:
host: 192.168.125.128 host: 192.168.59.129
password: MySQL@5678 password: Forely123!
redis: redis:
host: 192.168.125.128 host: 192.168.59.129
port: 6379 port: 6379
password: Redis@9012 password: Forely123!
rabbitmq: rabbitmq:
host: 192.168.125.128 host: 192.168.59.129
port: 5672 port: 5672
username: rabbit_admin username: admin
password: Rabbit@3456 password: Forely123!
minio: minio:
endpoint: http://192.168.125.128:9000 endpoint: http://192.168.59.129:9000
accessKey: minio_admin accessKey: forely
secretKey: Minio@1234 secretKey: Forely123!
#lj:
# db:
# host: 192.168.125.128
# password: MySQL@5678
# redis:
# host: 192.168.125.128
# port: 6379
# password: Redis@9012
# rabbitmq:
# host: 192.168.125.128
# port: 5672
# username: rabbit_admin
# password: Rabbit@3456
# minio:
# endpoint: http://192.168.125.128:9000
# accessKey: minio_admin
# secretKey: Minio@1234

@ -106,6 +106,7 @@ CREATE TABLE `comment` (
`user_id` BIGINT NOT NULL COMMENT '评论用户ID', `user_id` BIGINT NOT NULL COMMENT '评论用户ID',
`post_id` BIGINT NOT NULL COMMENT '关联的帖子ID', `post_id` BIGINT NOT NULL COMMENT '关联的帖子ID',
`parent_comment_id` BIGINT DEFAULT 0 COMMENT '父评论ID', `parent_comment_id` BIGINT DEFAULT 0 COMMENT '父评论ID',
`reply_user_id` BIGINT DEFAULT 0 COMMENT '回复用户ID',
`top_id` BIGINT COMMENT '顶层评论ID', `top_id` BIGINT COMMENT '顶层评论ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

Loading…
Cancel
Save