一、发布探店笔记

1.1 需求分析

探店笔记类似点评网站的评价,往往是图文结合。对应的表有两个:

  • tb_blog:探店笔记表,包含笔记中标题、文字、图片等
  • tb_blog_comments:其他用户对探店笔记的评价

    修改文件上传路径:

1.2 代码实现

由于我把 Nginx 放在了 Linux 虚拟机上,而 Java 程序则是在我本地,如果依旧使用老师讲的那种上传方式,肯定实行不通。为了实现通过 Java 代码向远程服务器上传文件,花了我两天时间。
如果想要从本地向远程服务器上传文件,需要使用 SSH 进行上传。
参考文章,感谢两位大佬:
Java用SSH2连接Linux服务器并执行命令,上传下载文件
SFTP中创建文件目录,上传文件(*)

引入 Jar 包

<!--java端连接ssh远程服务器-->
<dependency><groupId>com.jcraft</groupId><artifactId>jsch</artifactId><version>0.1.55</version>
</dependency>

创建工具类 SSHUtils

package com.hmdp.utils;import com.jcraft.jsch.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;public class SSHUtils {private static final Logger LOGGER = LoggerFactory.getLogger(SSHUtils.class);private static final int SESSION_TIMEOUT = 30 * 10000000;/*** 创建一个ssh会话* @param host 主机名* @param port 端口* @param userName 用户名* @param password 密码* @return Session*/public static Session createSshSession(String host, int port ,String userName, String password){// 创建jsch对象JSch jsch = new JSch();Session session = null;// 创建session会话try {session = jsch.getSession(userName, host, port);// 设置密码session.setPassword(password);// 创建一个session配置类Properties sshConfig = new Properties();// 跳过公钥检测sshConfig.put("StrictHostKeyChecking", "no");session.setConfig(sshConfig);// 我们还可以设置timeout时间session.setTimeout(SESSION_TIMEOUT);// 建立连接session.connect();}catch (Exception e){e.printStackTrace();}return session;}/*** 执行远程命令* @param session 会话* @param cmd cmd命令,也可以是&&在一起的命令* @return List<String>*/public static List<String> executeCmd(Session session, String cmd) {ChannelExec channelExec = null;InputStream inputStream = null;// 输出结果到字符串数组List<String> resultLines = new ArrayList<>();// 创建session会话try {// session建立之后,我们就可以执行shell命令,或者上传下载文件了,下面我来执行shell命令channelExec = (ChannelExec) session.openChannel("exec");// 将shell传入commandchannelExec.setCommand(cmd);// 开始执行channelExec.connect();// 获取执行结果的输入流inputStream = channelExec.getInputStream();String result = null;BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));while ((result = in.readLine()) != null) {resultLines.add(result);LOGGER.info("命令返回信息:{}", result);}} catch (Exception e) {LOGGER.error("Connect failed, {}", e.getMessage());ArrayList<String> errorMsg = new ArrayList<>();errorMsg.add(e.getMessage());return errorMsg;} finally {// 释放资源if (channelExec != null) {try {channelExec.disconnect();} catch (Exception e) {LOGGER.error("JSch channel disconnect error:", e);}}if (session != null) {try {session.disconnect();} catch (Exception e) {LOGGER.error("JSch session disconnect error:", e);}}if (inputStream != null) {try {inputStream.close();} catch (Exception e) {LOGGER.error("inputStream close error:", e);}}}return resultLines;}/*** 向远端上传文件* @param session 会话* @param directory 上传的目录* @param uploadFile 待上传的文件* @param uploadFileName 上传到远端的文件名*/public static void uploadFile(Session session,String directory,File uploadFile,String uploadFileName){ChannelSftp channelSftp = null;try {channelSftp = (ChannelSftp) session.openChannel("sftp");channelSftp.connect();LOGGER.info("start upload channel file!");channelSftp.cd(directory);channelSftp.put(new FileInputStream(uploadFile), uploadFileName);LOGGER.info("Upload Success!");}catch (Exception e){e.printStackTrace();} finally {if (null != channelSftp){channelSftp.disconnect();LOGGER.info("end execute channel sftp!");}if (session != null) {try {session.disconnect();} catch (Exception e) {LOGGER.error("JSch session disconnect error:", e);}}}}public static void uploadFile(Session session,String directory,FileInputStream inputStream,String uploadFileName){ChannelSftp channelSftp = null;try {if(uploadFileName.indexOf("/") == -1){return;}String[] fileSplit = uploadFileName.split("/");if(fileSplit == null){return;}String fileName = fileSplit[fileSplit.length-1];channelSftp = (ChannelSftp) session.openChannel("sftp");channelSftp.connect();LOGGER.info("start upload channel file!");channelSftp.cd(directory);for(int i = 0; i < fileSplit.length - 1; i++){if("".equals(fileSplit[i])){continue;}if(isDirExist(fileSplit[i] + "/", channelSftp)){channelSftp.cd(fileSplit[i]);} else {// 这里要注意:通过 channelSftp.mkdir 来创建文件夹,只能一个一个创建,不能批量创建channelSftp.mkdir(fileSplit[i] + "");channelSftp.cd(fileSplit[i] + "");}}channelSftp.put(inputStream, fileName);LOGGER.info("Upload Success!");}catch (Exception e){e.printStackTrace();} finally {if (null != channelSftp){channelSftp.disconnect();LOGGER.info("end execute channel sftp!");}if (session != null) {try {session.disconnect();} catch (Exception e) {LOGGER.error("JSch session disconnect error:", e);}}}}/*** 判断目录是否存在*/public static boolean isDirExist(String directory, ChannelSftp sftp) {boolean isDirExistFlag = false;try {SftpATTRS sftpATTRS = sftp.lstat(directory);return sftpATTRS.isDir();} catch (Exception e) {if (e.getMessage().toLowerCase().equals("no such file")) {isDirExistFlag = false;}}return isDirExistFlag;}/*** 从远端下载文件* @param session 会话* @param directory 远端需要下载的目录* @param savePathWithFileName 远端文件的路径包含文件名* @param downloadFileName  下载到本地的远端文件名*/public static void downloadFile(Session session, String directory,String savePathWithFileName,String downloadFileName) {ChannelSftp channelSftp = null;try {channelSftp = (ChannelSftp) session.openChannel("sftp");channelSftp.connect();LOGGER.info("start download channel file!");channelSftp.cd(directory);File file = new File(savePathWithFileName);channelSftp.get(downloadFileName, new FileOutputStream(file));LOGGER.info("Download Success!");}catch (Exception e){e.printStackTrace();} finally {if (null != channelSftp){channelSftp.disconnect();LOGGER.info("end execute channel sftp!");}if (session != null) {try {session.disconnect();} catch (Exception e) {LOGGER.error("JSch session disconnect error:", e);}}}}}

修改 UploadController

@Slf4j
@RestController
@RequestMapping("upload")
public class UploadController {@PostMapping("blog")public Result uploadImage(@RequestParam("file") MultipartFile image, HttpServletRequest request) {try {// 获取原始文件名称String originalFilename = image.getOriginalFilename();// 生成新文件名String fileName = createNewFileName(originalFilename);FileInputStream inputStream = (FileInputStream) image.getInputStream();Session SSHSESSION = SSHUtils.createSshSession("ip地址", 22, "远程服务器用户名", "远程服务器密码");SSHUtils.uploadFile(SSHSESSION, SystemConstants.IMAGE_UPLOAD_DIR ,inputStream, 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;// 判断目录是否存在String pathName = SystemConstants.IMAGE_UPLOAD_DIR + StrUtil.format("/blogs/{}/{}", d1, d2);// 生成文件名return StrUtil.format("/blogs/{}/{}/{}.{}", d1, d2, name, suffix);}
}

常量类 SystemConstant

public class SystemConstants {public static final String IMAGE_UPLOAD_DIR = "/usr/local/nginx/html/hmdp/imgs/";
}

然后就可以愉快地上传文件啦!

1.3 前端代码修改

我在上传完探店笔记后,发现个人主页加载不出来照片,看了下前端访问路径,是访问路径出了问题,前端页面多加了一个 /imgs/ 路径。

打开 info.html ,将多余的 /imgs/ 删除即可。


可以重新下载前端项目,应该是后来又重新上传了,这几个问题都解决。

二、查询探店笔记

需求:点击首页的探店笔记,会进入详情页面,实现该页面的查询接口:

BlogController

@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate 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") String id){return blogService.queryBlogById(id);}
}

IBlogService

public interface IBlogService extends IService<Blog> {Result queryBlogById(String id);Result queryHotBlog(Integer current);
}

BlogServiceImpl

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Autowiredprivate IUserService userService;@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(this::queryBlogUser);return Result.ok(records);}private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}@Overridepublic Result queryBlogById(String id) {Blog blog = getById(id);if(blog == null){return Result.fail("笔记不存在!");}queryBlogUser(blog);return Result.ok(blog);}
}

