达人探店
发布探店笔记

那第一张表block表它里边的结构呢是这个
首先呢第一个字段是i d,就是主键,第二个呢是shop id,就是商户你发的这个比例啊,它是跟哪个商户有关系的。第三个呢用户id就是谁发的这篇笔记,第四个呢标题,第五个呢是照片,照片呢最多不超过九张,多个呢以道号隔开,所以呢他这个是一个字段,里面包含了多张图片的
然后呢再往下呢content是探店的文字描述啊,然后再往下呢还有两个,一个叫点赞的数量啊,还有一个是评论的数量啊,点赞了不一定会评论是吧,所以这两个是分离去计数,再往下time update time,创建时间更新时间
实际生产中不会加外键,这会给每次操作进行验证,会大大降低性能,配了负载均衡的,记得启动两个服务,否则前端页面显示不全
uploadcontrol实现了功能,而因此呢啊在这个地方,我们会定义一个叫做image upload dr文件上传的地址,那么这个地址所以我们需要把它改成什么,我们当前的这个目录啊。你需要找到你自己的index的目录
java
package com.hmdp.utils;
public class SystemConstants {
public static final String IMAGE_UPLOAD_DIR = "D:\\lesson\\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;
}
成功上传,也可以多 次上传图片啊
点击发布,那么会自动跳到主页里,在app首页,我们可以看到,同样的数据库的tb blog也可以看得到这条消息
实现查看发布探店笔记的接口
需求:点击首页的探店笔记,会进入详情页面,实现该页面的查询接口

