黑马点评项目-达人探店
一、发布探店笔记探店笔记类似点评网站的评价,往往是图文结合。对应的表有两个:tb_blog:探店笔记表,包含笔记中标题、文字、图片等tb_blog_comments:其他用户对探店笔记的评价修改文件上传路径:...
一、发布探店笔记
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);
}
}
更多推荐
所有评论(0)