美食社交--评论服务

写在开始 : 主要是评论服务相关, 有兴趣的可以查看之前的连载文章,美食社交项目;

需求说明

在餐厅详情页面, 展示餐厅的最近十条最新评论

数据库设计

CREATE TABLE `t_reviews` (
`id`  int(11) NOT NULL AUTO_INCREMENT ,
`fk_restaurant_id`  int(11) NULL DEFAULT NULL COMMENT '餐厅外键ID' ,
`content`  varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL
DEFAULT NULL COMMENT '评论内容' ,
`fk_diner_id`  int(11) NULL DEFAULT NULL COMMENT '食客外键ID' ,
`like_it`  tinyint(11) NULL DEFAULT NULL ,
`is_valid`  tinyint(1) NULL DEFAULT NULL ,
`create_date`  datetime NULL DEFAULT NULL ,
`update_date`  datetime NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
AUTO_INCREMENT=1
ROW_FORMAT=COMPACT
;

解决思路

添加餐厅评论

美食社交--评论服务_第1张图片

数据库查询

select 
	id, fk_restaurant_id, content, fk_diner_id, like_it, create_date,update_date 
 from 
		t_reviews 
 where 
  	is_valid = 1 
 order by create_date desc limit 10

使用 Redis Lists 存储

  • 添加时,将评论数据 LPUSH key value 保存到队列
  • 查询时,利用 LRANGE key 0 9 查询前10条数据

二者对比

从数据库中直接查询结果,简单,在数据量较小的情况下,速度应该都能接受,使用Redis保存最新数据,会提高维护成本,但随着评论数的增加,Redis查询的性能肯定会更高,速度更快,数据库压力更小

代码编写

实体类

数据的 POJO 类
package com.itkaka.restaurants.model.pojo;

import com.itkaka.commons.model.base.BaseModel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@ApiModel(description = "餐厅评论实体类")
public class Reviews extends BaseModel {

    @ApiModelProperty("餐厅外键")
    private Integer fkRestaurantId;
    @ApiModelProperty("评论内容")
    private String content;
    @ApiModelProperty("食客外键")
    private Integer fkDinerId;
    @ApiModelProperty(value = "是否喜欢", example = "0=不喜欢,1=喜欢")
    private int likeIt;

}

编写查询结果 VO 类
package com.itkaka.restaurants.model.vo;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.itkaka.commons.model.vo.ShortDinerInfo;
import com.itkaka.restaurants.model.pojo.Reviews;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;

@Getter
@Setter
@ApiModel(description = "餐厅评论VO")
public class ReviewVO extends Reviews {

    @ApiModelProperty("食客信息")
    private ShortDinerInfo dinerInfo;
    @ApiModelProperty("创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "GMT+8")
    private Date createDate;

}

RedisKeyConstant

restaurant_new_reviews("restaurant:new:reviews:", "餐厅评论的Key"),

新增评论

ReviewsMapper新增方法
package com.itkaka.restaurants.mapper;

import com.itkaka.restaurants.model.pojo.Reviews;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Options;

/**
 * 评论服务 Mapper
 */
public interface ReviewsMapper {

    // 新增餐厅评论
    @Insert("INSERT INTO t_reviews (fk_restaurant_id, fk_diner_id, content, like_it, is_valid, create_date, update_date) " +
            " VALUES (#{fkRestaurantId}, #{fkDinerId}, #{content}, #{likeIt}, 1, NOW(), NOW())")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int save(Reviews reviews);

}

ReviewsService新增添加方法
/**
 * 评论服务 Service
 */
@Service
public class ReviewsService {

    @Value("${service.name.fs_oauth-server}")
    private String oauthServerName;
    @Value("${service.name.fs_diners-server}")
    private String dinersServerName;
    @Resource
    private RestTemplate restTemplate;
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private RestaurantsService restaurantsService;
    @Resource
    private ReviewsMapper reviewsMapper;
    private static final int START = 0;
    private static final int END = 9;

    //新增餐厅评论  likeId 0不喜欢  1喜欢
    public void addReview(Integer restaurantId,String accessToken,
                          String content,int likeIt){
        // 参数校验
        AssertUtil.isTrue(restaurantId == null || restaurantId < 1,
                "请选择评论的餐厅");
        AssertUtil.isNotEmpty(content, "请输入评论内容");
        AssertUtil.isTrue(content.length() > 800, "评论内容过长,请重新输入");
        // 判断餐厅是否存在
        Restaurants restaurants = restaurantsService.findById(restaurantId);
        AssertUtil.isTrue(restaurantId == null, "该餐厅不存在");
        // 获取登录用户信息
        SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
        // 插入数据库
        Reviews reviews = new Reviews();
        reviews.setContent(content);
        reviews.setFkRestaurantId(restaurantId);
        reviews.setFkDinerId(dinerInfo.getId());
        // 这里需要后台操作处理餐厅数据(喜欢/不喜欢)做自增处理
        reviews.setLikeIt(likeIt);
        int count = reviewsMapper.save(reviews);
        if (count == 0){
            return;
        }
        // 写入餐厅最新缓存评论信息
        String key = RedisKeyConstant.restaurant_new_reviews.getKey() + restaurantId;
        redisTemplate.opsForList().leftPush(key, reviews);
        // 修剪餐厅最新缓存评论信息
        redisTemplate.opsForList().trim(key, START, END);
    }

    // 获取登录用户信息
    private SignInDinerInfo loadSignInDinerInfo(String accessToken) {
        // 是否有 accessToken
        AssertUtil.mustLogin(accessToken);
        // 拼接远程请求 url
        String url = oauthServerName + "user/me?access_token={accessToken}";
        // 发送请求
        ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
        }
        SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(), new SignInDinerInfo(), false);
        return dinerInfo;
    }
}
ReviewsController添加
package com.itkaka.restaurants.controller;