好的,同学们,我们来继续分析这个接口。首先,接口相关的信息我已经提前分析了一下。请求的方式是GET,这一点我们通过查看就能知道,路径是/block
,后面跟着的是id
,这个也没有问题。然后请求参数自然就是这个id
了,也就是当前这篇博客的ID。最后返回值是什么呢?返回值根据id
查询这个博客(Blog),那返回值是不是应该就是Blog呀?理论上讲就是如此,但是大家别忘了,其实我们发布的任何一篇探店笔记,它里边都包含有这个用户的信息。然后才是这个图片,还有标题等等。
所以说我们在详情页面展示的时候,除了要展示这里的图片、标题、内容以外,那这个发布这篇笔记的用户是不是也应该展示出来,这样其他用户看到这篇博客以后,如果感兴趣,是不是可以直接关注当前的用户了。所以说呢,我们返回的结果中除了博客信息以外,还应该包含对应的用户信息。那我们怎么样才能在结果中包含两部分内容呢?
那我们回到IDEA看一下,在这里实现起来其实非常的简单,有两种选择。第一种选择就是在我们的这个Blog类里边加一个用户的成员变量就行了。那这样一来,我们只要把查询到的跟这个用户ID有关的这个用户(User)的对象存进去,是不是就OK了?但在这呢,我采用了一种简化的方法,怎么简化呢?就是在我们的Blog类里边加了两个字段,你看这个博客类里面的其他字段,店铺ID(shop id)、用户ID(user id)、还有我们的标题、图片、内容等等,这些都是数据库字段。而唯独这两个字段一个叫图标(icon)、一个叫姓名(name),这两个就是我们的用户字段了。
那我们的用户除了有ID以外,还有就是图标和姓名,剩下的敏感字段我们就不返回了,只返回这三个足够页面显示就可以了。而这两个字段呢我们加了@TableField
注解,它代表的含义就是当前字段不属于博客所定义的表,你表不是博客表吗?而这两个字段不在表里面,所以说我加了这样一个注解,那将来呢我们手动的要维护这两个字段就可以了。这样大家应该就能理解了吧。
那下面呢我们就可以去实现一下了,我们找到博客控制器(BlogController),我们在这儿去写这个接口。实现的时候,我们首先要根据id
查询到对应的博客(Blog)信息,然后根据博客信息中的用户ID(user id),再去查询用户(User)信息。查询到用户信息后,我们将用户信息添加到博客信息中,最后将包含用户信息的博客信息返回给前端。剩下的敏感字段我们就不返回,只返回这三个足够页面显示就可以了,而这两个字段呢我们加了table field,exit等于false这样一个注解,它代表的含义就是说当前字段不属于blog它所定义的表,而我这俩字段不在表里面,写control
java
package com.hmdp.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.service.IBlogService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author 虎哥
* @since 2021-12-22
*/
@RestController
@RequestMapping("/blog")
public class BlogController {
@Resource
private IBlogService blogService;
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
blogService.save(blog);
// 返回id
return Result.ok(blog.getId());
}
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
// 修改点赞数量
blogService.update()
.setSql("liked = liked + 1").eq("id", id).update();
return Result.ok();
}
@GetMapping("/of/me")
public Result queryMyBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
// 根据用户查询
Page<Blog> page = blogService.query()
.eq("user_id", user.getId()).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
return Result.ok(records);
}
@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);
}
}
service加一下就行,impl写一下如下
java
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 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());
}
@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();
// 查询用户
//lam
records.forEach(this::queryBlogUser);
return Result.ok(records);
}
}
点赞
你发现一个人可以无限次的点赞,这是因为代码直接数据库中++,不做任何判断限定
需求:
- 同一个用户只能点赞一次,再次点击则取消点赞
- 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)
由于需要记录点赞人和被点赞人,还有点赞状态(点赞、取消点赞),还要固定时间间隔取出 Redis 中所有点赞数据,分析了下 Redis 数据格式中 Hash 最合适。blog.java里面添加有这个(实际用的set)
/**
* 是否点赞过了
* boolean类型字段不能is打头,阿里明文规定
*/
@TableField(exist = false)
private Boolean isLike;
如果业务量比较多可以做个定时同步,隔一段时间同步一次点赞信息到数据库
java
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
return blogService.likeBlog(id);
}
修改或者添加这几个函数
java
@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:" + blog.getId();
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
blog.setIsLike(BooleanUtil.isTrue(isMember));
}
@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);
}
@Override
public Result likeBlog(Long id) {
// 1. 获取登录用户
Long userId = UserHolder.getUser().getId();
// 2. 判断当前登录用户是否已经点赞
String key = "blog:liked:" + 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());//此处应该拷贝if(isSuccess)判断数据更新成功才更新redis
}
}else {
// 4. 如果已点赞,取消点赞
// 4.1. 数据库点赞数 - 1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
//这里有线程安全问题,判断和修改不是原子.也有事务的问题,update了两条数据
// 4.2. 把用户从Redis的set集合移除
stringRedisTemplate.opsForSet().remove(key, userId.toString());
}
return Result.ok();
}
/*如果A线程查到当前用户没点赞,没等进行redis更新操作,B线程进来查询redis当前用户也没进行点赞,就会赞两次
一般账号都会设置限制在1-5台设备左右登录,本来并发度就不高,并且这类数据也不要求强一致性,安全问题还是很低很低的。*/
实现点赞和高亮
点赞排行榜