三、点赞功能

3.1 需求分析

需求:

  • 同一个用户只能点赞一次,再次点击则取消点赞
  • 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段 Blog 类的 isLike 属性)

实现步骤:
① 给 Blog 类中添加一个 isLike 字段,标识是否被当前用户点赞
② 修改点赞功能,利用 Redis 的 set 集合判断是否点赞过,未点赞过则点赞数 +1,已点赞过则点赞数 -1.
需要一个集合去记录所有点赞过的用户,同时一个用户只能点赞一次,要求用户 id 不能重复,即集合中元素唯一,而 Redis 中 set 集合满足这种需求。
③ 修改根据 id 查询 Blog 的业务,判断当前登录用户是否点赞过,赋值给 isLike 字段
④ 修改分页查询 Blog 业务,判断当前登录用户是否点赞过,赋值给 isLike 字段

3.2 代码实现

修改 Blog 实体类

@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;
}

BlogController 修改 likeBlog 方法(点赞方法)

@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;@PutMapping("/like/{id}")public Result likeBlog(@PathVariable("id") Long id) {return blogService.likeBlog(id);}@GetMapping("/hot")public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {return blogService.queryHotBlog(current);}@GetMapping("/{id}")public Result queryBlogById(@PathVariable("id") String id){return blogService.queryBlogById(id);}
}

