day09-达人探店
功能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)重新启动项目:可以看到之前的顺序已经变为真正的顺序了
day09-达人探店的更多相关文章
- day01-项目介绍&功能实现
项目介绍&功能实现 1.项目介绍&环境搭建 一个以社交平台为核心的轻电商项目,功能如下: 短信登录.商户查询缓存.优惠券秒杀.达人探店.好友关注.附近的商户.用户签到.UV统计 1.1 ...
- 马蜂窝视频编辑框架设计及在 iOS 端的业务实践
(马蜂窝技术公众号原创内容,ID: mfwtech) 熟悉马蜂窝的朋友一定知道,点击马蜂窝 App 首页的发布按钮,会发现发布的内容已经被简化成「图文」或者「视频」. 长期以来,游记.问答.攻略等图文 ...
- 听说你想在 WordPress 网站上嵌入 PPT ?
年底了,想在 WordPress 博客上展示自己的春节旅行计划,尝试在文章中插入一个旅行计划 PPT 结果长这个样子 你有没有遇到同样的情况,懊恼网页支持展示的内容无法满足我们的需求: 想展示年度家庭 ...
- Redis入门到实战
一.Redis基础 Redis所有的命令都可以去官方网站查看 1.基本命令 keys * 查找所有符合给定模式pattern(正则表达式)的 key .可以进行模糊匹配 del key1,key2,. ...
- 中文分词工具探析(二):Jieba
1. 前言 Jieba是由fxsjy大神开源的一款中文分词工具,一款属于工业界的分词工具--模型易用简单.代码清晰可读,推荐有志学习NLP或Python的读一下源码.与采用分词模型Bigram + H ...
- 中文分词工具探析(一):ICTCLAS (NLPIR)
1. 前言 ICTCLAS是张华平在2000年推出的中文分词系统,于2009年更名为NLPIR.ICTCLAS是中文分词界元老级工具了,作者开放出了free版本的源代码(1.0整理版本在此). 作者在 ...
- C#开发微信门户及应用(24)-微信小店货架信息管理
在前面微信小店系列篇<C#开发微信门户及应用(22)-微信小店的开发和使用>里面介绍了一些微信小店的基础知识,以及<C#开发微信门户及应用(23)-微信小店商品管理接口的封装和测试& ...
- C#开发微信门户及应用(23)-微信小店商品管理接口的封装和测试
在上篇<C#开发微信门户及应用(22)-微信小店的开发和使用>里面介绍了一些微信小店的基础知识,以及对应的对象模型,本篇继续微信小店的主题,介绍其中API接口的封装和测试使用.微信小店的相 ...
- C#开发微信门户及应用(22)-微信小店的开发和使用
在做企业电子商务方面,微信小店虽然较淘宝天猫等起步较晚,但是作为一个电商平台,这个影响力不容忽视,结合微信的特点和便利,微信小店具有很好的粘合性和广泛的用户基础,因此花费一定的时间,在这方面做深入的研 ...
- Silverlight 手鼓达人-仿太鼓达人 开源
Silverlight 手鼓达人-仿太鼓达人 介绍 手鼓达人是本人2012年中silverlight最火的一段时间开发的,本来目的只是想研究一下silverlight做游戏和做应用有何不同,但是后面 ...
随机推荐
- P5192 有源汇上下界最大流总结
之前听学长讲解时,只听了大体思路就跑路了,没有听到具体细节.后面在考虑出度多的点具体向虚拟源点连边还是虚拟汇点连边时,只凭直觉直接向源点连边,然后就一直WA,直到后来中午听同学讲解才反应过来,白白浪费 ...
- P3128 [USACO15DEC]Max Flow P(树上倍增和树链剖分)
思路1(树上倍增$ + $树上差分) 每次都修改一条从\(u\)到\(v\),不就是树上差分的专门操作吗?? 直接用倍增求\(LCA\),每次\(d[u]++,d[v]++,d[LCA(u,v)]-- ...
- VSCode 开发Vue + ElementUI
参考 (1)VSCode 开发Vue + ElementUI (2)玩转VSCode-完整构建VSCode开发调试环境 (shuzhiduo.com) (3)使用vscode搭建vue项目并引用ele ...
- Spring设计模式——单例模式
单例模式 单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点. 单例模式是创建型模式. 饿汉单例模式 饿汉单例模式在类的加载时候就立即初始 ...
- archlinux基本安装、以及图形化界面
磁盘刻录 在windows下载磁盘刻录工具 rufus,官网:https://rufus.ie/zh/ 中文界面,实在不是可以搜索一下磁盘刻录教程 在linux下使用balena-etcher,官网: ...
- 前端开发工具 VS Code 安裝及使用
一.下载地址 https://code.visualstudio.com/ 下载完后,傻瓜式安装即可 关注公众号"Java程序员进阶"回复"vs"也可获取 二. ...
- DES算法流程
初始置换IP 表格的使用方法: 将输入的64bit的明文从1开始标号,依次放入到IP初始置换表中数字对应的位置.填充完毕后,按照行优先的顺序从第1行开始依次读取获得输出. 16轮轮结构 整体结构 因为 ...
- Teamcenter_NX集成开发:通过NXOpen查询零组件是否存在
之前用过NXOpen PDM的命名空间下的类,现在记录一下通过PDM命名空间下的类查询Teamcenter零组件的信息,也可以用来判断该零组件是否存在. 1-该工程为DLL工程,直接在NX界面调用,所 ...
- 怎么在php7项目中安装event扩展
今天就跟大家聊聊有关怎么在php7项目中安装event扩展,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获. 安装支持库libevent wget ...
- VUE中的$set与$delete的原理
我们上文说了,Vue 是通过 Object.defineProperty 和重写数组的原型方法来达到监控数据的目的.但是,在某些情况下,上面两种方案无法做到监控数据的变化,例如: (1):当我们给对象 ...