import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.utils.ResultInfoUtil;
import com.itkaka.restaurants.model.vo.ReviewVO;
import com.itkaka.restaurants.service.ReviewsService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;

/**
 * 评论服务 Controller
 */
@RestController
@RequestMapping("reviews")
public class ReviewsController {

    @Resource
    private ReviewsService reviewsService;
    @Resource
    private HttpServletRequest request;

    // 新增餐厅评论
    @PostMapping("{restaurantId}")
    public ResultInfo<String> addReview(@PathVariable Integer restaurantId,
                                        String access_token,
                                        @RequestParam("content") String content,
                                        @RequestParam("likeIt") int likeIt) {
        reviewsService.addReview(restaurantId, access_token, content, likeIt);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), "添加成功");
    }
}

测试
Postman测试
访问:http://localhost/restaurants/reviews/1?access_token=116af711-645c-4ce9-bc32-cd304bdda020
美食社交--评论服务_第2张图片
美食社交--评论服务_第3张图片

查询最新评论

编写ReviewsService查询方法
  • LRAGEN 获取前十条数据
  • 转化成 List 对象,并获取 List dinerIds 集合
  • 调用用户服务,批量获取用户信息
  • 组装将用户信息填入返回结果集中
package com.itkaka.restaurants.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Lists;
import com.itkaka.commons.constant.ApiConstant;
import com.itkaka.commons.constant.RedisKeyConstant;
import com.itkaka.commons.exception.ParameterException;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.model.vo.ShortDinerInfo;
import com.itkaka.commons.model.vo.SignInDinerInfo;
import com.itkaka.commons.utils.AssertUtil;
import com.itkaka.restaurants.mapper.ReviewsMapper;
import com.itkaka.restaurants.model.pojo.Restaurants;
import com.itkaka.restaurants.model.pojo.Reviews;
import com.itkaka.restaurants.model.vo.ReviewVO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 评论服务 Service
 */
@Service
public class ReviewsService {

    @Value("${service.name.fs_oauth-server}")
    private String oauthServerName;
    @Value("${service.name.fs_diners-server}")
    private String dinersServerName;
    @Resource
    private RestTemplate restTemplate;
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private RestaurantsService restaurantsService;
    @Resource
    private ReviewsMapper reviewsMapper;
    private static final int START = 0;
    private static final int END = 9;

    //新增餐厅评论  likeId 0不喜欢  1喜欢
    public void addReview(Integer restaurantId,String accessToken,
                          String content,int likeIt){
        // 参数校验
        AssertUtil.isTrue(restaurantId == null || restaurantId < 1,
                "请选择评论的餐厅");
        AssertUtil.isNotEmpty(content, "请输入评论内容");
        AssertUtil.isTrue(content.length() > 800, "评论内容过长,请重新输入");
        // 判断餐厅是否存在
        Restaurants restaurants = restaurantsService.findById(restaurantId);
        AssertUtil.isTrue(restaurantId == null, "该餐厅不存在");
        // 获取登录用户信息
        SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
        // 插入数据库
        Reviews reviews = new Reviews();
        reviews.setContent(content);
        reviews.setFkRestaurantId(restaurantId);
        reviews.setFkDinerId(dinerInfo.getId());
        // 这里需要后台操作处理餐厅数据(喜欢/不喜欢)做自增处理
        reviews.setLikeIt(likeIt);
        int count = reviewsMapper.save(reviews);
        if (count == 0){
            return;
        }
        // 写入餐厅最新缓存评论信息
        String key = RedisKeyConstant.restaurant_new_reviews.getKey() + restaurantId;
        redisTemplate.opsForList().leftPush(key, reviews);
        // 修剪餐厅最新缓存评论信息
        redisTemplate.opsForList().trim(key, START, END);
    }

