为了方便说明,首先定义2个概念:
评论:用户针对某个文章发表的评论。
回复:用户针对某个评论发表的评论。
我们先分析评论和回复的需要属性。
评论的基本属性
评论的id
评论的用户id
评论内容
评论的点赞数
评论所属的文章id
最新回复
回复的数量
回复的基本属性
回复的id
回复的用户id
回复的内容
回复的点赞数
回复的目标评论id
回复的目标用户id
顶级评论的id
数据库
通过上述属性的列举,我们发现,评论和回复明显可以共用一张表!
CREATE TABLE IF NOT EXISTS `interaction_reply` (
`id` bigint NOT NULL COMMENT '文章的评论id',
`user_id` bigint NOT NULL COMMENT '评论者的用户id',
`article_id` bigint NOT NULL COMMENT '评论的所属文章id',
`answer_id` bigint NOT NULL COMMENT '评论的所属顶级评论id',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '评论内容',
`target_user_id` bigint DEFAULT '0' COMMENT '回复的目标用户id',
`target_reply_id` bigint DEFAULT '0' COMMENT '回复的目标评论id',
`reply_times` int NOT NULL DEFAULT '0' COMMENT '回复数量',
`liked_times` int NOT NULL DEFAULT '0' COMMENT '点赞数量',
`hidden` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否被隐藏,默认false',
`anonymity` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否匿名,默认false',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_article_id` (`article_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='评论';
相关说明
-
顶级评论:顶级评论是所有评论的评论等的最上级的评论。在这个评论下方无论是评论的评论还是评论都会累计到顶级评论的评论数。
-
是否被隐藏:后期如果有评论有一些违禁词等其他原因,我们可以在后台系统控制该评论的显示。用户端只会查询出没有被隐藏的评论。
数据库另一个方案
三张数据表,也即三级评论表实现,一级评论表存储评论对象下的评论,二级评论表存储一级评论的回复,三级评论表存储二级评论和其他三级评论的回复。
1.评论列表查询:一级评论列表,一级评论对应的所有评论列表(不用树状结构展示,三级评论通过@方式展示);
2.查看对话:当三级评论回复二级评论时,三级评论旁会有“查看对话选项”,点击查看对话会展示该二级评论下所有的@回复(不仅仅是@该二级评论的,还包括@该二级评论下的其他三级评论的回复)。
再说明:仅一级评论记录回复数,二三级评论不记录回复数,因为如果二三级评论也要记录回复数,那么如果是一个很深层次的评论回复,就需要层层向上找回复的评论并使其回复数+1,这样不太可取,b站好像也没有实现。
相关实现简述
1.评论列表查询
先根据target_id查询去一级评论表查询一级评论列表并分页,对每个一级评论,根据其id再去二级评论表和三级评论表(二三级评论都填充到一个集合中,这样才能实现不以树状形式展示)以one_comment_id = id为条件查询并分页。
对于三级评论记录,还要根据其reply_user_id回复用户ID字段去查询对应用户的昵称,前端展示时如果是三级评论就展示@[对应回复用户昵称],并展示“查看对话”选项。
2.三级评论查看对话
三级评论(二级以下评论)会展示“查看对话”选项,点击查看对话,取出该三级评论对应的二级评论id也即two_comment_id,去二级评论表中查询以id=two_comment_id为条件该评论信息,并去三级评论表中以two_comment_id=所给two_comment_id为条件查询该二级评论下的所有三级评论,也即该二级评论下的所有对话记录,并且三级评论还要填充@[回复用户昵称]。
【如果是三级评论就展示@[对应回复用户昵称],并展示“查看对话”选项】那么如何前端判断某评论是否是三级评论呢,可以取出其two_comment_id值,如果!=null就是三级评论,就展示@回复用户昵称和查看对话,当然也可以给哥级评论表添加个记录level,三级评论level=3,前端去除level判断是否为3即可。
实体类
package com.zd.domain;
import lombok.Data;
import org.jetbrains.annotations.NotNull;
@Data
public class ReplyDTO {
@NotNull
private String content;
private Boolean anonymity;
private Long answerId;//评论的顶级回复id
private Long articleId;
//回复的目标评论id
private Long targetReplyId;
//回复的目标用户id
private Long targetUserId;
}
package com.zd.domain;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ReplyVO {
//评论id
private Long id;
private String content;
private Boolean anonymity;
private Boolean hidden;
private Integer replyTimes;
private LocalDateTime createTime;
private Long answerId;//评论的顶级回复id
private Long userId;
private String userName;
private String userIcon;
private Integer userType;
private Boolean liked;//该用户是否已经点赞了
private Integer likedTimes;
private String targetUserName;
}
新增评论代码实现
controller层:
@ApiOperation("评论")
@PostMapping("/comment")
public void comment(@Validated ReplyDTO dto) {
replyService.reply(dto);
}
注意:@Valicated 注解可以帮助我们判断传过来的dto里面部分参数是否合理。无需我们再进行ifelse校验!
实现类:
@Override
public void reply(ReplyDTO dto) {
//todo 此处应该动态获取用户id
Long userId = 2L;
InteractionReply reply = new InteractionReply();
reply.setContent(dto.getContent());
reply.setArticleId(dto.getArticleId());
reply.setAnswerId(dto.getAnswerId());
reply.setAnonymity(dto.getAnonymity());
reply.setTargetReplyId(dto.getTargetReplyId());
reply.setTargetUserId(dto.getTargetUserId());
reply.setUserId(userId);
this.save(reply);
if(dto.getAnswerId()!=null){
//如果顶级评论id不为空 则这是回复 累加评论的回复次数
InteractionReply deep = this.getById(dto.getAnswerId());
InteractionReply target = this.getById(dto.getTargetReplyId());
if(deep==null){
try {
throw new Exception("主评论不存在!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
if(target==null){
try {
throw new Exception("你要回复的评论不存在!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
deep.setReplyTimes(deep.getReplyTimes()+1);
this.updateById(deep);
target.setReplyTimes(target.getReplyTimes()+1);
this.updateById(target);
}
//todo 更新文章的最新评论 同时累加文章表的评论次数
// article.setAnswerTimes(article.getAnswerTimes()+1);
// article.setLatestAnswerId(reply.getId());
// todo 更新文章
// replyMapper.updateById(article);
log.info("成功发表了评论!!!");
}
配置一下swagger和拦截器。
package com.zd.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
1. swagger配置类
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
//是否开启 (true 开启 false隐藏。生产环境建议隐藏)
//.enable(false)
.select()
//扫描的路径包,设置basePackage会将包下的所有被@Api标记类的所有方法作为api
.apis(RequestHandlerSelectors.basePackage("com.zd.controller"))
//指定路径处理PathSelectors.any()代表所有的路径
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
//设置文档标题(API名称)
.title("测试")
//文档描述
.description("接口说明")
//服务条款URL
.termsOfServiceUrl("http://localhost:8080/")
//版本号
.version("1.0.0")
.build();
}
}
package com.zd.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
/**
* @author Andon
* 2021/12/29
*/
@Configuration
public class WebMvcConfigurer extends WebMvcConfigurationSupport {
/**
* 发现如果继承了WebMvcConfigurationSupport,则在yml中配置的相关内容会失效。 需要重新指定静态资源
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(
"classpath:/static/");
registry.addResourceHandler("swagger-ui.html", "doc.html").addResourceLocations(
"classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations(
"classpath:/META-INF/resources/webjars/");
super.addResourceHandlers(registry);
}
}
启动后,我们通过后端swagger接口调用来辅助调试:
对照一下数据库的数据:
我们发现我们是成功的。
分页查询评论代码实现
首先,我们需要引入一个mybatisplus分页插件:
@Configuration
public class MybatisPlusPageInterceptor {
/**
* mybatis-plus分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor page = new PaginationInterceptor();
return page;
}
}
一开始本来我想写个query类,但是我觉得用mybatisplus自带的分页查询就足矣了。
controller层:
@ApiOperation("评论查询")
@GetMapping("/comment/{pageSize}/{pageNum}")
public IPage comment(@PathVariable("pageNum") long current,@PathVariable("pageSize") long size) {
return replyService.pageComment(current,size);
}
服务实现类:
@Override
public IPage pageComment(long current, long size) {
return lambdaQuery()
.eq(InteractionReply::getHidden,false)
.orderByDesc(InteractionReply::getUpdateTime)
.page(new Page(current,size));
}
注意:此处默认查询非隐藏状态的评论!并且按照更新时间倒序排序!
ps:如果有人回复了评论,则该评论的更新时间就会被更新到当前时间,所以按照更新时间查询,查询到的都是刚刚被回复/或者刚创建的评论。
启动项目,用swagger查一下:
查询一下:
我们发现,的确评论是按照更新时间排序的。
当然,此处后期可以通过封装单独的返回类,只让部分信息返回给前端。
参考文档
评论设计实现,完全模范bilibili实现评论功能,表的设计、sql如何查找,以及前端的一些设计_用户评论表的设计-CSDN博客
https://blog.csdn.net/m0_62116982/article/details/132126170