文章目录
- day09-MongoDB
- 一、回顾
- 1.1. 行为实战核心要点说明
- 二、评论系统
- 2.1 MongoDB
- 2.1.1 MongoDB简介
- ①简介
- ②体系结构与术语
- 2.1.2 安装与连接
- 2.1.3 Springboot整合MongoDB
- ①引入依赖
- ②添加服务端配置
- ③准备实体类
- ④测试-新增
- ⑤测试-查询
- ⑥测试-更新
- 测试-删除
- 2.2 app端评论-发表评论
- 2.2.1 需求分析
- ①需求分析
- ②对应数据存储结果-集合
- 2.2.2 接口定义
- ①实现步骤
- ②用户远程接口-查询用户-接口定义
- ③长整型数据精度丢失问题
- 2.3 app端评论-点赞评论
- 2.3.1 需求分析
- 2.3.2 思路分析
- 2.3.3 接口定义
- 2.3.4 接口实现
- 2.4 app端评论-评论列表
- 2.4.1 需求分析
- 2.4.2 接口定义
- 2.4.3 需求分析
- 2.4.4 接口实现
- 2.5 app端评论回复-发表回复、点赞回复、回复列表
- 2.6 热点评论
- 2.6.1 需求分析
- 实现思路-计算热点评论
day09-MongoDB
一、回顾
1.1. 行为实战核心要点说明
- 技术方案
Redis+MySQL
Redis:负责对外提供读与写,因为行为对读写的性能很高,不能直接去操作MySQL
MySQL:基于MQ实现异步数据同步,(不使用线程池的原因是因为Threadpool是基于本地内存的,不能把大量的数据放在线程池里,数据量过多),Redis操作完数据,把更改的数据同步到MySQL的行为表中 - 各行为接口
- user服务
- 关注与取消关注接口
- Redis设计:
- ZSET——关注列表
- key:behavior:follow:list:当前用户ID
- score:关注的时间
- value:作者的用户ID
- ZSET——粉丝列表
- key:behavior:follow:list:当前用户ID
- score:被关注的时间
- value:粉丝的用户ID
- ZSET——关注列表
- article服务
- 收藏与取消收藏接口
- Redis设计-HASH类型
- key:behavior:coll:当前用户ID
- value:
- key:文章ID
- value:文章详情
- 文章行为关系数据查询接口
- behavior服务
- 点赞与取消点赞接口
- Redis设计-HASH类型
- key:behavior:likes:文章ID
- value:
- key:用户ID
- value:操作的详情
- 阅读接口
- 不喜欢与取消不喜欢接口
- user服务
二、评论系统
2.1 MongoDB
2.1.1 MongoDB简介
①简介
端口号:27017
默认不支持事务
MongoDB是一个开源、高性能、无模式的文档型数据库
是NoSQL数据库产品中的一种,是最像关系型数据库(MySQL)的非关系性数据库
- 数据存储量较大,甚至巨大
- 对数据读写的响应速度要求非常高
- 某些数据安全性要求不高,可以接收一定范围内的误差
- 数据具有结构型(BSON)
应用场景:
评论、弹幕、观众列表中的一条数据
②体系结构与术语
2.1.2 安装与连接
启动MongoDB
docker start mongo-service
mongoDB连接工具——studio3t安装
studio3t是MongoDB优秀的客户端工具。官方地址在https://studio3t.com/
2.1.3 Springboot整合MongoDB
①引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
②添加服务端配置
server:
port: 9998
spring:
data:
mongodb:
host: 192.168.200.130
port: 27017
database: leadnews-comment
③准备实体类
/**
* APP评论信息
*/
@Data
// 使用这个注解来映射实体类和集合的关系
@Document("ap_comment")
public class ApComment {
/**
* id
*/
private String id;
/**
* 用户ID
*/
private Integer userId;
/**
* 用户昵称
*/
private String userName;
/**
* 文章id或动态id
*/
private Long objectId;
/**
* 频道ID
*/
private Integer channelId;
/**
* 评论内容类型
* 0 文章
* 1 动态
*/
private Integer type;
/**
* 评论内容
*/
private String content;
/**
* 作者头像
*/
private String image;
/**
* 点赞数
*/
private Integer likes;
/**
* 回复数
*/
private Integer reply;
/**
* 文章标记
* 0 普通评论
* 1 热点评论
* 2 推荐评论
* 3 置顶评论
* 4 精品评论
* 5 大V 评论
*/
private Integer flag;
/**
* 经度
*/
private BigDecimal longitude;
/**
* 维度
*/
private BigDecimal latitude;
/**
* 地理位置
*/
private String address;
/**
* 评论排列序号
*/
private Integer ord;
/**
* 创建时间
*/
private Date createdTime;
/**
* 更新时间
*/
private Date updatedTime;
}
④测试-新增
package com.itheima.mongo.test;
import com.itheima.mongo.pojo.ApComment;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Date;
/**
* @author tp
* @since 2024/2/19 13:24
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class MongoTest {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 测试新文档
*/
@Test
public void testAddDocument() {
for (int i = 1; i <= 10; i++) {
ApComment apComment = new ApComment();
apComment.setId(String.valueOf(i));// 评论id
apComment.setUserId(i);// 评论用户id
apComment.setUserName("测试用户"+i);// 评论用户名
apComment.setType(0);// 内容类型,0表示文章类型
apComment.setObjectId(Long.valueOf(10+i));//文章ID
if (i % 2 == 0) {
apComment.setFlag(1);// 热点评论
} else {
apComment.setFlag(0);// 普通评论
}
apComment.setContent("测试内容"+i);// 评论内容
apComment.setLikes(100+i);// 点赞数
apComment.setReply(0);// 回复数
apComment.setCreatedTime(new Date());// 创建时间
apComment.setUpdatedTime(new Date());// 创建时间
// mongoTemplate.insert(apComment);// 仅表示新增文档
mongoTemplate.save(apComment);// 表示新增文档或跟新文档
}
}
}
在测试的时候发现,MongoDB可以不创建数据库,可以不创建表,因为在运行的时候,会先读取配置的里database: leadnews-comment的值,发现数据库里没有这个数据库创建,同样的,读取@Document("ap_comment")里的值,发现没有这个集合就创建了这个集合。
⑤测试-查询
/**
* 测试查询
*/
@Test
public void testQueryDocument() {
System.out.println(mongoTemplate.findById("7", ApComment.class));
Query query = Query.query(Criteria.where("userName").is("测试用户7"));
System.out.println("----------------------------------------------");
// 查询一条数据
ApComment one = mongoTemplate.findOne(query, ApComment.class);
System.out.println(one);
System.out.println("-----------------------------------------------");
// 查询的多条
List<ApComment> all = mongoTemplate.findAll(ApComment.class);
all.forEach(x-> System.out.println(x));
System.out.println("-----------------------------------------------");
// 查询列表数据:单条件条件查询
List<ApComment> flag = mongoTemplate.find(Query.query(Criteria.where("flag").is(1)), ApComment.class);
flag.forEach(x-> System.out.println(x));
System.out.println("-----------------------------------------------");
// 查询列表数据:多条件条件查询
List<ApComment> flag1 = mongoTemplate.find(Query.query(Criteria.where("flag").is(1).and("likes").gt(102)), ApComment.class);
flag1.forEach(x-> System.out.println(x));
System.out.println("-----------------------------------------------");
//查询列表数据:根据域进行排序和限制查询条数
List<ApComment> apComments = mongoTemplate.find(Query.query(Criteria.where("flag").is(1).and("likes").gt(102)).with(Sort.by(Sort.Direction.DESC, "likes")).limit(3), ApComment.class);
apComments.forEach(x-> System.out.println(x));
}
⑥测试-更新
前两种常用,后一种有数字增减,可用
/**
* 测试更新文档
*/
@Test
public void testUpdateDocument() {
ApComment byId = mongoTemplate.findById("7", ApComment.class);
byId.setContent("测试内容007");
// 1. save
mongoTemplate.save(byId);
// 2. updateFirst(非线程安全的方法)
mongoTemplate.updateFirst(Query.query(Criteria.where("userId").is(7)), Update.update("content", "修改的测试内容007"), ApComment.class);
// 3. findAndModify(线程安全的方法)
mongoTemplate.findAndModify(Query.query(Criteria.where("userId").is(7)), Update.update("content", "修改的测试内容007"), ApComment.class);
}
测试-删除
/**
* 测试删除文档
*/
@Test
public void testDeleteDocument() {
// 1. 查询并删除
// ApComment apComment = mongoTemplate.findById("7", ApComment.class);
// mongoTemplate.remove(apComment);
// 2. 根据条件删除
mongoTemplate.remove(Query.query(Criteria.where("flag").is(1)), ApComment.class);
}
2.2 app端评论-发表评论
2.2.1 需求分析
①需求分析
- 文章详情页下方可以查看评论信息,按照点赞数量倒序排列,展示评论内容、评论的作者、点赞数、回复数、时间,默认查看10条评论,如果向查看更多,可以点击加载更多进行分页
- 可以针对当前文章发布评论
- 可以针对于某一条评论进行点赞操作
②对应数据存储结果-集合
APP评论信息
APP评论信息点赞
这两个集合是一对多的关系,表示一条评论可以让多个app用户点赞
2.2.2 接口定义
①实现步骤
1、搭建评论微服务
(1)创建项目heima-leadnews-comment
(2)bootstrap.yml
其中自动配置项去除了关于数据源的配置,因为这个项目不需要查询数据库,查询的mongodb
(3)nacos中添加comment的配置
spring:
data:
mongodb:
host: 192.168.200.130
port: 27017
database: leadnews-comment
(4)启动类
@SpringBootApplication
@EnableDiscoveryClient
public class CommentApplication {
public static void main(String[] args) {
SpringApplication.run(CommentApplication.class,args);
}
}
(5)添加WebMvcConfig
package com.heima.comment.config;
import com.heima.common.interceptor.TokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenInterceptor()).addPathPatterns("/**")
//放行swagger和knife4j
.excludePathPatterns( "/v2/api-docs",
"/doc.html",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/webjars/**",
"/actuator/**");
}
}
(6)接口定义
controller
@RestController
@RequestMapping("/api/v1/comment")
public class ApCommentController {
/**
* 新增评论
* @return
*/
@PostMapping("/save")
public ResponseResult save(@RequestBody CommentSaveDto dto) {
return null;
}
}
service
public interface ApCommentService{
/**
* 新增评论
* @param dto
* @return
*/
ResponseResult save(CommentSaveDto dto);
}
2、实现思路
判断用户是否存在
判断文章是否存在
判断评论内容是否大于140字
安全过滤
保存评论
serviceImpl
@Service
public class ApCommentServiceImpl implements ApCommentService {
@Autowired
private IUserClient userClient;
@Autowired
private IArticleClient articleClient;
@Autowired
private MongoTemplate mongoTemplate;
@Override
public ResponseResult save(CommentSaveDto dto) {
// 1. 判断参数是否为空
if (dto.getArticleId()==null || StringUtils.isBlank(dto.getContent())){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
// 2. 判断用户是否存在——调用user的feign接口(此feign接口一定要在user服务的webMvcConfig中放行)
Integer userId = ThreadLocalUtil.getUserId();
ApUser apUser = userClient.findOne(userId);
if (apUser == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"APP用户不存在");
}
// 3. 判断文章是否存在
ApArticle apArticle = articleClient.findOne(dto.getArticleId());
if (apArticle == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"文章不存在");
}
// 4. 判断评论内容是否大于140字
if (dto.getContent().length() > 140) {
return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR,"内容长度超过140字");
}
// 5. 安全过滤(DFA审核和百度云文本审核)
// 6. 保存评论到MongoDB中
ApComment apComment = new ApComment();
apComment.setUserId(userId);
apComment.setUserName(apUser.getName());
apComment.setImage(apUser.getImage());
apComment.setType(0); //评论的内容类型,0表示文章
apComment.setObjectId(dto.getArticleId());
apComment.setContent(dto.getContent());
apComment.setLikes(0);
apComment.setReply(0);
apComment.setFlag(0);
apComment.setCreatedTime(new Date());
apComment.setUpdatedTime(new Date());
mongoTemplate.save(apComment);
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
}
3、配置网关
②用户远程接口-查询用户-接口定义
package com.heima.apis.user;
import com.heima.model.user.pojos.ApUser;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @author tp
* @since 2024/2/19 16:17
*/
@FeignClient("leadnews-user")
public interface IUserClient {
/**
* 根据id查询用户
* @param id
* @return
*/
@GetMapping("/api/v1/user/one/{id}")
public ApUser findOne(@PathVariable("id") Integer id);
}
ApUserFeign
@FeignClient("leadnews-user")
public interface IUserClient {
/**
* 根据id查询用户
* @param id
* @return
*/
@GetMapping("/api/v1/user/one/{id}")
public ApUser findOne(@PathVariable("id") Integer id);
}
③长整型数据精度丢失问题
前端的问题
一旦遇到服务端响应的数据是长整型的,永远会把后两位,或者后三位变为0,所以服务端根本就不能返回长整型给前端
解决方法:
方案一:将文章的id的由long类型手动改为String类型,可以解决此问题。(需要修改表结构)pass掉
方案二:可以使用jackson进行序列化和反序列化解决
2.3 app端评论-点赞评论
2.3.1 需求分析
- 用户点赞,可以增加点赞数量,点赞后不仅仅要增加点赞数,需要记录当前用户对于当前评论的数据记录
- 用户取消点赞,点赞减一,更新点赞数据
2.3.2 思路分析
2.3.3 接口定义
2.3.4 接口实现
package com.heima.comment.service.impl;
import com.heima.apis.article.IArticleClient;
import com.heima.apis.user.IUserClient;
import com.heima.comment.service.ApCommentService;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.comment.dtos.CommentDto;
import com.heima.model.comment.dtos.CommentLikeDto;
import com.heima.model.comment.dtos.CommentSaveDto;
import com.heima.model.comment.pojos.ApComment;
import com.heima.model.comment.pojos.ApCommentLike;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.user.pojos.ApUser;
import com.heima.utils.common.ThreadLocalUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author tp
* @since 2024/2/19 16:06
*/
@Service
public class ApCommentServiceImpl implements ApCommentService {
@Autowired
private IUserClient userClient;
@Autowired
private IArticleClient articleClient;
@Autowired
private MongoTemplate mongoTemplate;
@Override
public ResponseResult save(CommentSaveDto dto) {
// 1. 判断参数是否为空
if (dto.getArticleId()==null || StringUtils.isBlank(dto.getContent())){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
// 2. 判断用户是否存在——调用user的feign接口(此feign接口一定要在user服务的webMvcConfig中放行)
Integer userId = ThreadLocalUtil.getUserId();
ApUser apUser = userClient.findOne(userId);
if (apUser == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"APP用户不存在");
}
// 3. 判断文章是否存在
ApArticle apArticle = articleClient.findOne(dto.getArticleId());
if (apArticle == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"文章不存在");
}
// 4. 判断评论内容是否大于140字
if (dto.getContent().length() > 140) {
return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR,"内容长度超过140字");
}
// 5. 安全过滤(DFA审核和百度云文本审核)
// 6. 保存评论到MongoDB中
ApComment apComment = new ApComment();
apComment.setUserId(userId);
apComment.setUserName(apUser.getName());
apComment.setImage(apUser.getImage());
apComment.setType(0); //评论的内容类型,0表示文章
apComment.setObjectId(dto.getArticleId());
apComment.setContent(dto.getContent());
apComment.setLikes(0);
apComment.setReply(0);
apComment.setFlag(0);
apComment.setCreatedTime(new Date());
apComment.setUpdatedTime(new Date());
mongoTemplate.save(apComment);
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
/**
* 加载评论
* @param dto
* @return
*/
@Override
public ResponseResult load(CommentDto dto) {
return null;
}
/**
* 点赞评论或者取消点赞
* @param dto
* @return
*/
@Override
public ResponseResult like(CommentLikeDto dto) {
// 1. 判断参数是否为空
if (StringUtils.isBlank(dto.getCommentId()) || dto.getOperation() == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
// 2. 判断评论是否存在
ApComment apComment = mongoTemplate.findById(dto.getCommentId(), ApComment.class);
if (apComment == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"评论不存在");
}
// 3. 查询评论对应的点赞记录
// 根据评论id查询点赞记录
ApCommentLike apCommentLike = mongoTemplate.findOne(Query.query(Criteria.where("userId").is(ThreadLocalUtil.getUserId()).and("commentId").is(dto.getCommentId())), ApCommentLike.class);
// 3.1 点赞记录不存在如何处理?
if (apCommentLike == null) {
// 就是说还没有点过赞
// 3.1.1 判断是否取消点赞
if (dto.getOperation() == 1) {
return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR,"点赞记录不存在,无法取消点赞");
}
// 3.1.2 更新评论的点赞数+1
ApComment comment = mongoTemplate.findAndModify(Query.query(Criteria.where("id").is(dto.getCommentId())), new Update().inc("likes", 1).set("updateTime", new Date()), ApComment.class);
// 3.1.3 保存点赞记录
apCommentLike = new ApCommentLike();
apCommentLike.setCommentId(dto.getCommentId());
apCommentLike.setUserId(ThreadLocalUtil.getUserId());
apCommentLike.setOperation(dto.getOperation());
apCommentLike.setCreatedTime(new Date());
apCommentLike.setUpdatedTime(new Date());
mongoTemplate.save(apCommentLike);
} else {
// 3.2 点赞记录存在如何处理?
// 3.2.1 判断是否重复点赞
if (dto.getOperation() == 0 && apCommentLike.getOperation() == 0) {
return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR,"不能重复点赞");
}
// 3.2.2 判断是否重复取消点赞
if (dto.getOperation() == 1 && apCommentLike.getOperation() == 1) {
return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR,"不能重复取消点赞");
}
// 3.2.3 如果操作的是点赞,则点赞数+1
if (dto.getOperation() == 0) {
mongoTemplate.findAndModify(Query.query(Criteria.where("id").is(dto.getCommentId())), new Update().inc("likes", 1).set("updateTime", new Date()), ApComment.class);
} else {
// 3.2.4 如果操作的是取消点赞,则点赞数-1
mongoTemplate.findAndModify(Query.query(Criteria.where("id").is(dto.getCommentId())), new Update().inc("likes", -1).set("updateTime", new Date()), ApComment.class);
}
// 3.2.5 更新点赞记录的操作类型和时间
apCommentLike.setOperation(dto.getOperation());
apCommentLike.setUpdatedTime(new Date());
mongoTemplate.save(apCommentLike);
}
// 4.查询最新点赞数并返回
apComment = mongoTemplate.findById(dto.getCommentId(),ApComment.class);
Map result = new HashMap();
result.put("likes", apComment.getLikes());
return ResponseResult.okResult(result);
}
}
2.4 app端评论-评论列表
2.4.1 需求分析
查询评论列表,根据当前文章id进行检索,按照创建时间倒序,分页查询(默认10条数据)
2.4.2 接口定义
2.4.3 需求分析
2.4.4 接口实现
public ResponseResult load(CommentDto dto) {
int size = 10; // 默认查询数
// 1. 判断参数是否为空
if (dto.getArticleId() == null || dto.getMinDate() == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
// 2. 根据条件查询评论列表(查询条件:type、objectId、createdTime 查询结果:createdTime倒序、限制查询10条)
Query query = Query.query(Criteria
.where("type").is(0)
.and("objectId").is(dto.getArticleId())
.and("createdTime").lt(dto.getMinDate()))
.with(Sort.by(Sort.Direction.DESC, "createdTime"))
.limit(size);
List<ApComment> apCommentList = mongoTemplate.find(query, ApComment.class);
// 3. 如果当前用户是游客,直接响应评论列表数据
Integer userId = ThreadLocalUtil.getUserId();
if (userId == 0) {
return ResponseResult.okResult(apCommentList);
}
// 4. 如果当前用户是正常用户,需要标识评论列表中被当前用户点赞过的评论,再响应
// 4.1 获取评论列表对应的评论ID列表
List<String> commentIdList = apCommentList.stream().map(ApComment::getId).collect(Collectors.toList());
// 4.2 查询当前用户针对当前评论列表对应的所有点赞记录(查询条件:userId,operation,commentId)
Query queryCommentLike = query = Query.query(Criteria.where("userId").is(userId).and("operation").is(0).and("commentId").in(commentIdList));
// 点赞记录列表
List<ApCommentLike> apCommentLikes = mongoTemplate.find(queryCommentLike, ApCommentLike.class);
// 4.3 在评论列表中找到被点赞过的评论并添加标识表示点赞过
List<CommentVo> commentVoList = new ArrayList<>();
for (ApComment apComment : apCommentList) {
long count = apCommentLikes.stream().filter(x -> x.getCommentId().equals(apComment.getId())).count();
CommentVo commentVo = new CommentVo();
BeanUtils.copyProperties(apComment,commentVo);
if (count > 0) {
commentVo.setOperation(0);
}
commentVoList.add(commentVo);
}
return ResponseResult.okResult(commentVoList);
}
2.5 app端评论回复-发表回复、点赞回复、回复列表
2.6 热点评论
2.6.1 需求分析
- 一个文章最多有5条热点评论
- 热点评论需要按照点赞数倒序排序
- 前5条评论是按照点赞数倒序,其他按照时间倒序查询