修改 IBlogService 类,增加 likeBlog 方法

public interface IBlogService extends IService<Blog> {Result queryBlogById(String id);Result queryHotBlog(Integer current);Result likeBlog(Long id);
}

修改 BlogServiceImpl 类,实现 likeBlog 方法,修改查询笔记逻辑

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Autowiredprivate IUserService userService;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@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、获取登录用户UserDTO user = UserHolder.getUser();// 2、判断当前登录用户是否已经点赞Boolean isMember = stringRedisTemplate.opsForSet().isMember(RedisConstants.BLOG_LIKED_KEY + id, user.getId().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(RedisConstants.BLOG_LIKED_KEY + id, user.getId().toString());}} else {// 4、如果已点赞,取消点赞// 4.1、数据库点赞数 -1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();// 4.2、把用户从 Redis 的 set 集合移除if(isSuccess){stringRedisTemplate.opsForSet().remove(RedisConstants.BLOG_LIKED_KEY + id, user.getId().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());}@Overridepublic Result queryBlogById(String id) {Blog blog = getById(id);if(blog == null){return Result.fail("笔记不存在!");}queryBlogUser(blog);// 查询 Blog 是否被点赞isBlogLiked(blog);return Result.ok(blog);}private void isBlogLiked(Blog blog) {Long userId = blog.getUserId();String key = RedisConstants.BLOG_LIKED_KEY + blog.getId();Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());blog.setIsLike(BooleanUtil.isTrue(isMember));}
}

