達人探店
發布探店筆記
那第一張表block表它里邊的結構呢是這個
首先呢第一個字段是i d,就是主鍵,第二個呢是shop id,就是商戶你發的這個比例啊,它是跟哪個商戶有關系的。第三個呢用戶id就是誰發的這篇筆記,第四個呢標題,第五個呢是照片,照片呢最多不超過九張,多個呢以道號隔開,所以呢他這個是一個字段,里面包含了多張圖片的
然后呢再往下呢content是探店的文字描述啊,然后再往下呢還有兩個,一個叫點贊的數量啊,還有一個是評論的數量啊,點贊了不一定會評論是吧,所以這兩個是分離去計數,再往下time update time,創建時間更新時間
?實際生產中不會加外鍵,這會給每次操作進行驗證,會大大降低性能,配了負載均衡的,記得啟動兩個服務,否則前端頁面顯示不全
?uploadcontrol實現了功能,而因此呢啊在這個地方,我們會定義一個叫做image upload dr文件上傳的地址,那么這個地址所以我們需要把它改成什么,我們當前的這個目錄啊。你需要找到你自己的index的目錄
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
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 {@Resourceprivate IBlogService blogService;@PostMappingpublic Result saveBlog(@RequestBody Blog blog) {// 獲取登錄用戶UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());// 保存探店博文blogService.save(blog);// 返回idreturn 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寫一下如下
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 {@Resourceprivate IUserService userService;@Overridepublic Result queryBlogById(Long id) {//1.查詢blogBlog 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());}@Overridepublic Result queryHotBlog(Integer current) {// 根據用戶查詢Page<Blog> page = query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 獲取當前頁數據List<Blog> records = page.getRecords();// 查詢用戶//lamrecords.forEach(this::queryBlogUser);return Result.ok(records);}
}
?
點贊
你發現一個人可以無限次的點贊,這是因為代碼直接數據庫中++,不做任何判斷限定
需求:
- 同一個用戶只能點贊一次,再次點擊則取消點贊
- 如果當前用戶已經點贊,則點贊按鈕高亮顯示(前端已實現,判斷字段Blog類的isLike屬性)
?由于需要記錄點贊人和被點贊人,還有點贊狀態(點贊、取消點贊),還要固定時間間隔取出 Redis 中所有點贊數據,分析了下 Redis 數據格式中 Hash 最合適。blog.java里面添加有這個(實際用的set)
/*** 是否點贊過了* boolean類型字段不能is打頭,阿里明文規定*/ @TableField(exist = false) private Boolean isLike;
?如果業務量比較多可以做個定時同步,隔一段時間同步一次點贊信息到數據庫
@PutMapping("/like/{id}")public Result likeBlog(@PathVariable("id") Long id) {return blogService.likeBlog(id);}
?修改或者添加這幾個函數
@Overridepublic Result queryBlogById(Long id) {//1.查詢blogBlog 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));}@Overridepublic 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);}@Overridepublic 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. 數據庫點贊數 + 1boolean 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. 數據庫點贊數 - 1boolean 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不剛好五個嘛,那就前五名啊,就這么來查的啊,所以呢這個將來我們要查前五名啊,也就知道該怎么做了。
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"
@Overridepublic 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. 數據庫點贊數 + 1boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();// 3.2. 保存用戶到Redis的set集合 zadd key value scoreif(isSuccess) {stringRedisTemplate.opsForZSet().add(key, userId.toString(),System.currentTimeMillis());//此處應該拷貝if(isSuccess)判斷數據更新成功才更新redis}}else {// 4. 如果已點贊,取消點贊// 4.1. 數據庫點贊數 - 1boolean 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的緩存刪除了
神了,有的帖子可以取消贊,有的就不能,哦牛批
@GetMapping("/likes/{id}")public Result queryBlogLikes(@PathVariable("id") Long id) {return blogService.queryBlogLikes(id);}
//使用map方法將每個元素轉換為對應的Long類型,并使用collect方法將結果收集到一個List中@Overridepublic Result queryBlogLikes(Long id) {String key = BLOG_LIKED_KEY + id;// 1. 查詢top5的點贊用戶 zrange key 0 4Set<String> top5 = stringRedisTemplate.opsForZSet().range(key,0,4);if (top5 == null || top5.isEmpty()) {return Result.ok(Collections.emptyList());}// 2. 解析出其中的用戶idList<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,不然賬號退出登錄再打開首頁會殘留上一個賬號的點贊狀態。
//使用map方法將每個元素轉換為對應的Long類型,并使用collect方法將結果收集到一個List中@Overridepublic Result queryBlogLikes(Long id) {String key = BLOG_LIKED_KEY + id;// 1. 查詢top5的點贊用戶 zrange key 0 4Set<String> top5 = stringRedisTemplate.opsForZSet().range(key,0,4);if (top5 == null || top5.isEmpty()) {return Result.ok(Collections.emptyList());}// 2. 解析出其中的用戶idList<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有字幕