一、发布探店笔记

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传入command
            channelExec.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 {

    @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") 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 {

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

    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 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 {

    @Resource
    private 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 {

    @Autowired
    private IUserService userService;

    @Autowired
    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 -> {
            this.queryBlogUser(blog);
            this.isBlogLiked(blog);
        });
        return Result.ok(records);
    }

    @Override
    public 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、数据库点赞数 +1
            boolean 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、数据库点赞数 -1
            boolean 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());
    }

    @Override
    public 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 {

    @Resource
    private 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 {

    @Autowired
    private IUserService userService;

    @Autowired
    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 -> {
            this.queryBlogUser(blog);
            this.isBlogLiked(blog);
        });
        return Result.ok(records);
    }

    @Override
    public 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、数据库点赞数 +1
            boolean isSuccess = update().setSql("liked = liked+1").eq("id", id).update();
            // 3.2、保存用户到 Redis 的 set 集合
            if(isSuccess){
                // 时间作为 key 的 score
                stringRedisTemplate.opsForZSet().add(RedisConstants.BLOG_LIKED_KEY + id, user.getId().toString(), System.currentTimeMillis());
            }
        } else {
            // 4、如果已点赞,取消点赞
            // 4.1、数据库点赞数 -1
            boolean 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();
    }

    @Override
    public 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());
        }
        // 解析出其中的用户id
        List<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());
    }

    @Override
    public 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);
    }
}
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