学习目标
- 能够完成mongodb的环境搭建
- 能够掌握mongodb的基本使用
- 能够完成app端评论的查询、发表、点赞等功能
- 能够完成app端评论回复的查询,发表、点赞功能
Mongodb
Mongodb简介
mysql mongodb Redis
MongoDB是一个开源、高性能、无模式的文档型数据库
应用场景:
- 支持文本查询
- 不需要支持事务,不存在复杂的多表查询,不支持join操作
- 存储数据可持久化
- 需要TB甚至 PB 级别数据存储
- 需求变化较快,数据模型无法确认,预计使用快速迭代开发形式
- 需要至少2000以上的读写QPS【高性能】
- 能支持快速水平扩展【高扩展】
- 99.999%高可用【高可用】
- 适合于数据价值低的数据存储
小结:mongo的特点 1、模式自由 ---- 不需要提前创建表 直接放数据就可以
2、支持高并发 2000以上
3、搭建集群比较方便
4、支持海量数据存储
MongoDB体系结构
MongoDB 的逻辑结构是一种层次结构。主要由:文档(document)、集合(collection)、数据库(database)这三部分组成的。逻辑结构是面向用户的,用户使用 MongoDB 开发应用程序使用的就是逻辑结构。
- MongoDB 的文档(document),相当于关系数据库中的一行记录。
- 多个文档组成一个集合(collection),相当于关系数据库的表。
- 多个集合(collection),逻辑上组织在一起,就是数据库(database)。
- 一个 MongoDB 实例支持多个数据库(database)。
文档(document)、集合(collection)、数据库(database)的层次结构如下图:
SQL术语/概念 | MongoDB术语/概念 | 解释/说明 |
database | database | 数据库 |
table | collection | 数据库表/集合 |
row | document | 数据记录行/文档 |
column | field | 数据字段/域 |
index | index | 索引 |
table joins | 表连接,MongoDB不支持 | |
primary key | primary key | 主键,MongoDB自动将_id字段设置为主键 |
MongoDB数据类型
数据类型 | 描述 |
String | 字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的。 |
Integer | 整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位。 |
Boolean | 布尔值。用于存储布尔值(真/假)。 |
Double | 双精度浮点值。用于存储浮点值。 |
Min/Max keys | 将一个值与 BSON(二进制的 JSON)元素的最低值和最高值相对比。 |
Array | 用于将数组或列表或多个值存储为一个键。 |
Timestamp | 时间戳。记录文档修改或添加的具体时间。 |
Object | 用于内嵌文档。 |
Null | 用于创建空值。 |
Symbol | 符号。该数据类型基本上等同于字符串类型,但不同的是,它一般用于采用特殊符号类型的语言。 |
Date | 日期时间。用 UNIX 时间格式来存储当前日期或时间。你可以指定自己的日期时间:创建 Date 对象,传入年月日信息。 |
Object ID | 对象 ID。用于创建文档的 ID。 |
Binary Data | 二进制数据。用于存储二进制数据。 |
Code | 代码类型。用于在文档中存储 JavaScript 代码。 |
Regular expression | 正则表达式类型。用于存储正则表达式。 |
ObjectId
ObjectId 类似唯一主键,可以很快的去生成和排序,包含 12 bytes,含义是:
- 前 4 个字节表示创建 unix 时间戳,格林尼治时间 UTC 时间,比北京时间晚了 8 个小时
- 接下来的 3 个字节是机器标识码
- 紧接的两个字节由进程 id 组成 PID
- 最后三个字节是随机数
MongoDB 中存储的文档必须有一个 _id 键。这个键的值可以是任何类型的,默认是个 ObjectId 对象
MongoDB安装
cd /opt/hmtt/nosql
./close.sh
./start.sh
mongo客户端工具安装
安装后启动,连接MongoDB
MongoDB基本操作
数据库以及表的操作
// 查看数据库
show dbs
// 创建数据库
// use 数据库名称 如果数据库名称存在,那么切换到该数据库,不如不存在,那就新增数据库
use article
use student
use commentdb
// 查看当前所在数据库
db
// 删除数据库
// db.dropDatabase() 删除当前所在的数据库
db.dropDatabase()
// 操作集合
// 查看集合
show tables
// 新增集合
// db.createCollection(集合名)
db.createCollection("student")
// 删除集合
// db.集合名.drop()
db.student.drop()
新增数据
在MongoDB中,存储的文档结构是一种类似于json的结构,称之为bson(全称为:Binary JSON)。
// 新增文档
// db.集合名.insert(文档) json格式的文档
db.comment.insert({"name":"tom","age":18})
db.comment.insert({"_id":2,"name":"jerry","age":19})
// 查看所有文档
// db.集合名.find()
db.comment.find()
查询数据
MongoDB 查询数据的语法格式如下:
db.表名.find([query],[fields])
- query :可选,使用查询操作符指定查询条件
- fields :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需db.省略该参数即可(默认省略)。
如果你需要以易读的方式来读取数据,可以使用 prett() 方法,语法格式如下:
>db.表名.find().pretty()
pretty() 方法以格式化的方式来显示所有文档。
MongoDB 与 RDBMS Where 语句比较
操作 | 格式 | 范例 | RDBMS中的类似语句 |
等于 |
|
|
|
小于 |
|
|
|
小于或等于 |
|
|
|
大于 |
|
|
|
大于或等于 |
|
|
|
不等于 |
|
|
|
包含使用$in操作符
查询评论集合中userid字段包含101和102的文档:
db.comment.find({userId:{$in:["101","102"]}})
不包含使用$nin操作符
查询评论集合中userid字段不包含101和102的文档:
db.comment.find({userId:{$nin:["101","102"]}})
And 条件
db.col.find({key1:value1, key2:value2})
Or 条件
db.col.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
)
And 和 Or 联合使用
类似常规 SQL 语句为: 'where thumbUp>1000 AND (userId = '101' OR articleId = '001')'
db.col.find({"thumbUp": {$gt:50}, $or: [{"userId": "101"},{"articleId": "001"}]})
limit()方法
db.COLLECTION_NAME.find().limit(NUMBER)
skip() 方法
db.COLLECTION_NAME.find().limit(NUMBER).skip(NUMBER)
sort() 方法
db.COLLECTION_NAME.find().sort({KEY:1})
其中 1 为升序排列,而 -1 是用于降序排列。
db.comment.find().sort({"thumbUp":-1})
count() 方法
db.comment.count(query, options)
模糊查询
db.comment.find({content:/理/})
db.comment.find({content:/^理/})
db.comment.find({content:/理$/})
范例 | RDBMS中的类似语句 |
|
|
|
|
|
|
案例
db.comment.drop()
// 插入测试数据
db.comment.insert({_id:"1",articleId:"001",content:"说得很有道理",userId:"101",parentId:"0",publishDate:new Date(),thumbUp:1000});
db.comment.insert({_id:"2",articleId:"001",content:"楼主太有才了",userId:"102",parentId:"0",publishDate:new Date(),thumbUp:1100});
db.comment.insert({_id:"3",articleId:"001",content:"这个可以啊",userId:"103",parentId:"0",publishDate:new Date(),thumbUp:1200});
db.comment.insert({_id:"4",articleId:"002",content:"我怀疑你在开车,可我没有证据",userId:"102",parentId:"0",publishDate:new Date(),thumbUp:1300});
db.comment.insert({_id:"5",articleId:"002",content:"同上",userId:"104",parentId:"0",publishDate:new Date(),thumbUp:1400});
db.comment.insert({_id:"6",articleId:"002",content:"这个有内涵",userId:"101",parentId:"0",publishDate:new Date(),thumbUp:1500});
db.comment.insert({_id:"7",articleId:"003",content:"理论上可行",userId:"103",parentId:"0",publishDate:new Date(),thumbUp:1600});
// 条件查询
// db.集合名.find(query,projection) query-->json projection --> 指定返回值 1为返回 0 为不返回
db.comment.find({userId:"101"})
db.comment.find({userId:"101"},{articleId:1,content:1})
db.comment.find({userId:"101"},{articleId:1,content:1,_id:0})
// 多条件查询and 将多个条件写在json中
// {key1:value1,key2:value2}
db.comment.find({userId:"101",articleId:"001"})
// or 查询
// {$or:[{},{}]}
db.comment.find({$or:[{userId:"101"},{articleId:"002"}]})
// > >= < <= != gt gte lt lte ne
// {key:{$gt:value}}
db.comment.find({thumbUp:{$gt:1200}})
db.comment.find({thumbUp:{$gte:1200}})
db.comment.find({thumbUp:{$ne:1200}})
// 需要注意类型不同
db.comment.find({_id:{$lt:2}})
db.comment.find({_id:{$lt:"2"}})
// in nin
// {key:{$in:[value1,value2]}}
db.comment.find({userId:{$in:["101","102"]}})
// 模糊查询 支持正则表达式
// db.集合名.find({key:/value/}) 匹配开头 {key:/^value/} 匹配结尾 {key:/value$/}
db.comment.find({content:/理/})
db.comment.find({content:/^理/})
db.comment.find({content:/理$/})
// 统计数量
// db.集合名.count(json)
db.comment.count()
db.comment.count({userId:"101"})
//限制行数 limit
db.comment.find().limit(2)
// 跳过数据 skip
db.comment.find().skip(2)
db.comment.find().skip(2).limit(2)
// 排序
// db.集合名.find().sort({key:value}) value 为1时为升序 -1 为降序
db.comment.find().sort({thumbUp:1})
db.comment.find().sort({thumbUp:-1})
更新数据
update() 方法用于更新已存在的文档。语法格式如下:
db.表名.update(
<query>,
<update>,
[
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
]
)
参数说明:
- query : update的查询条件,类似sql update查询内where后面的。
- update : update的对象和一些更新的操作符(如$,$inc.$set)等,也可以理解为sql update查询内set后面的
- upsert : 可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。
- multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。
// 更新文档
// db.集合名.update(query,update,option) query --> json update --> json option {upsert,multi}
// upsert表示如果没有查到对应的记录,那么新增一项
// multi 表示更新多个查询到的记录,默认情况下只更新一条
// 文档替换
db.comment.update({_id:"5"},{content:"我认为可行"})
// 更新字段 {$set:{key:value}}
db.comment.update({_id:"5"},{$set:{userId:"103"}})
// 更新多个字段 {$set:{key1:value1,key2:value2}}
db.comment.update({_id:"5"},{$set:{articleId:"001",thumbUp:1400}})
db.comment.update({_id:"8"},{content:"我认为可行"},{upsert:1})
// 默认情况下只更新一条
db.comment.update({userId:"101"},{$set:{thumbUp:0}})
db.comment.update({userId:"101"},{$set:{thumbUp:1}},{multi:1})
// 值递增 $inc:{key:value} value指的是更新的步长
db.comment.update({userId:"101"},{$inc:{thumbUp:10}},{multi:1})
删除数据
通过remove()方法进行删除数据,语法如下:
db.表名.remove(
<query>,
[
justOne: <boolean>,
writeConcern: <document>
]
)
参数说明:
- query :(可选)删除的文档的条件。
- justOne : (可选)如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值 false,则删除所有匹配条件的文档。
- writeConcern :(可选)抛出异常的级别。
实例:
// 删除
// db.集合名.remove(json)
db.comment.remove({_id:"5"})
db.comment.remove({userId:"101"})
db.comment.remove({})
SpringDataMongoDB快速入门
创建项目
创建项目:mongodb-demo
pom文件:
<!-- 继承Spring boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.8.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
application.yml
spring:
data:
mongodb:
host: 192.168.85.143
port: 27017
database: leadnews-comment
引导类:
package com.heima.mongo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author syl
*/
@SpringBootApplication
public class MongoApp {
public static void main(String[] args) {
SpringApplication.run(MongoApp.class, args);
}
}
实体类准备
实体类准备,添加注解@Document
与mongodb中的集合对应上
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.math.BigDecimal;
import java.util.Date;
/**
* APP评论信息
*/
@Data
@Document("ap_comment")
public class ApComment {
/**
* id
*/
private String id;
/**
* 用户ID
*/
private Integer userId;
/**
* 用户昵称
*/
private String userName;
/**
* 文章id或动态id
*/
private Long entryId;
/**
* 评论内容
*/
private String content;
}
基本增删改查
可以使用MongoTemplate来操作
创建测试类:
package com.heima.mongo;
import com.heima.mongo.entity.ApComment;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
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;
@SpringBootTest
// 如果启动类所在的包名和当前测试类的包名不一致,需要手动指定classes到启动类
// @SpringBootTest(classes = MongoApp.class)
public class MongoTests {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 新增文档
*/
@Test
public void addDoc() {
ApComment comment = new ApComment();
comment.setId("123456");
comment.setUserId(10010);
comment.setUserName("火星联通");
comment.setContent("欢迎来到火星!!!");
mongoTemplate.save(comment);
}
/**
* 根据id查询文档
*/
@Test
public void getDocById(){
ApComment apComment = mongoTemplate.findById("123456", ApComment.class);
System.out.println(apComment);
}
/**
* 更新文档
*/
@Test
public void updateDoc(){
// 构建查询条件
Query query = new Query();
// 相当于sql select * from tb where id = '123456'
query.addCriteria(Criteria.where("_id").is("123456"));
Update update = new Update();
update.set("userId","10011");
UpdateResult result = mongoTemplate.updateFirst(query, update, ApComment.class);
System.out.println(result);
}
/**
* 删除文档
*/
@Test
public void delete(){
// 构建查询条件
Query query = new Query();
// 相当于sql select * from tb where id = '123456'
query.addCriteria(Criteria.where("_id").is("123456"));
DeleteResult result = mongoTemplate.remove(query, ApComment.class);
System.out.println(result);
}
}
app端评论-发表评论
需求分析
- 文章详情页下方可以查看评论信息,按照点赞数量倒序排列,展示评论内容、评论的作者、点赞数、回复数、时间,默认查看20条评论,如果想查看更多,可以点击加载更多进行分页
- 登录用户可以进行评论
- 可以针对当前文章发布评论
- 可以针对于某一条评论进行点赞操作
接口定义
说明 | |
接口路径 | /api/v1/comment/save |
请求方式 | POST |
参数 | CommentSaveDto |
响应结果 | ResponseResult |
思路分析
APP评论信息点赞
(2)思路分析
- 评论内容不为空且不超过140字
- 评论内容需要做文本反垃圾检测
- 评论内容需要做敏感词过滤
- 用户登录后可以对文章发表评论
- 评论数据修改较为频繁,并且数据库比较大,这里最终存储在mongo数据库中
功能实现
搭建评论微服务
(1)创建项目heima-leadnews-comment
(2)pom依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
(3) 启动类
package com.heima.comment;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author mcm
*/
@SpringBootApplication
public class CommentApp {
public static void main(String[] args) {
SpringApplication.run(CommentApp.class, args);
}
}
(4)application.yml
server:
port: 9007
spring:
application:
name: leadnews-comment
cloud:
nacos:
discovery:
server-addr: http://192.168.85.143:8848
data:
mongodb:
host: 192.168.85.143
port: 27017
database: leadnews_comment
配置
(1)需要使用阿里云安全、接口knife4j、通用异常
如下配置:
package com.heima.comment.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(value = {"com.heima.common.exception","com.heima.common.knife4j"})
public class InitConfig {
}
(2)获取当前登录用户信息
评论必须在登录的情况下才能发布,所以需要验证用户获取用户信息,添加拦截器:
package com.heima.comment.interceptor;
import com.heima.common.dto.User;
import com.heima.common.util.UserThreadLocalUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从请求头中获取userId和name
String userId = request.getHeader("userId");
String name = request.getHeader("name");
if(StringUtils.isNotBlank(userId)){
User user = new User(Integer.parseInt(userId),name);
UserThreadLocalUtil.set(user);
}
return true; //一直是放行
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserThreadLocalUtil.remove();
}
}
拦截器配置
package com.heima.comment.config;
import com.heima.comment.interceptor.UserInterceptor;
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 MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInterceptor()).addPathPatterns("/**");
}
}
实体
实体类ApComment
package com.heima.comment.entity;
import lombok.Data;
import org.springframework.data.mongodb.core.mapping.Document;
import java.math.BigDecimal;
import java.util.Date;
/**
* APP评论信息
*/
@Data
@Document("ap_comment")
public class ApComment {
/**
* id
*/
private String id;
/**
* 用户ID
*/
private Integer userId;
/**
* 用户昵称
*/
private String userName;
/**
* 用户头像
*/
private String image;
/**
* 文章id
*/
private Long articleId;
/**
* 评论内容
*/
private String content;
/**
* 点赞数
*/
private Integer likes = 0;
/**
* 回复数
*/
private Integer repay = 0;
/**
* 创建时间
*/
private Date createdTime;
}
ApCommentLike
package com.heima.comment.entity;
import lombok.Data;
import org.springframework.data.mongodb.core.mapping.Document;
/**
* APP评论信息点赞
*/
@Data
@Document("ap_comment_like")
public class ApCommentLike {
/**
* id
*/
private String id;
/**
* 用户ID
*/
private Integer userId;
/**
* 评论id
*/
private String commentId;
/**
* 0:点赞
* 1:取消点赞
*/
private Integer operation;
private Date createTime;
}
远程调用接口
评论中需要查询登录的用户信息,所以需要定义远程feign接口根据用户id获取用户信息
1、在user用户微服务中修改ApUserController ,添加根据id查询用户方法
@RestController
@RequestMapping("/api/v1/user")
@CrossOrigin
public class ApUserController {
@Autowired
private IApUserService apUserService;
@GetMapping("/{id}")
public ResponseResult<ApUser> getById(@PathVariable("id") Integer id) {
ApUser apUser = apUserService.getById(id);
return ResponseResult.okResult(apUser);
}
}
2、在comment微服务中定义feign接口
先添加feign的启动器
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
再添加对Feign的注解支持
3、把user微服务的实体类拷贝过来,去掉表相关的注解
package com.heima.comment.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* <p>
* APP用户信息表
* </p>
*
* @author syl
* @since 2022-08-22
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(description="APP用户信息表")
public class ApUser implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@ApiModelProperty(value = "主键")
private Integer id;
/**
* 用户名
*/
@ApiModelProperty(value = "用户名")
private String name;
/**
* 头像
*/
@ApiModelProperty(value = "头像")
private String image;
}
4、在comment微服务中定义feign接口
package com.heima.comment.feign;
import com.heima.comment.entity.ApUser;
import com.heima.common.dto.ResponseResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("leadnews-user")
public interface UserFeign {
/**
* 根据id查询用户
* @param id
* @return
*/
@GetMapping("/api/v1/user/{id}")
public ResponseResult<ApUser> getUserById(@PathVariable("id") Integer id);
}
接口定义
说明 | |
接口路径 | /api/v1/comment/save |
请求方式 | POST |
参数 | CommentSaveDto |
响应结果 | ResponseResult |
保存评论
package com.heima.comment.controller;
import com.heima.comment.dto.CommentSaveDto;
import com.heima.comment.service.ICommentService;
import com.heima.common.dto.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/comment")
public class CommentController {
@Autowired
private ICommentService commentService;
/**
* 发表评论
* @param dto
* @return
*/
@PostMapping("/save")
public ResponseResult saveComment(@RequestBody CommentSaveDto dto) {
return commentService.saveComment(dto);
}
}
发表评论:CommentSaveDto
package com.heima.comment.dto;
import lombok.Data;
@Data
public class CommentSaveDto {
/**
* 文章id
*/
private Long articleId;
/**
* 频道id
*/
private Integer channelId;
/**
* 评论内容
*/
private String content;
}
业务层
业务层接口:
package com.heima.comment.service;
import com.heima.comment.dto.CommentSaveDto;
import com.heima.common.dto.ResponseResult;
public interface ICommentService {
ResponseResult saveComment(CommentSaveDto dto);
}
实现类:
package com.heima.comment.service.impl;
import com.heima.comment.dto.ApUser;
import com.heima.comment.dto.CommentSaveDto;
import com.heima.comment.entity.ApComment;
import com.heima.comment.feign.UserFeign;
import com.heima.comment.service.ICommentService;
import com.heima.common.dto.ResponseResult;
import com.heima.common.dto.User;
import com.heima.common.enums.AppHttpCodeEnum;
import com.heima.common.util.AppThreadLocalUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class CommentServiceImpl implements ICommentService {
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private UserFeign userFeign;
@Override
public ResponseResult saveComment(CommentSaveDto dto) {
User user = UserThreadLocalUtil.get();
// 用户登录后可以对文章发表评论
if(user==null||user.getUserId()==0){
return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}
// 评论内容不为空且不超过140字
if(StringUtils.isBlank(dto.getContent()) || dto.getContent().length()>140){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
ResponseResult<ApUser> responseResult = userFeign.getById(user.getUserId());
ApUser apUser = responseResult.getData();
ApComment apComment = new ApComment();
// apComment.setId(); //不需要 mongodb会自动生成
apComment.setUserId(user.getUserId());
apComment.setUserName(apUser.getName());
apComment.setImage(apUser.getImage());
apComment.setArticleId(dto.getArticleId());
apComment.setContent(dto.getContent());
apComment.setLikes(0); //当前这个评论收到的点赞数
apComment.setRepay(0); //当前这个评论收到的回复数
apComment.setCreatedTime(new Date());
mongoTemplate.save(apComment);
return ResponseResult.okResult();
}
}
在APP网关中添加评论服务
- id: comment
uri: lb://leadnews-comment
predicates:
- Path=/comment/**
filters:
- StripPrefix= 1
测试
打开前端页面进行测试
app端评论-评论列表
需求分析
- 根据文章id查询评论列表
- 按照评论发布时间倒序排列 createTime desc
- 分页查询
- 如果用户登录了,需要判断当前用户点赞了列表中哪些评论(这个等做完对评论点赞后再说)
功能实现
接口定义
说明 | |
接口路径 | /api/v1/comment/load |
请求方式 | POST |
参数 | CommentDto |
响应结果 | ResponseResult |
CommentController
中添加方法
/**
* 加载评论列表
* @param dto
* @return
*/
@PostMapping("/load")
public ResponseResult loadComment(@RequestBody CommentDto dto) {
return commentService.loadComment(dto);
}
参数 CommentDto
import lombok.Data;
import java.util.Date;
@Data
public class CommentDto {
/**
* 文章id
*/
private Long articleId;
/**
* 显示条数
*/
private Integer size;
/**
* 最小时间
*/
private Date minDate;
/**
* 是否首页
*/
private Integer index;
}
业务层
业务层接口:
修改CommentService业务层接口,添加方法
/**
* 根据文章id查询评论列表
* @return
*/
ResponseResult loadComment(CommentDto dto);
实现类:
@Override
public ResponseResult loadComment(CommentDto dto) {
if (dto.getSize() == null || dto.getSize() <= 0 || dto.getSize() >= 50) {
dto.setSize(10);
}
// - 根据文章id查询评论列表
// - 按照评论发布时间倒序排列 createTime desc
// - 分页查询 limit skip
Query query = new Query(Criteria.where("articleId").is(dto.getArticleId()));
query.with(Sort.by(Sort.Direction.DESC,"createdTime"));
query.limit(dto.getSize());
List<ApComment> commentList = mongoTemplate.find(query, ApComment.class);
return ResponseResult.okResult(commentList);
}
app端评论-点赞评论
需求分析
评论与点赞对应关系如下:
- 用户登录后可以对评论点赞
- 根据当前用户id和评论id查询是否已有点赞记录
- 没有点赞记录,新增点赞记录,更新评论的点赞数+1
- 已有点赞记录,判断点赞记录中的点赞状态是否与当前操作一致,不一致进行点赞数更新和点赞记录更新
- 点赞操作,评论的点赞数+1
- 取消点赞操作,评论的点赞数-1
接口定义
说明 | |
接口路径 | /api/v1/comment/like |
请求方式 | POST |
参数 | CommentLikeDto |
响应结果 | ResponseResult |
功能实现
CommentControll中添加方法
/**
* 点赞某一条评论
* @param dto
* @return
*/
@PostMapping("/like") //对评论点赞或取消点赞
public ResponseResult commentLike(@RequestBody CommentLikeDto dto){
return commentService.commentLike(dto);
}
用户点赞:CommentLikeDto
import lombok.Data;
@Data
public class CommentLikeDto {
/**
* 评论id
*/
private String commentId;
/**
* 0:点赞
* 1:取消点赞
*/
private Integer operation;
}
业务层
业务层接口:ICommentService
/**
* 点赞评论
* @param dto
* @return
*/
public ResponseResult commentLike(CommentLikeDto dto);
实现类:
@Override
public ResponseResult commentLike(CommentLikeDto dto) {
User user = UserThreadLocalUtil.get();
// 用户登录后可以对评论点赞
if(user==null||user.getUserId()==0){
return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}
// 向点赞表中插入数据
ApCommentLike apCommentLike = new ApCommentLike();
apCommentLike.setUserId(user.getUserId());
apCommentLike.setCommentId(dto.getCommentId());
apCommentLike.setOperation(dto.getOperation());
apCommentLike.setCreateTime(new Date());
mongoTemplate.save(apCommentLike);
// 对评论数修改 如果是SQL update ap_commet set likes=like+_1 where _id=??
Query query = new Query(Criteria.where("_id").is(dto.getCommentId()));
Update update = new Update();
// update.set("likes",);
if(dto.getOperation()==0){ //0:点赞
update.inc("likes",1);
}else{
update.inc("likes",-1);
}
mongoTemplate.updateFirst(query,update,ApComment.class);
return ResponseResult.okResult();
}
测试 //10.11为什么拿postman测试
使用postman来测试
评论列表中显示是否点赞
需求分析
实现思路:
当前登录人是否对这个评论点赞需求记录起来,为了提高效率,这种数据标记可以记录到Redis中
在点赞和取消点赞那里就应该存储和删除Redis中的数据
功能实现
定义返回vo类
package com.heima.comment.vo;
import com.heima.comment.entity.ApComment;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class ApCommentVo extends ApComment {
/**
* 0:点赞
* 1:取消点赞
*/
private Integer operation;
}
添加Redis配置
在leadnews-comment模块中引入redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件中添加redis地址
spring:
redis:
host: 192.168.85.143
点赞时记录数据
列表回显时赋值
完整的load方法代码:
@Override
public ResponseResult load(CommentDto dto) {
// 根据文章id查询评论列表
// 按照创建时间倒序排列
// 分页判断数据小于前端传递的最小时间
// 每页默认显示10条评论数据
if (dto.getSize() == null || dto.getSize() <= 0 || dto.getSize() >= 50) {
dto.setSize(10);
}
Query query = new Query();
query.addCriteria(Criteria.where("entryId").is(dto.getArticleId()).and("createdTime").lt(dto.getMinDate()));
// 分页 和 排序
Pageable page = PageRequest.of(0, dto.getSize(), Sort.Direction.DESC, "createdTime");
query.with(page);
List<ApComment> apComments = mongoTemplate.find(query, ApComment.class);
// TODO 登录用户需要查看当前返回的列表中是否已经有点赞记录
User user = UserThreadLocalUtil.get();
if(user!=null&&user.getUserId()!=0){
List<ApCommentVo> commentVoList = new ArrayList<>();
for (ApComment comment : apComments) {
ApCommentVo commentVo = new ApCommentVo();
BeanUtils.copyProperties(comment,commentVo);
Boolean hasKey = redisTemplate.hasKey("comment_like_" + user.getUserId() + "_" + comment.getId());
commentVo.setOperation(hasKey?0:1); //0代表当前登录人对这个评论点赞
commentVoList.add(commentVo);
}
return ResponseResult.okResult(commentVoList);
}
return ResponseResult.okResult(apComments);
}
app端评论回复(学员实现)
包含的功能有:发表回复、点赞回复、回复列表
需求分析
- 当用户点击了评论中的回复就可以查看所有评论回复内容
- 可以针对当前评论进行回复,需要更新评论的回复数量
- 可以对当前评论回复列表进行点赞操作,同时记录当前回复评论点赞信息
思路分析
(1)数据实体
操作数据实体为mongodb中的集合,评论回复集合是ap_comment_repay
对应实体类为:
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.math.BigDecimal;
import java.util.Date;
/**
* APP评论回复信息
*/
@Data
@Document("ap_comment_repay")
public class ApCommentRepay {
/**
* id
*/
@Id
private String id;
/**
* 用户ID
*/
private Integer userId;
/**
* 用户昵称
*/
private String userName;
/**
* 评论id
*/
private String commentId;
/**
* 回复内容
*/
private String content;
/**
* 点赞数
*/
private Integer likes = 0;
/**
* 创建时间
*/
private Date createdTime;
}
APP评论回复点赞信息,对回复点赞的集合是ap_comment_repay_like
import lombok.Data;
import org.springframework.data.mongodb.core.mapping.Document;
/**
* APP评论回复信息点赞信息
*/
@Data
@Document("ap_comment_repay_like")
public class ApCommentRepayLike {
/**
* id
*/
private String id;
/**
* 用户ID
*/
private Integer userId;
/**
* 评论id
*/
private String commentRepayId;
/**
* 0:点赞
* 1:取消点赞
*/
private Integer operation;
}
(2)思路分析:
1 评论回复
- 回复内容不为空且不超过140字
- 回复内容需要做文本反垃圾检测
- 回复内容需要做敏感词过滤
- 用户登录后可以对评论发表回复
2 回复点赞
- 用户登录后可以对回复点赞
- 根据当前用户id和回复id查询是否已有点赞记录
- 没有点赞记录,新增点赞记录,更新回复的点赞数+1
- 已有点赞记录,判断点赞记录中的点赞状态是否与当前操作一致,不一致进行点赞数更新和点赞记录更新
- 点赞操作,回复的点赞数+1
- 取消点赞操作,回复的点赞数-1
3 回复列表
- 根据评论id查询回复列表
- 按照点赞数量倒序排列
- 分页查询
- 如果用户未登录,返回回复列表
- 如果用户登录了,需要判断当前用户点赞了列表中哪些回复
以上描述是不是非常眼熟,和评论文章功能除了表名不一样以外其他的一模一样啊~~~~
接下来就根据接口文档自己实现吧
接口定义
某评论回复列表
说明 | |
接口路径 | /api/v1/comment_repay/load |
请求方式 | POST |
参数 | CommentRepayDto |
响应结果 | ResponseResult |
对某评论发表回复
说明 | |
接口路径 | /api/v1/comment_repay/save |
请求方式 | POST |
参数 | CommentRepaySaveDto |
响应结果 | ResponseResult |
点赞某条回复
说明 | |
接口路径 | /api/v1/comment_repay/like |
请求方式 | POST |
参数 | CommentRepayLikeDto |
响应结果 | ResponseResult |
功能实现
(1)接口定义
package com.heima.comment.controller;
import com.heima.comment.dto.CommentRepayDto;
import com.heima.comment.dto.CommentRepayLikeDto;
import com.heima.comment.dto.CommentRepaySaveDto;
import com.heima.comment.service.ICommentRepayService;
import com.heima.common.dto.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/comment_repay")
public class CommentRepayController {
@Autowired
private ICommentRepayService repayService;
/**
* 发表回复
* @param dto
* @return
*/
@PostMapping("/save")
public ResponseResult saveRepay(@RequestBody CommentRepaySaveDto dto) {
return repayService.saveRepay(dto);
}
/**
* 点赞回复
* @param dto
* @return
*/
@PostMapping("/like")
public ResponseResult like(@RequestBody CommentRepayLikeDto dto){
return repayService.like(dto);
}
/**
* 加载回复列表
* @param dto
* @return
*/
@PostMapping("/load")
public ResponseResult load(@RequestBody CommentRepayDto dto){
return repayService.load(dto);
}
}
加载评论回复列表参数:CommentRepayDto
import lombok.Data;
import java.util.Date;
@Data
public class CommentRepayDto {
/**
* 评论id
*/
private String commentId;
/**
* 显示条数
*/
private Integer size;
/**
* 最小时间
*/
private Date minDate;
}
保存回复内容参数:CommentRepaySaveDto
import lombok.Data;
@Data
public class CommentRepaySaveDto {
/**
* 评论id
*/
private String commentId;
/**
* 回复内容
*/
private String content;
}
点赞回复内容参数:CommentRepayLikeDto
import lombok.Data;
@Data
public class CommentRepayLikeDto {
/**
* 回复id
*/
private String commentRepayId;
/**
* 0:点赞
* 1:取消点赞
*/
private Integer operation;
}
(2) 业务层
评论回复业务层接口
package com.heima.comment.service;
import com.heima.comment.dto.CommentRepayDto;
import com.heima.comment.dto.CommentRepayLikeDto;
import com.heima.comment.dto.CommentRepaySaveDto;
import com.heima.common.dto.ResponseResult;
public interface ICommentRepayService {
ResponseResult load(CommentRepayDto dto);
ResponseResult saveRepay(CommentRepaySaveDto dto);
ResponseResult like(CommentRepayLikeDto dto);
}
加载评论回复列表数据封装类
package com.heima.comment.vo;
import com.heima.comment.entity.ApCommentRepay;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class ApCommentRepayVo extends ApCommentRepay {
/**
* 0:点赞
* 1:取消点赞
*/
private Integer operation;
}
实现类
package com.heima.comment.service.impl;
import com.heima.comment.dto.CommentRepayDto;
import com.heima.comment.dto.CommentRepayLikeDto;
import com.heima.comment.dto.CommentRepaySaveDto;
import com.heima.comment.entity.ApComment;
import com.heima.comment.entity.ApCommentRepay;
import com.heima.comment.entity.ApCommentRepayLike;
import com.heima.comment.entity.ApUser;
import com.heima.comment.feign.UserFeign;
import com.heima.comment.service.ICommentRepayService;
import com.heima.comment.vo.ApCommentRepayVo;
import com.heima.comment.vo.ApCommentVo;
import com.heima.common.dto.ResponseResult;
import com.heima.common.dto.User;
import com.heima.common.enums.AppHttpCodeEnum;
import com.heima.common.util.UserThreadLocalUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
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.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class CommentRepayServiceImpl implements ICommentRepayService {
@Autowired
private UserFeign userFeign;
@Autowired
private MongoTemplate mongoTemplate;
@Override
public ResponseResult load(CommentRepayDto dto) {
// 根据文章id查询评论列表
// 按照创建时间倒序排列
// 分页判断数据小于前端传递的最小时间
// 每页默认显示10条评论数据
if (dto.getSize() == null || dto.getSize() <= 0 || dto.getSize() >= 50) {
dto.setSize(10);
}
Query query = new Query();
query.addCriteria(Criteria.where("commentId").is(dto.getCommentId()).and("createdTime").lt(dto.getMinDate()));
// 分页 和 排序
query.limit(dto.getSize());
query.with(Sort.by(Sort.Direction.DESC,"createdTime"));
List<ApCommentRepay> apCommentRepayList = mongoTemplate.find(query, ApCommentRepay.class);
// 判断当前用户是否登录
User user = UserThreadLocalUtil.get();
// TODO 登录用户需要查看当前返回的列表中是否已经有点赞记录
if(user!=null&&user.getUserId()!=0){
List<ApCommentRepayVo> commentRepayVoList = new ArrayList<>();
for (ApCommentRepay commentRepay : apCommentRepayList) {
ApCommentRepayVo commentRepayVo = new ApCommentRepayVo();
BeanUtils.copyProperties(commentRepay,commentRepayVo);
Boolean hasKey = redisTemplate.hasKey("comment_repay_like_" + user.getUserId() + "_" + commentRepay.getId());
commentRepayVo.setOperation(hasKey?0:1); //0代表当前登录人对这个回复点赞
commentRepayVoList.add(commentRepayVo);
}
return ResponseResult.okResult(commentRepayVoList);
}
return ResponseResult.okResult(apCommentRepayList);
}
@Override
public ResponseResult saveRepay(CommentRepaySaveDto dto) {
// 判断内容是否大于140字
if (dto == null || dto.getContent().length() > 140) {
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
// 判断用户是否登录(要求使用手机号和密码登录的用户)
User user = UserThreadLocalUtil.get();
if (user == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}
// 安全过滤--阿里云文本审核 todo 自行实现
ApCommentRepay comment = new ApCommentRepay();
comment.setUserId(user.getUserId());
// 根据用户id查询用户,调用用户微服务提供的远程接口
ResponseResult<ApUser> userResponseResult = userFeign.getUserById(user.getUserId());
if (userResponseResult.getCode().equals(AppHttpCodeEnum.SUCCESS.getCode())) {
ApUser apUser = userResponseResult.getData();
comment.setUserName(apUser.getName());
}
comment.setCommentId(dto.getCommentId());
comment.setContent(dto.getContent());
comment.setLikes(0);
comment.setCreatedTime(new Date());
// 保存评论
mongoTemplate.save(comment);
// 将评论表的回复数量+1
Query commentQuery = new Query();
commentQuery.addCriteria(Criteria.where("_id").is(dto.getCommentId()));
// 更新条件
Update update = new Update();
update.inc("repay");
mongoTemplate.updateFirst(commentQuery,update, ApComment.class);
return ResponseResult.okResult();
}
@Override
public ResponseResult like(CommentRepayLikeDto dto) {
// 获取当前的用户
User user = UserThreadLocalUtil.get();
if (user == null||user.getUserId()==0) {
return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}
// 构建点赞记录
ApCommentRepayLike like = new ApCommentRepayLike();
like.setUserId(user.getUserId());
like.setCommentRepayId(dto.getCommentRepayId());
like.setOperation(dto.getOperation());
// 保存点赞或取消点赞记录
mongoTemplate.save(like);
// 更新回复的点赞数量+1或者-1
Query commentQuery = new Query();
commentQuery.addCriteria(Criteria.where("_id").is(dto.getCommentRepayId()));
Update update = new Update();
if (dto.getOperation() == 1) { //取消点赞
update.inc("likes",-1);
//删除在Redis中做的标记
redisTemplate.delete("comment_repay_like_"+user.getUserId()+"_"+dto.getCommentRepayId());
}else{ //点赞
update.inc("likes",1);
//在Redis中做标记
redisTemplate.boundValueOps("comment_repay_like_"+user.getUserId()+"_"+dto.getCommentRepayId())
.set("");//这里放什么值无所谓,只要判断存在这个key就可以了
}
mongoTemplate.updateFirst(commentQuery, update, ApCommentRepay.class);
return ResponseResult.okResult();
}
@Autowired
private StringRedisTemplate redisTemplate;
}
(3) 测试
打开前端页面进行测试