Day81(7)-F:\code\hm-dianping\hm-dianping
黑马点评
达人探店
发布探店笔记
@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);
}
}
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);
}
存储地址需要更改
package com.hmdp.utils;
public class SystemConstants {
public static final String IMAGE_UPLOAD_DIR = "F:\\lesson\\hmdp\\nginx-1.18.0\\html\\hmdp\\imgs";
public static final String USER_NICK_NAME_PREFIX = "user_";
public static final int DEFAULT_PAGE_SIZE = 5;
public static final int MAX_PAGE_SIZE = 10;
}
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
blogService.save(blog);
// 返回id
return Result.ok(blog.getId());
}
查询探店笔记
知识点
- 实体类中属性加上@TableField说明该属性不属于这个实体类@TableName("tb_blog")对应的表的信息
package com.hmdp.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
*
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_blog")
public class Blog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 商户id
*/
private Long shopId;
/**
* 用户id
*/
private Long userId;
/**
* 用户图标
*/
@TableField(exist = false)
private String icon;
/**
* 用户姓名
*/
@TableField(exist = false)
private String name;
/**
* 是否点赞过了
*/
@TableField(exist = false)
private Boolean isLike;
/**
* 标题
*/
private String title;
/**
* 探店的照片,最多9张,多张以","隔开
*/
private String images;
/**
* 探店的文字描述
*/
private String content;
/**
* 点赞数量
*/
private Integer liked;
/**
* 评论数量
*/
private Integer comments;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
查询blog-controller
@GetMapping("/hot")
public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
return blogService.queryHotBlog(current);
}
@GetMapping("/{id}")
public Result queryBlogById(@PathVariable("id") Long id){
return blogService.queryBlogById(id);
}
查询blog-service(39)
package com.hmdp.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;
@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(this::queryBlogUser);
return Result.ok(records);
}
@Override
public Result queryBlogById(Long id) {
//1.查询blog
Blog blog = getById(id);
if (blog == null){
return Result.fail("笔记不存在");
}
//2.查询blog有关的用户
queryBlogUser(blog);
return Result.ok(blog);
}
private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}
点赞功能
分页查询blog并将blog加上isLiked属性-controller
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
// 修改点赞数量 update tb_blog set liked + 1 where id = ?
// blogService.update()
// .setSql("liked = liked + 1").eq("id", id).update();
return blogService.likeBlog(id);
}
分页查询blog并将blog加上isLiked属性-service
redis中存set,记录每个blog被谁点过赞,key为blog的id,value为点赞的userId
package com.hmdp.service.impl;
import cn.hutool.core.util.BooleanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import static com.hmdp.utils.RedisConstants.BLOG_LIKED_KEY;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private IUserService userService;
@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(this::queryBlogUser);
records.forEach(blog -> {
this.queryBlogUser(blog);
this.isBlogLiked(blog);
});
return Result.ok(records);
}
@Override
public Result queryBlogById(Long id) {
//1.查询blog
Blog blog = getById(id);
if (blog == null){
return Result.fail("笔记不存在");
}
//2.查询blog有关的用户
queryBlogUser(blog);
//3.查询blog是否被点赞
isBlogLiked(blog);
return Result.ok(blog);
}
private void isBlogLiked(Blog blog) {
//1.获取登录用户
Long userId = UserHolder.getUser().getId();
//如果未登录
//2.判断当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + blog.getId();
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
blog.setIsLike(BooleanUtil.isTrue(isMember));
}
@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());
//不要直接判断包装类
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) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}
点赞排行榜
将点赞时间按时间戳作为sortedset的score
通过zscore获取对应元素的score替代zset里面没有sismember的功能
通过zrange获取按照score排序的对应序列的值
技术栈
- 通过zset存储点赞信息,zscore判断set内有没有该元素,zrange获取按照score排序后的前几名
1.通过zset存储点赞信息(26),zscore判断set内有没有该元素(7、18),zrange获取按照score排序后的前几名
private void isBlogLiked(Blog blog) {
//1.获取登录用户
Long userId = UserHolder.getUser().getId();
//如果未登录
//2.判断当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + blog.getId();
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
blog.setIsLike(score != null);
}
@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());
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
//不要直接判断包装类
if (score == null) {
//3.如果未点赞,可以点赞
//3.1.数据库点赞数+1
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
//3.2.保存用户到我们的redis的zset集合 zadd key value score
if (isSuccess){
stringRedisTemplate.opsForZSet().add(key,userId.toString(),System.currentTimeMillis());
}
}else {
//4.如果已点赞,取消点赞
//4.1.数据库点赞数-1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
//4.2.把用户从redis的zset集合里面移除
if (isSuccess){
stringRedisTemplate.opsForZSet().remove(key,userId.toString());
}
}
return Result.ok();
}
private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
知识点
1.SELECT id,phone,password,nick_name,icon,create_time,update_time FROM tb_user WHERE id IN ( 5 , 1 )
这样就先查的1再查5
技术栈
- 通过自定义sql语句,改进由于WHERE id IN ( 5 , 1 )返回结果倒置问题,涉及将list集合内的数据拼接为字符串StrUtil.join
1.通过自定义sql语句,改进由于WHERE id IN ( 5 , 1 )返回结果倒置问题,涉及将list集合内的数据拼接为字符串StrUtil.join(11-18)
@Override
public Result queryBlogLikes(Long id) {
String key = BLOG_LIKED_KEY + id;
//1.查询top5的点赞用户 zrange key 0 4
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());
String idStr = StrUtil.join(",", ids);
//3.根据用户id查询用户 SELECT id,phone,password,nick_name,icon,create_time,update_time FROM tb_user WHERE id IN ( 5 , 1 )
//List<UserDTO> userDTOS = userService.listByIds(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改为自增长
new QueryWrapper<Follow>()是一个条件构造器。
1.将表的id改为自增长
右键表命选择“Modify Table”,点击“id”,勾选“Auto Increment”
2.new QueryWrapper<Follow>()是一个条件构造器(37-38)
QueryWrapper:专门用于封装查询(Select)和删除(Delete)条件的一类。
<Follow>: 泛型标识,指定这个构造条件器是为Follow实体类(对应数据库的follow表)服务的。
.eq("列名", 值):eq是Equal的缩写,表示“相等”。
- 第一个参数是数据库表中的字段名(通常是下划线命名)。
- 第二个参数是数值。
package com.hmdp.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hmdp.dto.Result;
import com.hmdp.entity.Follow;
import com.hmdp.mapper.FollowMapper;
import com.hmdp.service.IFollowService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
/**
* <p>
* 服务实现类
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
@Override
public Result follow(Long followUserId, Boolean isFollow) {
//1.获取登录用户
Long userId = UserHolder.getUser().getId();
//2.判断到底是关注还是取关
if (isFollow){
//3.1.关注,新增数据
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
save(follow);
}else {
//3.2.取关,删除 delete from tb_follow where user_id = ? and follow_user_id = ?
remove(new QueryWrapper<Follow>()
.eq("user_id",userId).eq("follow_user_id",followUserId));
}
return Result.ok();
}
@Override
public Result isFollow(Long followUserId) {
//1.获取登录用户
Long userId = UserHolder.getUser().getId();
//2.查询是否关注 select count(*) from tb_follow where user_id = ? and follow_user_id = ?
Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
//3.判断,该登录用户有没有人关注这个人
return Result.ok(count>0);
}
}
优化
// 这种写法效果一样,但更推荐
remove(new LambdaQueryWrapper
.eq(Follow::getUserId, userId)
.eq(Follow::getFollowUserId, followUserId));
共同关注
改造,将关注列表存在redis里面:key是当前登录的用户id,值是我当前关注的所有用户的id
改造:加上redis(17、25):redis里面存的key是关注其他人的用户,value是被关注的人,也就是key关注了哪些value
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result follow(Long followUserId, Boolean isFollow) {
//1.获取登录用户
Long userId = UserHolder.getUser().getId();
String key = "follows:" + userId;
//2.判断到底是关注还是取关
if (isFollow){
//3.1.关注,新增数据
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
boolean isSuccess = save(follow);
if (isSuccess){
//把关注用户的id,放入redis的set集合 sadd userId followerUserId
stringRedisTemplate.opsForSet().add(key,followUserId.toString());
}
}else {
//3.2.取关,删除 delete from tb_follow where user_id = ? and follow_user_id = ?
boolean isSuccess = remove(new QueryWrapper<Follow>()
.eq("user_id", userId).eq("follow_user_id", followUserId));
//把关注用户的id从redis中移除
if (isSuccess){
stringRedisTemplate.opsForSet().remove(key,followUserId.toString());
}
}
return Result.ok();
}
共同关注-controller
@GetMapping("/common/{id}")
public Result followCommons(@PathVariable("id") Long id){
return followService.followCommons(id);
}
共同关注-service
@Override
public Result followCommons(Long id) {
//求目标用户和当前用户的交集,因为是在目标用户主页查看的共同关注
//1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
String key = "follows:" + userId;
//2.求交集
String key2 = "follows:" + id;
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
if (intersect == null||intersect.isEmpty()){
//无交集
return Result.ok(Collections.emptyList());
}
//3.解析出id
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
//4.查询用户,返回DTO
List<UserDTO> users = userService.listByIds(ids)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(users);
}
关注推送
推模式关注推送
技术栈
- 将blog信息保存到数据库之后,保存到redis中对应粉丝为key的收件箱feed中(11-20)
@Override
public Result saveBlog(Blog blog) {
// 1.获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 2.保存探店笔记
boolean isSuccess = save(blog);
if (!isSuccess){
return Result.fail("新增笔记失败!");
}
//3.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id = ?
List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
//4.推送笔记id给所有粉丝
for (Follow follow : follows) {
//4.1.获取粉丝id
Long userId = follow.getUserId();
//4.2.推送
String key = "feed:" + userId;
stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
}
// 5.返回id
return Result.ok(blog.getId());
}
滚动分页查询
如果中途有人修改了score导致改成上一次的最后一个score的值,分数重复,那就不能只跳过一个;需要跳过和上一次最后一个score大小一样的个数
滚动查询分页参数
max:当前时间戳|上一次查询的最小时间戳
min:0
offset:0|与上一次结果查询到的最小score的个数
count:固定
技术栈
- 基于mybatis自带的listByIds()导致的倒置问题的优化
- minTime(时间戳)、offset的循环覆盖(设计算法)
1.基于mybatis自带的listByIds()导致的倒置问题的优化(31-34)
2.minTime(时间戳)、offset的循环覆盖(设计算法)(14-28)
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
//1.获得当前用户
Long userId = UserHolder.getUser().getId();
//2.获取当前用户的收件箱 ZREVANGEBYSCORE key Max Min
String key = FEED_KEY + userId;
//TypedTuple<String>元组
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 3);
//3.非空判断
if (typedTuples == null||typedTuples.isEmpty()){
return Result.ok();
}
//4.解析数据:blogId、minTime(时间戳)、offset
List<Long> ids = new ArrayList<>(typedTuples.size());
long minTime = 0;
int os = 1;
for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
//4.1.获取id,因为一开始存进去的value就是id的String格式
ids.add(Long.valueOf(typedTuple.getValue()));
//4.2.获取分数,循环覆盖
long time = typedTuple.getScore().longValue();
if (time == minTime){
os++;
}else {
minTime = time;
os = 1;
}
}
//5.根据id查询blog
//不能直接用mybatis自带的listByIds()里面是基于where in (?,?,?)会使得查出来的倒置
String idStr = StrUtil.join(",", ids);
List<Blog> blogs = query()
.in("id",ids).last("ORDER BY FIELD(id,"+idStr+")").list();
for (Blog blog : blogs) {
//5.1.查询blog有关的用户
queryBlogUser(blog);
//5.2.查询blog是否被点赞
isBlogLiked(blog);
}
//6.封装并返回
ScrollResult r = new ScrollResult();
r.setList(blogs);
r.setOffset(os);
r.setMinTime(minTime);
return Result.ok(r);
}

浙公网安备 33010602011771号