四、点赞排行榜

在探店笔记的详情页面,应该把给该笔记点赞的人显示出来,比如最早点赞的 TOP5,形成点赞排行榜:

set 集合中的元素是无序的,点赞排行榜需要对点赞时间进行排序,这里 set 集合并不满足需求。

SortedSet 更符合需求。

通过 ZSCORE 命令获取 SortedSet 中存储的元素的相关的 SCORE 值。
通过 ZRANGE 命令获取指定范围内的元素。

BlogController

@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;@PutMapping("/like/{id}")public Result likeBlog(@PathVariable("id") Long id) {return blogService.likeBlog(id);}@GetMapping("/hot")public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {return blogService.queryHotBlog(current);}@GetMapping("/{id}")public Result queryBlogById(@PathVariable("id") String id){return blogService.queryBlogById(id);}@GetMapping("/likes/{id}")public Result queryBlogLikes(@PathVariable("id") String id) {return blogService.queryBlogLikes(id);}
}

IBlogService

public interface IBlogService extends IService<Blog> {Result queryBlogById(String id);Result queryHotBlog(Integer current);Result likeBlog(Long id);Result queryBlogLikes(String id);
}

BlogServiceImpl

Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Autowiredprivate IUserService userService;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@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、获取登录用户UserDTO user = UserHolder.getUser();// 2、判断当前登录用户是否已经点赞Double score = stringRedisTemplate.opsForZSet().score(RedisConstants.BLOG_LIKED_KEY + id, user.getId().toString());if(score == null) {// 3、如果未点赞,可以点赞// 3.1、数据库点赞数 +1boolean isSuccess = update().setSql("liked = liked+1").eq("id", id).update();// 3.2、保存用户到 Redis 的 set 集合if(isSuccess){// 时间作为 key 的 scorestringRedisTemplate.opsForZSet().add(RedisConstants.BLOG_LIKED_KEY + id, user.getId().toString(), System.currentTimeMillis());}} else {// 4、如果已点赞,取消点赞// 4.1、数据库点赞数 -1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();// 4.2、把用户从 Redis 的 set 集合移除if(isSuccess){stringRedisTemplate.opsForZSet().remove(RedisConstants.BLOG_LIKED_KEY + id, user.getId().toString());}}return Result.ok();}@Overridepublic Result queryBlogLikes(String id) {String key = RedisConstants.BLOG_LIKED_KEY + id;// 查询 top5 的点赞用户Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);if(top5 == null){return Result.ok(Collections.emptyList());}// 解析出其中的用户idList<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());String join = StrUtil.join(",", ids);// 根据用户id查询用户List<UserDTO> userDTOS = userService.query().in("id", ids).last("order by filed(id, "+join+")").list().stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());return Result.ok(userDTOS);}private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}@Overridepublic Result queryBlogById(String id) {Blog blog = getById(id);if(blog == null){return Result.fail("笔记不存在!");}queryBlogUser(blog);// 查询 Blog 是否被点赞isBlogLiked(blog);return Result.ok(blog);}private void isBlogLiked(Blog blog) {UserDTO user = UserHolder.getUser();if(user == null){return;}Long userId = user.getId();String key = RedisConstants.BLOG_LIKED_KEY + blog.getId();Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());blog.setIsLike(score != null);}
}