好的同学们,刚才我已经为大家总结了接口的信息,并且进行了梳理。可以看到,请求方式是GET请求,路径是`/blog/likes`或者`/blog/likes/d`,这里的`d`可能是指"点赞"的意思,后面跟着的是当前这篇笔记的ID。我们要查的是给这篇笔记点赞的人,所以参数就是`blogId`了。返回的自然是点赞的前五名了,所以是一个集合,集合里装的是用户信息。这里我们用了`UserDTO`,因为`UserDTO`里把敏感信息都已经给去掉了,所以不用担心数据泄漏的风险。在这个地方,我们要返回的这个集合就是所谓的前五名,也就是排行榜里面的前五名。但在这就有一个问题了,我上哪去查这前五名的用户去,而且是点赞的用户。
那同学们,我们点赞的信息都放在哪儿啊?而在上一节课当中,我们点赞是基于一个Redis的Set集合来实现的,也就是说我现在要想查询这些用户信息,我是不是得去Set集合来查?但是我要的是前五名,Set集合是所有的,那我就必须把Set集合的元素给它排个序啊。但是同学们,Set集合是有虚的吗?显然不是,我们当初为什么选Set集合呀,是因为当初我们的业务需求是:第一要存多个元素,也就是集合;第二点为一。那这两点他都满足呀。但现在我们又多了个需求,我们要做排行榜,所以我们的需求变成了三个需求了:要能够存多个元素,要能为一,完了还能排序。那Set集合就不行了,我们该选谁?
哎,在这儿呢我们去做一个对比啊,目前为止我们学习的Redis集合总共就三种,那这三种都是集合,所以第一条是不是就满足了,那么还剩两条吗,那一个就是排序,一个就是唯一。在这呢我给大家对比的就是这三点,哪个呢,第一排序,第二唯一性,第三个多了一个,我们要对比一下他们的查找元素时的方式。为什么呢?因为将来我们除了去做排序以外,我们还有一个需求就是点赞,点赞我们要判断它存不存在,对不对,而你要判断存不存在,不就是查找吗?所以说这个也要去做对比。那来一个看啊,首先Set集合,它首先是可以做排序的,为什么呢?他是个链表吗,那他按什么排,按照添加顺序,还有`lpush`和`rpush`两种,对不对?如果说我们所有的元素都是按`lpush`就插入的,那元素呢先插入进去的,是不是在最后,后插入的,在最前变成了一个按照插入顺序的倒序排序,这个跟我们点赞排行榜不太相符,但是如果我们全部采用`rpush`呢,先点赞在最前边,后点在最后边,这样是不是就刚好符合?所以说它是支持这种排序的啊,而Set集合就不好意思了,不支持啊,无法排序,而我们的Sorted Set,这个咱们之前是不是也学过呢?它可以排序的,按什么排按分数,也就是说我们存入`zset`的元素啊,除了元素本身以外,还要带一个score,那就分数啊,那么这个score分数呢,可以是用户自定义的任意的东西,那如果我把它按照时间戳啊作为score值存进去,那这样一来是不是也能实现按添加顺序排序了?那想添加越早,时间说是不是越小,那天下越晚,时间戳越大,这样天然是不是就带有一个顺序了?因此Sorted Set能不能实现按照时间排序没问题,这也就是说符合要求的是不是就有两个啊,从排序上来讲啊,那再从唯一性上来讲啊,直接把list排除了啊,list是链表,无法保证数据的唯一啊,他只管一共要往里面加,但是呢Set和Sorted Set都满足,因为他们底层都有一个哈希啊哈表,接下来它可以判断元素是否存在,从而把一些重复元素给剔除或者是覆盖,那从查找方式上来讲呢,我们的list查找,我们刚才讲过了,它底层是什么呢,是链表,所以说呢它只能按角标查找元素,或者是首尾查找啊,那意思我想知道一个元素存不存在,它的做法只能是便利一边啊,但是Set和Sorted Set就不一样了,那么这两个呢因为他们底层采用的是哈希表,所以说他们可以根据元素做这种哈希运算,快速定位到对应的那个数组位置,然后呢去判断是否存在,所以他们的查找更加高效,对不对,所以从这三点来看,哪个更适合啊,其实把前两点看完就已经找到答案了,是不是Sorted Set更符合我们的业务需求?同学们,只要你掌握了啊这种Redis数据结构的特点,然后你再结合你的业务需求,是不是一目了然,就能快速定位到合适的数据类型?那这里呢我们就需要用Sorted Set来代替我们的Set集合,改造我们之前的点赞业务了,但是在这就有一个问题了,Sorted Set虽然跟Set类似,但是还是存在差异的,很多的命令上是不一样的,那我们来看一下啊,在这呢我们之前使用这个Set的时候,我们去添加是`sadd`,对不对啊,`sadd`去添加一个元素,我们就判断是否存在叫`sismember`,它可以直接判断一个元素是否存在,但是现在我们用的是Sorted Set,Sorted Set里面就不存在啊,这个`sismember`这样一个命令了,那它添加元素是一样的,都是去`zadd`,但是呢他判断元素是否存在,它没有一个叫`sismember`的没有,那怎么办啊,那这里呢我们只能用一种别的方式啊,在这儿呢我们会使用一个叫做`zscore`的,意思是获取指定一个元素对应的分数,那为什么用它可以判断元素是否存在呢?我去获取元素的分数,元素如果存在,返回的自然就是分数,元素不存在,返回的是不是就是空了啊,你看我们可以试一下,现在我们通过这个`zadd`啊去添加这个元素啊,比如说k叫`z1`啊,然后呢我一啊,这是分数吗,`m1`,`m2`,`m3`,这样我是不是一下添加了三个元素,好全部添加进去了,那接下来呢我们就用`zscore`啊去查看一下,这个`z1`里边的`m1`这个元素,它的分数是不是查到了`m2`,查到了`m3`,查到了`m4`,不存在,看到没有,返回的是不是`null`,也就是空,所以说我们完全可以通过查分数的形式查到了就存在查不到是不是不存在,来判断元素是否存在,其他的就没什么太大差别了啊,那当然最后我们要去查排行榜,我们怎么查,这个之前咱们讲过,其实就用`zrange`,`range`是什么啊,`range`查询的是按范围查吗,就是你要查找哪个范围内的,它天然的会帮你做排序嘛,对不对,假如说我们按时间啊,时间戳插入,那么它会天然的按照时间戳从小到大排序,那显然啊最早插入的是不是就在最前,那这时我们要查前五名,其实就是查什么呢,它里边从0~4的这些啊,01234不刚好五个嘛,那就前五名啊,就这么来查的啊,所以呢这个将来我们要查前五名啊,也就知道该怎么做了。
java
127.0.0.1:6379> ZADD z1 1 m1 2 m2 3 m3
(integer) 3
127.0.0.1:6379> ZSCORE z1 m1
"1"
127.0.0.1:6379> ZSCORE z1 m3
"3"
127.0.0.1:6379> ZSCORE z1 m9
(nil)
127.0.0.1:6379> ZRANGE z1 0 4
1) "m1"
2) "m2"
3) "m3"
java
@Override
public Result likeBlog(Long id) {
// 1. 获取登录用户
Long userId = UserHolder.getUser().getId();
// 2. 判断当前登录用户是否已经点赞
String key = "blog:liked:" + 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的set集合 zadd key value score
if(isSuccess) {
stringRedisTemplate.opsForZSet().add(key, userId.toString(),System.currentTimeMillis());//此处应该拷贝if(isSuccess)判断数据更新成功才更新redis
}
}else {
// 4. 如果已点赞,取消点赞
// 4.1. 数据库点赞数 - 1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
//这里有线程安全问题,判断和修改不是原子.也有事务的问题,update了两条数据
// 4.2. 把用户从Redis的set集合移除
stringRedisTemplate.opsForSet().remove(key, userId.toString());
}
return Result.ok();
}
private void isBlogLiked(Blog blog) {
// 1. 获取登录用户
Long userId = UserHolder.getUser().getId();
// 2. 判断当前登录用户是否已经点赞
String key = "blog:liked:" + blog.getId();
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
blog.setIsLike(score!=null);
}
这里记得改完调试的时候把之前Redis的缓存删除了
神了,有的帖子可以取消赞,有的就不能,哦牛批
java
@GetMapping("/likes/{id}")
public Result queryBlogLikes(@PathVariable("id") Long id) {
return blogService.queryBlogLikes(id);
}
java
//使用map方法将每个元素转换为对应的Long类型,并使用collect方法将结果收集到一个List中
@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());
// 3. 根据用户id查询用户
List<UserDTO> userDTOS = userService.listByIds(ids).stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
// 4. 返回
return Result.ok(userDTOS);
}
private void isBlogLiked(Blog blog) {
// 1. 获取登录用户
//return前可以把isLike设为null或者false,不然账号退出登录再打开首页会残留上一个账号的点赞状态。
UserDTO user = UserHolder.getUser();
if (user == null) {
return;
}
Long userId = UserHolder.getUser().getId();
// 2. 判断当前登录用户是否已经点赞
String key = "blog:liked:" + blog.getId();
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
blog.setIsLike(score!=null);
}

return前可以把isLike设为null或者false,不然账号退出登录再打开首页会残留上一个账号的点赞状态。
java
//使用map方法将每个元素转换为对应的Long类型,并使用collect方法将结果收集到一个List中
@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());
// 3. 根据用户id查询用户
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);
}

foottxt有字幕