1.发布探店笔记
- 上传图片(上传图片和发布笔记分开两步,先上传图片)
修改图片上传的路径,这里先存放至硬盘上,然后返回图片的路径,前端服务器作保存。
@Slf4j
@RestController
@RequestMapping("upload")
public class UploadController {
@PostMapping("blog")//当请求的参数名称(页面表单上的name属性)与Controller的业务方法参数名称不一致时,就需要通过@RequestParam注解显示的绑定
public Result uploadImage(@RequestParam("file") MultipartFile image) {
try {
// 获取原始文件名称
String originalFilename = image.getOriginalFilename();
// 生成新文件名
String fileName = createNewFileName(originalFilename);
// 保存文件
image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));
// 返回结果
log.debug("文件上传成功,{}", fileName);
return Result.ok(fileName);
} catch (IOException e) {
throw new RuntimeException("文件上传失败", e);
}
}
...
}
public class SystemConstants {
public static final String IMAGE_UPLOAD_DIR = "D:\\aTest\\SpringBootTest\\new_start\\nginx-1.18.0\\html\\hmdp\\imgs";
......
}
- 发布笔记(图片来自步骤1的上传图片后返回的图片地址,地址记录在前端页面猜测是JS的ES6对象属性当中)
@RestController
@RequestMapping("/blog")
public class BlogController {
@Resource
private IBlogService blogService;
@Resource
private IUserService userService;
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
blogService.save(blog);
// 返回id
return Result.ok(blog.getId());
}
...
}
2.查看探店笔记
需求:点击博客,请求 URL: http://localhost:8080/api/blog/id,请求方法GET,返回博客文章信息以及用户id、icon、姓名信息。可以在Blog类里面增加userId(是数据库字段)、icon和username(非数据库字段)
设计:当查询某个id的博客文章时,首先根据博客id查询博客,得到博客对象,再根据博客对象中里面的userId来查询user的icon和username,再对博客对象中非数据库表中的字段值username和icon进行赋值,返回博客对象。
Controller,
/**
* 根据id查询Blog
* */
@GetMapping("/{id}")
public Result queryBlogById(@PathVariable("id") Long id){
return blogService.queryBlogById(id);
}
Service,
@Override
/**
* 根据id查询Blog
* */
public Result queryBlogById(Long id) {
// 1.查询 blog
Blog blog = this.getById(id);
if (blog == null){
log.error("笔记不存在!"+id);
return Result.fail("笔记不存在!");
}
// 2.查询笔记作者用户
this.queryBlogUser(blog);
return Result.ok(blog);
}
/**
* 查询笔记的作者用户
* */
public void queryBlogUser(Blog blog){
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
3.点赞
-
需求:
- 同一个用户只能点赞一次,再次点击则是取消
- 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
-
实现步骤
- 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
- 分析:如果在MySQL数据库中新建一张表,记录每个博客文章被哪个用户点赞过,可以,但是数据库的读写速度性能较低,用户体验较差。因此可以利用redis这种轻量级数据库,读写速度快。因为一个用户只能点赞一次,因此可以使用redis中的Set结构,不可重复性。一个Set集合里面包含点赞博客文章的用户id。
- 修改点赞功能。利用redis的set集合判断是否点赞过,为未点赞过则点赞数+1,点赞过则点赞数-1
- 修改根据id查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
- 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
- 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
-
思考:但是存在一个问题,如果直接使用上面的private void isBlogLiked(Blog blog)方法,如果用户没有登录,则会报空指针的异常,导致页面的博客文章显示不出来。
解决方法:在private void isBlogLiked(Blog blog)方法当中,判断用户是否登录,未登录则什么都不做,而且在未登录显示的页面当中,热点文章的点赞需要先登录。
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;
@Resource
private StringRedisTemplate stringRedisTemplate;
//根据分页查询热点文章
@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(blog ->{
queryBlogUser(blog);
this.isBlogLiked(blog);
});
return Result.ok(records);
}
//根据id查询文章
@Override
public Result queryBlogById(Long id) {
// 1.查询blog
Blog blog = getById(id);
if(blog == null){
return Result.fail("博客文章不存在!");
}
// 2.查询blog有关的用户
queryBlogUser(blog);
// 3.查询blog是否被点赞,前端页面的判断来自与这里blog对象中的isLike字段值
isBlogLiked(blog);
return Result.ok(blog);
}
//判断文章是否被当前用户点赞,设置blog的isLike字段值
private void isBlogLiked(Blog blog) {
try{
UserDTO userDTO = UserHolder.getUser();
if(userDTO != null){
// 1.获取登录用户
Long userId = userDTO.getId();
// 2.判断当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + blog.getId();
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
blog.setIsLike(BooleanUtil.isTrue(isMember));
}
}catch (Exception e){
throw new RuntimeException(e);
}
}
//点赞文章
@Override
public Result likeBlog(Long id) {
// 1.获取登录用户
Long userId = UserHolder.getUser().getId();
// 2.判断当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
//因为isMember是包装类,所以不能直接判断,使用Boolean包装类
if(BooleanUtil.isFalse(isMember)){
// 3.如果未点赞,可以点赞
// 3.1数据库点赞数 + 1
boolean isSuccess = update()
.setSql("liked = liked + 1")
.eq("id", id).update();
// 3.2保存用户到Redis的set集合
if(isSuccess){
stringRedisTemplate.opsForSet().add(key,userId.toString());
}
}else{
// 4.如果已点赞,取消点赞
// 4.1.数据库点赞数 - 1
boolean isSuccess = update()
.setSql("liked = liked - 1")
.eq("id", id).update();
// 4.2.把用户从Redis的set集合移除
if(isSuccess){
stringRedisTemplate.opsForSet().remove(key,userId.toString());
}
}
return Result.ok();
}
//设置文章的用户相关信息
private void queryBlogUser(Blog blog) {
try{
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
4.点赞排行榜(SortedSet)
-
需求:根据时间顺序,展示出最早点赞的五个用户的信息。请求URL:…/blog/likes/{id},请求方式GET,请求参数为blog的id,返回值List给这个笔记点赞的TopN用户集合
-
Redis数据类型对比
-
根据需求决定使用SortedSort,而增添元素的命令是ZADD,但SortedSet没有isMember这个命令,所以用ZSCORE这个命令输入key 和 member来获取member的score来判断是否存在该元素。另一条指令就是ZRANGE key start end,注意redis下标也是从0开始,返回排序从0-4的5个元素member。
-
修改代码
修改点赞代码,将点赞的用户id从原来存放的set集合改成sortedset集合,
@Override
/**
* 登录用户点赞笔记
* */
public Result likeBlog(Long id) {
// 获取登录用户
Long userId = UserHolder.getUser().getId();
// 查看用户是否已点赞
String key = RedisConstants.BLOG_LIKED_KEY + id;
// 从 sortedset 集合判断分数是否存在判断用户是否点赞
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
// 未点赞
if (score == null){
// 数据库点赞数+1
boolean isSuccess = this.update().setSql("liked = liked + 1").eq("id", id).update();
// 保存登录用户 id 进 redis
if (isSuccess){
// zadd key value score
stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
}
}else {
// 已点赞
// 数据库点赞数-1
boolean isSuccess = this.update().setSql("liked = liked - 1").eq("id", id).update();
// 在 redis 删除登录用户 id
if (isSuccess){
stringRedisTemplate.opsForZSet().remove(key, userId.toString());
}
}
return Result.ok();
}
- 修改用户是否点赞的代码
/**
* 查询笔记是否被当前登录用户点赞
* */
public void queryIsLike(Blog blog){
// 1.获取登录用户
UserDTO user = UserHolder.getUser();
if (user == null) {
// 用户未登录,无需查询是否点赞
return;
}
// 查询 redis 的 sortedset 是否存在当前用户
Long userId = user.getId();
String key = RedisConstants.BLOG_LIKED_KEY + blog.getId();
// 存在则已点赞
// 不存在则未点赞
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
// Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
// blog.setIsLike(BooleanUtil.isTrue(isMember));
blog.setIsLike(score != null);
}
- 增加查询点赞该id笔记的前五名用户信息
尽管redis中ZRANGE key 0 4是按score排序获得userId,但是对数据库进行读操作时,SELECT id,phone,password,nick_name,icon,create_time,update_time FROM tb_user WHERE id IN ( ? , ? )结果却是反过来的顺序。
解决办法:对field中的id进行排序
SELECT id,phone,password,nick_name,icon,create_time,update_time FROM tb_user WHERE id IN ( 1010 , 1 ) ORDER BY FIELD(id,1010,1);
@Override
/**
* 查询笔记的点赞排行榜前5的用户
* */
public Result likesBlog(Long id) {
String key = RedisConstants.BLOG_LIKED_KEY + id;
// 1.查询redis sortedset 的前5 zrange key 0 4
Set<String> range = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if (range == null || range.isEmpty()){
return Result.ok(Collections.emptyList());
}
// 2.解析排行榜前几的用户 id 数据
List<Long> ids = range.stream().map(Long::valueOf).collect(Collectors.toList());
// 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)
String idStr = StrUtil.join(",", ids);
List<UserDTO> userDTOS = userService.query()
.in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list()
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
// 4.返回数据
return Result.ok(userDTOS);
}
达人探店流程如下:
- 热点笔记分页查询、根据id查询笔记 ->(数据库:根据blog表中的liked值降序排序,分页第1页,页面size为5) -> 从IPage(Page)中获取records,然后从records中取出blog,设置blog的用户相关信息 -> 传入blog,根据用户id查询redis,判断该笔记是否被当前用户点赞;
- 查看blog,根据用户id查询redis判断该笔记是否被当前用户点赞,然后设置blog中的isLike属性,传到前端判断是否设置高亮。
- 点赞,根据传入的blog的id,读取redis数据判断是否被当前用户点赞,如果没有,则点赞,将用户id存入redis;否则,删除redis中用户id