目录
一、需求分析
1.1 功能分析
1.2 表结构分析
二、服务搭建
2.1 服务搭建
2.1.1 创建module
2.1.2 添加依赖
2.1.3 编写配置
2.1.4 启动类
2.2 准备工作
2.2.1 创建实体类
2.2.2 创建数据访问接口
2.2.3 添加路由
2.3 功能实现
2.3.1 分页查询评论
2.3.2 发布评论
2.3.3 删除评论
2.3.4 修改评论
2.3.5 根据id查询评论
2.3.6 评论点赞
2.3.7 评论访问量增加
2.4 测试
2.4.1 增加100条评论数据
2.4.2 分页查询
2.4.2 根据id查询评论
2.4.3 根据id修改评论
2.4.4 根据id删除评论
2.4.5 评论点赞
2.4.6 评论的评论
三、补充
3.1 Feign
3.2 添加相关依赖
3.3 修改
采用SpringDataMongoDB框架实现评论微服务的持久层。
实现功能:
(1)基本CRUD API
(2)评论点赞
字段名称 |
字段含义 |
字段类型 |
备注 |
---|---|---|---|
_id |
ID |
文本 |
|
orderid | 订单id | 文本 | |
spuid |
商品id |
文本 |
|
userid |
用户id |
文本 |
|
nickname |
评论人昵称 |
文本 |
|
parentid |
评论id |
文本 |
父评论id,顶级评论为0 |
isparent |
是否是顶级评论 |
布尔型 |
|
publishtime |
评论日期 |
日期 |
|
visits |
浏览量 |
整型 |
|
thumbup |
点赞数 |
整型 |
|
images |
评论中的图片 |
数组 |
|
content |
评论内容 |
文本 |
|
comment |
回复数 |
整型 |
|
iscomment |
是否允许回复 |
布尔型 |
|
type |
评价类型 |
整型 |
0:非常差 1:差 2:一般 3:好 4:非常好 -1:表示评论的回复 |
子模块interface:
子模块service:
目录结构:
在leyou-comments-service中添加依赖
org.springframework.boot
spring-boot-starter-parent
2.0.4.RELEASE
4.0.0
com.leyou.comments
leyou-comments-service
org.springframework.boot
spring-boot-starter-web
2.0.4.RELEASE
org.springframework.boot
spring-boot-starter-data-mongodb
2.1.0.RELEASE
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
2.0.2.RELEASE
org.springframework.cloud
spring-cloud-starter-config
2.0.2.RELEASE
org.springframework.cloud
spring-cloud-bus
2.0.0.RELEASE
org.springframework.cloud
spring-cloud-stream-binder-rabbit
2.0.0.RELEASE
org.springframework.boot
spring-boot-starter-actuator
2.0.0.RELEASE
com.leyou.authentication
leyou-authentication-common
1.0.0-SNAPSHOT
compile
com.leyou.authentication
leyou-authentication-common
1.0.0-SNAPSHOT
compile
com.leyou.common
leyou-common
1.0.0-SNAPSHOT
compile
com.leyou.review
leyou-review-interface
1.0.0-SNAPSHOT
compile
com.leyou.comments
leyou-comments-interface
1.0.0-SNAPSHOT
compile
在这里面需要说明一下,spring boot在整合mongodb的时候,其配置文件中的parent属性必须是spring boot的依赖:
否则在继承MongoRepository时无法自动加载。这样就破坏了整个工程结构,这个坑怎么填?请大佬赐教!!!!!!!!!
server:
port: 8092
tomcat:
max-connections: 5000
spring:
application:
name: comments-service
data:
mongodb:
host: 192.168.19.121
database: spitdb
datasource:
url: jdbc:mysql://127.0.0.1:3306/leyou?characterEncoding=UTF-8&useSSL=false
username: root
password: 123456
hikari:
maximum-pool-size: 20
minimum-idle: 10
rabbitmq:
virtual-host: /leyou
username: /leyou
password: leyou
host: 192.168.19.121
redis:
host: 192.168.19.121
jackson:
default-property-inclusion: non_null # 配置json处理时忽略空值
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 5
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true #当你获取host时,返回的不是主机名,而是ip
ip-address: 127.0.0.1
lease-expiration-duration-in-seconds: 10 #10秒不发送九过期
lease-renewal-interval-in-seconds: 5 #每隔5秒发一次心跳
ribbon:
ConnectTimeout: 4000 # 连接超时时间(ms)
ReadTimeout: 2000 # 通信超时时间(ms)
OkToRetryOnAllOperations: true # 是否对所有操作重试
MaxAutoRetriesNextServer: 1 # 同一服务不同实例的重试次数
MaxAutoRetries: 1 # 同一实例的重试次数
leyou:
worker:
workerId: 2
dataCenterId: 2
jwt:
cookieName: LY_TOKEN
pubKeyPath: G:\\tmp\\rsa\\rsa.pub # 公钥地址
package com.leyou.comments;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* @Author: 98050
* @Time: 2018-11-29 15:41
* @Feature:
*/
@SpringBootApplication
@EnableDiscoveryClient
public class LyCommentsApplication {
public static void main(String[] args) {
SpringApplication.run(LyCommentsApplication.class,args);
}
}
package com.leyou.comments.pojo;
import javax.persistence.Id;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* @Author: 98050
* @Time: 2018-11-26 14:45
* @Feature:
*/
public class Review implements Serializable {
@Id
private String _id;
/**
* 订单id
*/
private String orderid;
/**
* 商品id
*/
private String spuid;
/**
* 评论内容
*/
private String content;
/**
* 评论时间
*/
private Date publishtime;
/**
* 评论用户id
*/
private String userid;
/**
* 评论用户昵称
*/
private String nickname;
/**
* 评论的浏览量
*/
private Integer visits;
/**
* 评论的点赞数
*/
private Integer thumbup;
/**
* 评论中的图片
*/
private List images;
/**
* 评论的回复数
*/
private Integer comment;
/**
* 该评论是否可以被回复
*/
private Boolean iscomment;
/**
* 该评论的上一级id
*/
private String parentid;
/**
* 是否是顶级评论
*/
private Boolean isparent;
/**
* 评论的类型
*/
private Integer type;
/**
* json转换需要
*/
public Review() {
}
public Review(String orderid,String spuid, String content, String userid, String nickname, List images, Boolean iscomment, String parentid, Boolean isparent, Integer type) {
this.orderid = orderid;
this.spuid = spuid;
this.content = content;
this.userid = userid;
this.nickname = nickname;
this.images = images;
this.iscomment = iscomment;
this.parentid = parentid;
this.isparent = isparent;
this.type = type;
}
}
package com.leyou.comments.dao;
import com.leyou.comments.pojo.Review;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.repository.MongoRepository;
/**
* @Author: 98050
* @Time: 2018-11-26 20:51
* @Feature:
*/
public interface CommentDao extends MongoRepository {
}
在网关中添加路由
Controller
请求方式:GET
请求路径:/list
请求参数:商品id、当前页码、页面大小,封装一个请求参数对象:
返回结果:返回评论的分页信息
请求对象,每页大小20,固定参数两个:spuId、page。其中构造函数是用来接收前端传进来的数据,其余都在后端生成。
package com.leyou.comments.bo;
/**
* @Author: 98050
* @Time: 2018-11-26 21:40
* @Feature:
*/
public class CommentRequestParam {
/**
* 商品id
*/
private Long spuId;
/**
* 当前页码
*/
private Integer page;
/**
* 每页大小,不从页面接收,而是固定大小
*/
private static final Integer DEFAULT_SIZE = 20;
/**
* 默认页
*/
private static final Integer DEFAULT_PAGE = 1;
public Long getSpuId() {
return spuId;
}
public void setSpuId(Long spuId) {
this.spuId = spuId;
}
public Integer getPage() {
if (page == null){
return DEFAULT_PAGE;
}
/**
* 获取页码时做一些校验,不能小于1
*/
return Math.max(DEFAULT_PAGE,page);
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getDefaultSize() {
return DEFAULT_SIZE;
}
}
package com.leyou.comments.controller;
import com.leyou.auth.entity.UserInfo;
import com.leyou.comments.bo.CommentRequestParam;
import com.leyou.comments.interceptor.LoginInterceptor;
import com.leyou.comments.pojo.Review;
import com.leyou.comments.service.CommentService;
import com.leyou.common.pojo.PageResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* @Author: 98050
* @Time: 2018-11-26 21:30
* @Feature:
*/
@RequestMapping
@RestController
public class CommentController {
@Autowired
private CommentService commentService;
/**
* 分页查询某一商品下的所有顶级评论
* @param requestParam
* @return
*/
@GetMapping("list")
public ResponseEntity findReviewBySpuId(@RequestBody CommentRequestParam requestParam){
Page result = commentService.findReviewBySpuId(requestParam);
if (result == null){
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
PageResult pageResult = new PageResult();
pageResult.setTotal(result.getTotalElements());
pageResult.setItems(result.getContent());
pageResult.setTotalPage((long)result.getTotalPages());
return ResponseEntity.ok(pageResult);
}
}
Service
package com.leyou.comments.service;
import com.leyou.comments.bo.CommentRequestParam;
import com.leyou.comments.pojo.Review;
import org.springframework.data.domain.Page;
/**
* @Author: 98050
* @Time: 2018-11-26 15:40
* @Feature:
*/
public interface CommentService {
/**
* 查询某一商品下的所有顶级评论
* @param commentRequestParam
* @return
*/
Page findReviewBySpuId(CommentRequestParam commentRequestParam);
}
package com.leyou.comments.service.impl;
import com.leyou.comments.bo.CommentRequestParam;
import com.leyou.comments.dao.CommentDao;
import com.leyou.comments.pojo.Review;
import com.leyou.comments.service.CommentService;
import com.leyou.utils.IdWorker;
import com.mongodb.client.result.UpdateResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
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;
/**
* @Author: 98050
* @Time: 2018-11-26 15:41
* @Feature:
*/
@Service
public class CommentServiceImpl implements CommentService {
@Autowired
private CommentDao commentDao;
@Autowired
private MongoTemplate mongoTemplate;
/**
* 查询某一商品下的所有顶级评论
* @param commentRequestParam
* @return
*/
@Override
public Page findReviewBySpuId(CommentRequestParam commentRequestParam) {
PageRequest pageRequest = PageRequest.of(commentRequestParam.getPage()-1, commentRequestParam.getDefaultSize());
return this.commentDao.findReviewBySpuid(commentRequestParam.getSpuId()+"",pageRequest);
}
}
Dao
在CommentDao中添加分页查询的方法:
public interface CommentDao extends MongoRepository {
/**
* 分页查询
* @param spuId
* @param pageable
* @return
*/
Page findReviewBySpuid(String spuId, Pageable pageable);
}
Controller
请求方式:POST
请求路径:/
请求参数:review对象
返回结果:201
每个用户对每一个订单只能发表一次顶级评论,可以追评论。
/**
* 增加评论
* @param review
* @return
*/
@PostMapping
public ResponseEntity addReview(@RequestBody Review review){
boolean result = this.commentService.add(review);
if (!result){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
return ResponseEntity.status(HttpStatus.CREATED).build();
}
Service
添加评论有两种情况,第一种是为商品添加评论,第二种是评论的回复。在这里就体现出表结构设计的技巧,同理参考商品分类管理中的表结构设计。
这里面评论的id需要自己指定,所以要使用IdWorker,参照以前的模块,cv一下
/**
* 新增
* 注意:一个用户只能发表一个顶级评论,可以追评(在一个订单中)
* @param review
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean add(Long orderId,Review review) {
//1.检查用户是否在该商品下发表过顶级评论过
if (review.getIsparent()) {
Query query2 = new Query();
query2.addCriteria(Criteria.where("userid").is(review.getUserid()));
query2.addCriteria(Criteria.where("orderid").is(review.getOrderid()));
query2.addCriteria(Criteria.where("spuid").is(review.getSpuid()));
List old = this.mongoTemplate.find(query2, Review.class);
if (old.size() > 0 && old.get(0).getIsparent()) {
return false;
}
}
//2.修改订单状态,6代表评价状态
boolean result = this.orderClient.updateOrderStatus(orderId, 6).getBody();
if (!result){
return false;
}
//3.添加评论
/**
* 设置主键
*/
review.set_id(idWorker.nextId() + "");
review.setPublishtime(new Date());
review.setComment(0);
review.setThumbup(0);
review.setVisits(0);
if (review.getParentid() != null && !"".equals(review.getParentid())){
//如果存在上级id,则上级评论数加1,将上级评论的isParent设置为true,浏览量加一
Query query = new Query();
query.addCriteria(Criteria.where("_id").is(review.getParentid()));
Update update = new Update();
update.inc("comment",1);
update.set("isparent",true);
update.inc("visits",1);
this.mongoTemplate.updateFirst(query,update,"review");
}
commentDao.save(review);
return true;
}
Controller
请求方式:DELETE
请求路径:/commentId/{id}
请求参数:评论id
返回结果:200
/**
* 根据评论id删除评论
* @param id
* @return
*/
@DeleteMapping("/commentId/{id}")
public ResponseEntity deleteReview(@PathVariable("id") String id){
this.commentService.deleteById(id);
return ResponseEntity.status(HttpStatus.OK).build();
}
Service
/**
* 删除
*
* @param id
*/
@Override
public void deleteById(String id) {
commentDao.deleteById(id);
}
Controller
请求方式:PUT
请求路径:/comment
请求参数:review对象
返回结果:200
/**
* 修改评论
* @param review
* @return
*/
@PutMapping("/comment")
public ResponseEntity updateReview(@RequestBody Review review){
this.commentService.update(review);
return ResponseEntity.status(HttpStatus.OK).build();
}
Service
/**
* 修改
*
* @param review
*/
@Override
public void update(Review review) {
commentDao.save(review);
}
Controller
请求方式:GET
请求路径:/commentId/{id}
请求参数:评论id
返回结果:review对象
/**
* 根据评论id查询评论
* @param id
* @return
*/
@GetMapping("/commentId/{id}")
public ResponseEntity findReviewById(@PathVariable("id") String id){
Review review = this.commentService.findOne(id);
if (review == null){
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok(review);
}
Service
@Override
public Review findOne(String id) {
//判断空
Optional optional = commentDao.findById(id);
return optional.orElse(null);
}
Controller
请求方式:PUT
请求路径:thumb/{id}
请求参数:评论id
返回结果:true or false
点赞就是让字段thumbup加一,使用MongoTemplate类来实现对某列的操作,不用全部都查询出来,那样效率不高。使用自增函数inc实现thumbup加一。
使用redis控制不能重复点赞
private final String THUMBUP_PREFIX = "thumbup";
/**
* 评论点赞
* @param id
* @return
*/
@PutMapping("thumb/{id}")
public ResponseEntity updateThumbup(@PathVariable String id){
//1.首先判断当前用户是否点过赞
UserInfo userInfo = LoginInterceptor.getLoginUser();
String userId = userInfo.getId()+"";
if (redisTemplate.opsForValue().get(THUMBUP_PREFIX + userId + "_" + id) != null){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
boolean result = this.commentService.updateThumbup(id);
if (!result){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
redisTemplate.opsForValue().set(THUMBUP_PREFIX + userId + "_" + id,"1");
return ResponseEntity.ok(result);
}
Service
/**
* 评论点赞
* @param id
*/
@Override
public boolean updateThumbup(String id) {
Query query = new Query();
query.addCriteria(Criteria.where("_id").is(id));
Update update = new Update();
update.inc("thumbup",1);
UpdateResult result = this.mongoTemplate.updateFirst(query,update,"review");
return result.isModifiedCountAvailable();
}
这个功能和评论点赞差不多,但是具体怎么判断用户浏览过? 好吧,我也不知道,这个功能就当测试了。。。。。。。
Controller
请求方式:PUT
请求路径:/visit/{id}
请求参数:评论id
返回结果:200
/**
* 根据评论id访问量加1
* @param id
* @return
*/
@PutMapping("visit/{id}")
public ResponseEntity updateReviewVisit(@PathVariable("id") String id){
boolean result = this.commentService.updateVisits(id);
if (!result){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
return ResponseEntity.status(HttpStatus.OK).build();
}
Service
/**
* 访问量加一
* @param id
*/
@Override
public boolean updateVisits(String id) {
Query query = new Query();
query.addCriteria(Criteria.where("_id").is(id));
Update update = new Update();
update.inc("visits",1);
UpdateResult result = this.mongoTemplate.updateFirst(query,update,"review");
return result.isModifiedCountAvailable();
}
创建测试类:
import com.leyou.comments.LyCommentsApplication;
import com.leyou.comments.dao.CommentDao;
import com.leyou.comments.pojo.Review;
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.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: 98050
* @Time: 2018-12-09 20:37
* @Feature:
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LyCommentsApplication.class)
public class CommentsTest {
@Autowired
private CommentDao commentDao;
/**
* 为spuId为2的商品添加100条评顶级论数据
*/
@Test
public void LoadData(){
for (int i = 0; i < 100; i++) {
String spuId = "2";
String content = "手机不错"+i;
String userId = (35 + i) + "";
String nickname = "username"+i;
List images = new ArrayList<>();
boolean iscomment = i % 2 == 0;
String parentId = 0 + "";
boolean isparent = true;
int type = i % 5;
Review review = new Review(spuId, content, userId, nickname, images, iscomment, parentId,isparent,type);
commentDao.insert(review);
}
}
}
同一用户再次发布评论:
结果:
修改id为1071767094657556480的评论
重新查询:
删除id为1071767094657556480的评论:
重新查询该评论:
要添加cookie。
查询id为 1071767095416725504的评论:
这个没什么特别,在前端进行数据封装就可以了:
查看id为1071767095433502720的评论:
在添加评论的时候,还有一个需要做的工作就是修改订单的状态,为了测试方便,上面就没有给出,熟悉完mongodb后,再把这个坑填上。
在leyou-order微服务的接口中新增修改订单状态的方法:
在评论微服务中添加FeignClient:
package com.leyou.comments.client;
import com.leyou.order.api.OrderApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* @Author: 98050
* @Time: 2018-11-12 15:19
* @Feature: 订单接口
*/
@FeignClient(value = "order-service")
public interface OrderClient extends OrderApi {
}
com.leyou.order
leyou-order-interface
1.0.0-SNAPSHOT
compile
org.springframework.cloud
spring-cloud-openfeign-core
2.0.2.RELEASE
compile
注意:review对象中的orderid是后期加上去的,当时考虑不周,但是整体没有多大影响,在测试时添加100条数据的时候加上这个字段就可以了。