    // 获取登录用户信息
    private SignInDinerInfo loadSignInDinerInfo(String accessToken) {
        // 是否有 accessToken
        AssertUtil.mustLogin(accessToken);
        // 拼接远程请求 url
        String url = oauthServerName + "user/me?access_token={accessToken}";
        // 发送请求
        ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
        }
        SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(), new SignInDinerInfo(), false);
        return dinerInfo;
    }

    // 查询餐厅最新评论
    public List<ReviewVO> findNewReviews(Integer restaurantId,String accessToken){
        // 参数校验
        AssertUtil.isTrue(restaurantId == null || restaurantId < 1,
                "请选择餐厅进行查看");
        // 构建 Redis Key
        String key = RedisKeyConstant.restaurant_new_reviews.getKey() + restaurantId;
        // 拿前十条
        List<LinkedHashMap> reviews = redisTemplate.opsForList().range(key,START,END);
        // 初始化 VO 集合
        List<ReviewVO> reviewsVOS = Lists.newArrayList();
        // 初始化食客 ID 集合
        List<Integer> dinerIds = Lists.newArrayList();
        // 循环处理评论集合
        reviews.forEach(review -> {
            ReviewVO reviewsVO = BeanUtil.fillBeanWithMap(review, new ReviewVO(), false);
            reviewsVOS.add(reviewsVO);
            dinerIds.add(reviewsVO.getFkDinerId());
        });
        // 完善食客昵称和头像
        ResultInfo resultInfo = restTemplate.getForObject(dinersServerName +
                        "findByIds?access_token={accessToken}&ids={ids}", ResultInfo.class,
                accessToken, StrUtil.join(",", dinerIds));
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
        }
        List<LinkedHashMap> dinerInfoMaps = (List<LinkedHashMap>) resultInfo.getData();
        Map<Integer, ShortDinerInfo> dinerInfos = dinerInfoMaps.stream()
                .collect(Collectors.toMap(
                        diner -> (Integer) diner.get("id"),
                        diner -> BeanUtil.fillBeanWithMap(diner, new ShortDinerInfo(), false)
                ));
        // 循环处理 VO 集合插入食客信息
        reviewsVOS.forEach(review -> {
            ShortDinerInfo dinerInfo = dinerInfos.get(review.getFkDinerId());
            if (dinerInfo != null) {
                review.setDinerInfo(dinerInfo);
            }
        });
        return reviewsVOS;
    }

}

编写ReviewsController查询方法
package com.itkaka.restaurants.controller;

import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.utils.ResultInfoUtil;
import com.itkaka.restaurants.model.vo.ReviewVO;
import com.itkaka.restaurants.service.ReviewsService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;

/**
 * 评论服务 Controller
 */
@RestController
@RequestMapping("reviews")
public class ReviewsController {

    @Resource
    private ReviewsService reviewsService;
    @Resource
    private HttpServletRequest request;

    // 新增餐厅评论
    @PostMapping("{restaurantId}")
    public ResultInfo<String> addReview(@PathVariable Integer restaurantId,
                                        String access_token,
                                        @RequestParam("content") String content,
                                        @RequestParam("likeIt") int likeIt) {
        reviewsService.addReview(restaurantId, access_token, content, likeIt);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), "添加成功");
    }


    // 获取餐厅最新评论
    @GetMapping("{restaurantId}/news")
    public ResultInfo<List<ReviewVO>> findNewReviews(@PathVariable Integer restaurantId,
                                                      String access_token) {
        List<ReviewVO> reviewsVOList = reviewsService.findNewReviews(restaurantId, access_token);
        return ResultInfoUtil.buildSuccess(request.getServletPath(), reviewsVOList);
    }

}

测试:
美食社交--评论服务_第4张图片
访问:http://localhost/restaurants/reviews/1/news?access_token=116af711-645c-4ce9-bc32-cd304bdda020

写在最后

:::info

最新餐厅评论 这个功能中我们实现了添加餐厅评论、获取餐厅评论功能。 这个功能中 Redis 主要用于存储餐厅评论信息,使用了 List 数据类型

:::

你可能感兴趣的:(美食社交项目(Java微服务),JAVA,Java实战项目,美食,java,开发语言)