1. 达人探店-发布探店笔记
发布探店笔记
探店笔记类似点评网站的评价,往往是图文结合。对应的表有两个:
tb_blog
:探店笔记表,包含笔记中的标题、文字、图片等
tb_blog_comments
:其他用户对探店笔记的评价
具体发布流程:
图片上传接口:
@Slf4j
@RestController
@RequestMapping("upload")
public class UploadController {
@PostMapping("blog")
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);
}
}
@GetMapping("/blog/delete")
public Result deleteBlogImg(@RequestParam("name") String filename) {
File file = new File(SystemConstants.IMAGE_UPLOAD_DIR, filename);
if (file.isDirectory()) {
return Result.fail("错误的文件名称");
}
FileUtil.del(file);
return Result.ok();
}
private String createNewFileName(String originalFilename) {
// 获取后缀
String suffix = StrUtil.subAfter(originalFilename, ".", true);
// 生成目录
String name = UUID.randomUUID().toString();
int hash = name.hashCode();
int d1 = hash & 0xF;
int d2 = (hash >> 4) & 0xF;
// 判断目录是否存在
File dir = new File(SystemConstants.IMAGE_UPLOAD_DIR, StrUtil.format("/blogs/{}/{}", d1, d2));
if (!dir.exists()) {
dir.mkdirs();
}
// 生成文件名
return StrUtil.format("/blogs/{}/{}/{}.{}", d1, d2, name, suffix);
}
}
注意:读者在操作时,需要修改SystemConstants.IMAGE_UPLOAD_DIR
自己图片所在的地址,在实际开发中图片一般会放在nginx上或者是阿里OSS云存储服务上。
保存博客笔记:
BlogController
@RestController
@RequestMapping("/blog")
public class BlogController {
@Resource
private IBlogService blogService;
/**
* 保存博客
* @param blog
* @return
*/
@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. 达人探店-查看探店笔记
实现查看发布探店笔记的接口
实现代码:
BlogServiceImpl
/**
* 根据博客id查询博客
* @param id
* @return
*/
@Override
public Result queryBlogById(Long id) {
//1.查询blog
Blog blog = getById(id);
if(blog == null) return Result.fail("blog不存在!");
//2.根据blog查询相关用户
queryBlogUser(blog);
return Result.ok(blog);
}
/**
* 增加博客关联的用户名及用户头像信息
* @param blog
*/
private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
3. 达人探店-点赞功能
初始代码
BlogController类
@GetMapping("/likes/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
//修改点赞数量
blogService.update().setSql("liked = liked +1 ").eq("id",id).update();
return Result.ok();
}
问题分析:这种方式会导致一个用户无限点赞,明显是不合理的
造成这个问题的原因是,我们现在的逻辑,发起请求只是给数据库+1,所以才会出现这个问题。
完善点赞功能
需求:
- 同一个用户只能点赞一次,再次点击则取消点赞
- 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的
isLike
属性)
实现步骤:
- 给Blog类中添加一个
isLike
字段,标示是否被当前用户点赞 - 修改点赞功能,利用Redis的
set
集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1 - 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给
isLike
字段 - 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给
isLike
字段
为什么采用set集合:
因为我们的数据是不能重复的,一个用户只能点赞一次,要么在set集合中存在,sadd
添加当前用户的userId
;要么取消点赞,则srem
移除set中的用户userId
具体步骤:
1、在 Blog
类 添加一个字段
@TableField(exist = false)
private Boolean isLike;
2、修改代码
/**
* 设置点赞功能
* @param id
* @return
*/
@Override
public Result likeBlog(Long id) {
//1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
String key = RedisConstants.BLOG_LIKED_KEY + id;
//2.判断当前登录用户是否已经点赞
Boolean isMember = stringRedisTemplate.opsForSet()
.isMember(key, userId.toString());
if(BooleanUtil.isFalse(isMember)) {
//3.如果未点赞, 可以点赞
//3.1数据库字段liked + 1
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
//3.2将当前用户保存到set集合当中
if(isSuccess)
stringRedisTemplate.opsForSet().add(key, userId.toString());
}else {
//4.如果已点赞, 则取消点赞
//4.1数据库字段liked - 1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
//4.2将当前用户从set集合当中移除
if(isSuccess)
stringRedisTemplate.opsForSet().remove(key, userId.toString());
}
return Result.ok();
}
/**
* 博客分页数据
* @param current
* @return
*/
@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 -> {
this.queryBlogUser(blog);
this.isBlogLiked(blog);
});
return Result.ok(records);
}
/**
* 根据博客id查询博客
* @param id
* @return
*/
@Override
public Result queryBlogById(Long id) {
//1.查询blog
Blog blog = getById(id);
if(blog == null) return Result.fail("blog不存在!");
//2.根据blog查询相关用户
queryBlogUser(blog);
//3.查询blog是否已被点赞
isBlogLiked(blog);
return Result.ok(blog);
}
/**
* 设置博客是否被点赞(blog的IsLike属性)
* @param blog
*/
private void isBlogLiked(Blog blog) {
//1.获取当前登录用户
UserDTO user = UserHolder.getUser();
//如果不判空, 对于未登录用户,再去走user.getId()会报空指针异常
if(user == null) return;//如果用户没有登录, 则不用走该博客是否被点赞的逻辑
Long userId = user.getId();
String key = RedisConstants.BLOG_LIKED_KEY + blog.getId();
//2.判断当前登录用户是否已经点赞
Boolean isMember = stringRedisTemplate.opsForSet()
.isMember(key, userId.toString());
blog.setIsLike(BooleanUtil.isTrue(isMember));
}
/**
* 增加博客关联的用户名及用户头像信息
* @param blog
*/
private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
4. 达人探店-点赞排行榜
在探店笔记的详情页面,应该把给该笔记点赞的人显示出来,比如最早点赞的TOP5,形成点赞排行榜:
之前的点赞是放到set
集合,但是set
集合是不能排序的,所以这个时候,咱们可以采用一个可以排序的set
集合,就是redis中的另一种数据类型sortedSet
我们接下来来对比一下这些集合的区别是什么
所有点赞的人,需要是唯一的,所以我们应当使用set
或者是sortedSet
其次我们需要排序,就可以直接锁定使用sortedSet
啦
回顾一下zsort
的redis命令语法,zsort
会按照score
这个字段值进行排序,那么我们可以score中存入时间戳,根据时间戳来判断用户先后点赞的顺序。
另外需要注意的是,zset
没有判断成员是否在集合中存在sismember
这个方法,但是我们可以使用zscore
判断集合中成员的score
属性值是否为空,若返回nil
则说明没有这个成员。
修改代码:将之前使用的set
集合数据全部改为zset
类型数据
BlogServiceImpl
点赞逻辑代码:
/**
* 设置点赞功能
* @param id
* @return
*/
@Override
public Result likeBlog(Long id) {
//1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
String key = RedisConstants.BLOG_LIKED_KEY + id;
//2.判断当前登录用户是否已经点赞
Double score = stringRedisTemplate.opsForZSet()
.score(key, userId.toString());
if(score == null) {
//3.如果未点赞, 可以点赞
//3.1数据库字段liked + 1
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
//3.2将当前用户保存到zset集合当中 zadd key score member
if(isSuccess)
stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
}else {
//4.如果已点赞, 则取消点赞
//4.1数据库字段liked - 1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
//4.2将当前用户从zset集合当中移除
if(isSuccess)
stringRedisTemplate.opsForZSet().remove(key, userId.toString());
}
return Result.ok();
}
/**
* 博客分页数据
* @param current
* @return
*/
@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 -> {
this.queryBlogUser(blog);
this.isBlogLiked(blog);
});
return Result.ok(records);
}
/**
* 根据博客id查询博客
* @param id
* @return
*/
@Override
public Result queryBlogById(Long id) {
//1.查询blog
Blog blog = getById(id);
if(blog == null) return Result.fail("blog不存在!");
//2.根据blog查询相关用户
queryBlogUser(blog);
//3.查询blog是否已被点赞
isBlogLiked(blog);
return Result.ok(blog);
}
/**
* 设置博客是否被点赞(blog的IsLike属性)
* @param blog
*/
private void isBlogLiked(Blog blog) {
//1.获取当前登录用户
UserDTO user = UserHolder.getUser();
//如果不判空, 对于未登录用户,再去走user.getId()会报空指针异常
if(user == null) return;//如果用户没有登录, 则不用走该博客是否被点赞的逻辑
Long userId = user.getId();
String key = RedisConstants.BLOG_LIKED_KEY + blog.getId();
//2.判断当前登录用户是否已经点赞
Double score = stringRedisTemplate.opsForZSet()
.score(key, userId.toString());
blog.setIsLike(score != null);
}
点赞排行榜查询列表
BlogController
/**
* 点赞排行榜
* @param id
* @return
*/
@GetMapping("/likes/{id}")
public Result queryBlogLikes(@PathVariable("id") Long id){
return blogService.queryBlogLikes(id);
}
BlogService
/**
* 点赞排行榜
* @param id
* @return
*/
@Override
public Result queryBlogLikes(Long id) {
//1.查询top5的点赞用户 zrange key min max
String key = RedisConstants.BLOG_LIKED_KEY + id;
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if(top5 == null || top5.isEmpty()) return Result.ok(Collections.emptyList());
//2.解析出用户id
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
//3.根据用户id查询用户 where id in(5, 4) order by field(id, 5, 4)
String idStr = StrUtil.join(",", ids);
List<UserDTO> userDTOS = userService
//.query().in("id", ids).last("order by field(id," + idStr + ")") .list()
.listByIds(ids)
.stream()
.map(user -> {
return BeanUtil.copyProperties(user, UserDTO.class);
}).collect(Collectors.toList());
return Result.ok(userDTOS);
}
最后我们重启代码,发现点赞头像出现了顺序混乱问题,
最后我们发现是因为数据库中,系统不会按照in
关键字中值的顺序来进行排序并返回。所以我们需要使用order by filed()
实现自定义排序。
修改java中的代码:
再次刷新,顺序已经被调整过来了。