功能04-達人探店 5.功能04-達人探店 5.1發佈&查看探店筆記 5.1.1發佈探店筆記 探店筆記類似點評網站的評價,往往是圖文結合。對應的表有兩個: tb_blog:探店筆記表,包含筆記中的標題、文字、圖片等 tb_blog_comments:其他用戶對探店筆記的評價 /*表: tb_blog ...
功能04-達人探店
5.功能04-達人探店
5.1發佈&查看探店筆記
5.1.1發佈探店筆記
探店筆記類似點評網站的評價,往往是圖文結合。對應的表有兩個:
- tb_blog:探店筆記表,包含筆記中的標題、文字、圖片等
- tb_blog_comments:其他用戶對探店筆記的評價
/*表: tb_blog*/
CREATE TABLE `tb_blog` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`shop_id` bigint(20) NOT NULL COMMENT '商戶id',
`user_id` bigint(20) unsigned NOT NULL COMMENT '用戶id',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '標題',
`images` varchar(2048) NOT NULL COMMENT '探店的照片,最多9張,多張以","隔開',
`content` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '探店的文字描述',
`liked` int(8) unsigned DEFAULT '0' COMMENT '點贊數量',
`comments` int(8) unsigned DEFAULT NULL COMMENT '評論數量',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT
/*表: tb_blog_comments*/
CREATE TABLE `tb_blog_comments` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`user_id` bigint(20) unsigned NOT NULL COMMENT '用戶id',
`blog_id` bigint(20) unsigned NOT NULL COMMENT '探店id',
`parent_id` bigint(20) unsigned NOT NULL COMMENT '關聯的1級評論id,如果是一級評論,則值為0',
`answer_id` bigint(20) unsigned NOT NULL COMMENT '回覆的評論id',
`content` varchar(255) NOT NULL COMMENT '回覆的內容',
`liked` int(8) unsigned DEFAULT NULL COMMENT '點贊數',
`status` tinyint(1) unsigned DEFAULT NULL COMMENT '狀態,0:正常,1:被舉報,2:禁止查看',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT
點擊首頁最下方菜單欄中的“+”按鈕,即可發佈探店圖文:
需要註意的是:
發佈照片和發佈筆記這兩個功能是分離的。因為上傳照片的功能不僅僅是發佈筆記時需要用到,其他業務也有需求,因此上傳照片是一個獨立功能。
我們在發佈筆記時,點擊上傳照片,會先向服務端發出一個請求,實現圖片上傳。上傳成功以後,服務端會返回圖片的地址(即上傳之後可訪問的該圖片地址),這個地址將來會作為表單的參數,在發佈筆記的時候一起提交到後臺(也就是說,在提交筆記的時候,我們提交的就不是照片本身了,而是上傳成功後的圖片地址)。
(1)上傳圖片功能
上傳圖片的功能已經提前實現了,詳見UploadController.java及其介面
上傳的圖片其實是放在放在前端伺服器中的,這裡為了模擬,放在了D盤的前端項目(nginx-1.18.0)的目錄下:
同時,需要將代碼中保存的目錄修改為對應的目錄:
(2)發佈筆記功能
發佈筆記的功能也提前實現了,詳見BlogController.java及其介面
(3)測試
點擊+號進入如下頁面,點擊上傳照片,一次可以上傳多張圖片:
每次上傳圖片成功,後端都會返回該圖片可訪問的圖片地址:
點擊發佈後,可以在個人主頁中看到發佈的文章:
5.1.2查看探店筆記
實現查看筆記的介面。需求:點擊首頁的筆記,可以進入詳情頁面,實現該頁面的查詢介面。
筆記的詳情頁面需要顯示:
- 筆記信息
- 發佈的用戶信息(用戶id、用戶昵稱、用戶頭像)
代碼實現
(1)Blog.java,筆記實體類
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;
/**
* 筆記實體
*
* @author 李
* @version 1.0
*/
@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;
}
(2)BlogMapper.java
package com.hmdp.mapper;
import com.hmdp.entity.Blog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* Mapper 介面
*
* @author 李
* @version 1.0
*/
public interface BlogMapper extends BaseMapper<Blog> {
}
(3)IBlogService.java
package com.hmdp.service;
import com.hmdp.dto.Result;
import com.hmdp.entity.Blog;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 服務類
*
* @author 李
* @version 1.0
*/
public interface IBlogService extends IService<Blog> {
Result queryHotBlog(Integer current);//分頁查詢blog
Result queryBlogById(Long id);//根據id查詢blog
}
(4)BlogServiceImpl.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;
/**
* 服務實現類
*
* @author 李
* @version 1.0
*/
@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);
//2.查詢blog有關的用戶
if (blog == null) {
return Result.fail("筆記不存在!");
}
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());
}
}
(5)BlogController.java
package com.hmdp.controller;
import com.hmdp.dto.Result;
import com.hmdp.service.IBlogService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 前端控制器
*
* @author 李
* @version 1.0
*/
@RestController
@RequestMapping("/blog")
public class BlogController {
@Resource
private IBlogService blogService;
@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);
}
}
(6)測試:重啟項目,點擊筆記,可以查看筆記詳情
5.2點贊
5.2.1需求分析
在首頁的探店筆記排行榜和探店圖文詳情頁面都有點贊的功能:
需求:
-
同一個用戶只能點贊一次,再次點擊則取消點贊
-
如果當前用戶已經點贊,則點贊按鈕高亮顯示(前端已實現,判斷欄位Blog類的isLike屬性)
實現步驟:
-
給Blog類中添加一個isLike欄位,標識是否被當前用戶點贊
-
修改點贊功能,利用Redis的set集合判斷是否點贊過,未點贊過則點贊數+1,已點贊過則再次點贊時點贊數-1
blog的id作為key,點贊的用戶id作為value
-
修改根據id查詢Blog的業務,判斷當前用戶是否點贊過,賦值給isLike欄位
用於blog詳情的點贊顯示
-
修改分頁查詢Blog業務,判斷當前登錄用戶是否點贊過,賦值給isLike欄位
用於一頁blog時的所有點贊顯示
5.2.2代碼實現
(1)給Blog類中添加一個isLike欄位,標識是否被當前用戶點贊
(2)修改IBlogService,添加方法聲明
Result likeBlog(Long id);
(3)修改BlogServiceImpl
- 實現方法likeBlog():修改點贊功能,利用Redis的set集合判斷是否點贊過,未點贊過則點贊數+1,已點贊過則再次點贊時點贊數-1
- 修改BlogServiceImpl的queryBlogById()方法和queryHotBlog(),在查詢blog信息的同時,查詢當前用戶有沒有點贊過該blog
註意判斷當前用戶有沒有登錄
package com.hmdp.service.impl;
import ...
/**
* 服務實現類
*
* @author 李
* @version 1.0
*/
@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 -> {
//查詢發佈blog的user
this.queryBlogUser(blog);
//查詢當前用戶有沒有點贊過該blog
this.isBlogLiked(blog);
});
return Result.ok(records);
}
@Override
public Result queryBlogById(Long id) {
//1.查詢blog
Blog blog = getById(id);
//2.查詢blog有關的用戶
if (blog == null) {
return Result.fail("筆記不存在!");
}
queryBlogUser(blog);
//3.查詢blog是否被點贊了
isBlogLiked(blog);
return Result.ok(blog);
}
private void isBlogLiked(Blog blog) {
//1.獲取當前登錄用戶
if (UserHolder.getUser() == null) {
return;//如果當前用戶未登錄
}
Long userId = UserHolder.getUser().getId();
//2.判斷當前登錄用戶是否已經點贊了
// (去redis的set集合中判斷 SISMEMBER key member)
String key = "blog:liked:" + 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();
if (userId == null) {
return Result.fail("用戶未登錄");
}
//2.判斷當前登錄用戶是否已經點贊了
// (去redis的set集合中判斷 SISMEMBER key member)
String key = "blog:liked:" + id;
Boolean isMember =
stringRedisTemplate.opsForSet().isMember(key, userId.toString());
//3.如果未點贊
if (BooleanUtil.isFalse(isMember)) {
//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();
}
public void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}
(4)修改BlogController,添加方法likeBlog()
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
return blogService.likeBlog(id);
}
(5)測試:啟動項目,已登錄用戶第一次點贊時,點贊數+1,圖標高亮;第二次點贊時,點贊數-1,圖標變回灰色。
redis中的數據:set結構,key為blogId,value為userId
如果用戶未登錄,點贊時則會自動跳轉到登錄頁面:
5.3點贊排行榜
5.3.1需求分析
在探店筆記的詳情頁面,應該把筆記點贊的用戶信息顯示出來,比如最早點贊的TOP5,形成點贊排行榜:
實現查詢點贊排行榜的介面:
需求:按照點贊時間先後排序,返回Top5的用戶。
之前我們使用的是redis中的Set結構,對於點贊功能來說,要求數據唯一且可方便查找。在此基礎上,點贊排行榜功能還要求對數據進行排序,因此我們選用SortedSet結構實現,並對之前的點贊功能進行改造。
zset的整體結構:key value score(key存儲blogId,value存儲點贊的userId,score可以存儲當前點贊的時間戳)
zset結構沒有判斷元素是否存在的命令,但可以查找指定元素的score,根據這個命令,判斷指定的元素的score,如果score不存在,則該元素不存在。
ZSCORE key member
summary: Get the score associated with the given member in a sorted set
since: 1.2.0
例如:
127.0.0.1:6379> ZADD z1 1 m1 2 m2 3 m3
(integer) 3
127.0.0.1:6379> ZSCORE z1 m4 #側面判斷m4不存在
(nil)
127.0.0.1:6379> ZSCORE z1 m1
"1"
查詢排行(比較score)則使用 zrange 命令:
127.0.0.1:6379> ZRANGE z1 0 4 #查詢排行前5名
1) "m1"
2) "m2"
3) "m3"
5.3.2代碼實現
(1)IBlogService增加方法聲明queryBlogLikes
public interface IBlogService extends IService<Blog> {
...
Result queryBlogLikes(Long id);
}
(2)修改BlogServiceImpl:
- 修改之前的點贊功能,將其用到的set結構改為zset結構(修改isBlogLiked和likeBlog方法)
- 實現點贊排行功能--queryBlogLikes()
//判斷當前用戶是否點贊過該blog
private void isBlogLiked(Blog blog) {
//1.獲取當前登錄用戶
if (UserHolder.getUser() == null) {
return;//如果當前用戶未登錄
}
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();
if (userId == null) {
return Result.fail("用戶未登錄");
}
//2.判斷當前登錄用戶是否已經點贊了
String key = BLOG_LIKED_KEY + id;
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
//3.如果未點贊(score為null,證明該用戶不存在zset中,即未點贊)
if (score == null) {
//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();
}
//根據blogId返回點贊該blog的top5的用戶信息
@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);
}
(3)修改 BlogController,增加方法
@GetMapping("/likes/{id}")
public Result queryBlogLikes(@PathVariable("id") Long id) {
return blogService.queryBlogLikes(id);
}
(4)測試:使用不同的用戶賬號給同一篇探店blog點贊,成功顯示點贊的用戶信息:
但是顯示的順序出現了問題:如下所示,分別有三個用戶。正確的點贊順序如redis緩存所示:1033,1,2
但是從資料庫中查找返回的用戶順序卻是:1,2 ,1033,前端顯示的用戶頭像id也是按照1,2,1033的順序,這顯然不符合我們要的順序。
我們返回看之前的代碼:
代碼底層發出的sql語句如下:可以發現傳入的參數順序是正確的(1033,1,2),但是返回的數據順序並不和我們的入參一致:
怎麼保證使用IN子句的時候,返回的數據結果和參數的順序一致呢?
解決方法:使用 order by 指定排序
(5)修改queryBlogLikes()方法,自定義查詢語句:
(6)重新啟動項目:可以看到之前的順序已經變為真正的順序了