黑马点评项目-达人探店相关推荐

  1. 黑马点评项目-短信登录功能

    一.导入黑马点评项目 1.代码下载 视频资源链接:P25 实战篇-02.短信登录-导入黑马点评项目 代码可以直接去黑马微信公众号上搜索,或者从下面的网盘链接中下载:链接: https://pan.ba ...

  2. 黑马点评项目笔记(四)社交、附近人、数据统计功能实现

    目录 达人探店 查看博文 点赞博文 点赞排行榜 好友关注 关注和取关 共同关注 关注推送(Feed流) Feed流的两种模式 Timeline 三种实现模式 基于推模式实现消息推送 滚动分页 附近商户 ...

  3. Redis学习笔记②实战篇_黑马点评项目

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 资料链接:https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA( ...

  4. 黑马点评项目全部功能实现及详细笔记--Redis练手项目

    目录 一.项目详情 1.1 项目简介 1.2 数据库表设计 1.3 前端部署 1.4 后端搭建 二.短信登录 2.1 发送验证码 2.2 验证码登录 2.3 登录校验拦截器 2.4 退出登录(补充) ...

  5. 菜鸟项目练习:黑马点评项目总结

    目录 1. 项目介绍 2.各个功能模块 2.1  登录模块 2.1.1 实现短信登录 2.1.2 编写拦截器 2.2 查询商户模块 2.2.1 主页面查询商户类型 2.2.3 按距离查询商户 2.3 ...

  6. 【Redis企业实战】仿黑马点评项目

    目录 一.短信登陆:基于Redis实现共享session实现登录 1.发送短信验证码 2.短信验证码登录.注册 3.校验登陆状态 二.商户查询缓存 1.添加Redis缓存 2.缓存更新策略: 3.缓存 ...

  7. 探店达人专业版盈利模式达人探店盈利模式优势利润在哪里?

    一.探店项目背景 本地生活带货已经达到瓶颈,大量的机构开始转型做代运营版块,但是这并不能根本性 的解决门店营销问题,达不到预期效果,投入成本高,行业乱象的问题频发. 商业的本质是解决需求创造收益,首先 ...

  8. 达人探店小程序全套源码

    简介: 达人探店小程序全套源码,UI做的很简陋,分享给初学者学习一下小程序. 前台采用了微信小程序(WXML+WXSS+Javascript) 后台是IDEA开发JAVA用了spring-boot,数 ...

  9. 抖音达人探店有用吗?算不算过时呢

    其实我看到这个问题的时候也曾怀疑探店的方式真的对当下年轻人有作用吗?会不会有人怀疑探店达人和商家是串通好的?毕竟在有利益链的情况下,我们还能否相信仅仅靠一个视频就能知晓事物的孰好孰坏?今天就来为大家分 ...

最新文章

  1. 解密首批人工智能国家队 BAT都在【附下载】
  2. Matplotlib实例教程(十)边缘直方图
  3. uni-app小程序v-show内容始终不显示
  4. Netfilter深度解剖
  5. 系统蓝屏的几种姿势,确定不了解下么?
  6. JPA – Hibernate –包级别的类型映射
  7. canal mysql从库_canal中间件|数据增量同步解决方案
  8. iphone 字符串分隔与组合
  9. 尚硅谷_springcloud(2020新版 思维导图_全网最火SpringCloud2020全家桶教程
  10. CCF 201503-1 图像旋转
  11. opensource项目_最佳Opensource.com:艺术与设计
  12. 初步了解关于js跨域问题
  13. 19 Tips For Everyday Git Use
  14. 从Python迁移到Go的原因和好处
  15. 第55章、播放视频(从零开始学Android)
  16. python进行回归方程显著性检验
  17. 机器人算法工程师入门指南(三)机器人算法工程师需要学习哪些知识?(及教材推荐)
  18. Nature Reviews Microbiology | 土壤微生物组与同一健康
  19. 使用CSS绘制几何图形(圆形、三角形、扇形、菱形等
  20. 02. 交换机的基本配置和管理

热门文章

  1. 企业电子名片小程序哪家?市面上哪一款名片小程序更好用?
  2. 【Android】安卓四大组件之Activity(一)
  3. 微信小程序之自动跳转页面
  4. Invalid Host header 服务器域名访问出现的问题
  5. 【Nodejs】学习之koa2框架
  6. 楞严咒全文翻译_楞严咒解释
  7. 速腾聚创32线雷达雷达,RVIZ显示激光点云
  8. 《C++语言基础》实践参考——旱冰场造价
  9. 使用Opencv中的SVM分类器进行图像分类
  10. 【KD-tree】[NOI2019]弹跳