Spring Boot仿YouTube应用问题:评论点赞失效+缩略图不显示
仿YouTube Spring Boot应用问题解决指南
我正在开发一款仿YouTube的Spring Boot应用,目前已实现视频上传功能,但存在三个问题:
- 上传的视频缩略图无法正常显示(已确认缩略图已成功保存)
- 无法添加评论
- 无法实现点赞/点踩交互
以下是针对每个问题的具体解决方案:
一、缩略图无法显示问题
问题分析
HomeController的getFile方法中,判断文件名是否以_thumbnail结尾,但实际缩略图文件名是xxx_thumbnail.png(带扩展名),导致匹配失败,调用了错误的处理逻辑。writeThumbnailToResponse方法通过findByStorageFileName查找视频,但缩略图的存储文件名并未关联到Video实体的storageFileName字段,而是存在thumbnailUrl中,查询返回null。
修复代码
1. 修改HomeController的getFile方法
@GetMapping("/files/{file-name:.+}") public void getFile(@PathVariable("file-name") String fileName, HttpServletResponse response){ // 判断是否为缩略图(包含_thumbnail标识) if (fileName.contains("_thumbnail")) { // 通过thumbnailUrl匹配视频 Video video = videoRepository.findByThumbnailUrl(storagePath + "/" + fileName); if (video != null) { videoService.writeThumbnailToResponse(fileName, response); return; } } else { Video video = videoService.findByStorageName(fileName); if (video != null) { videoService.writeFileToResponse(fileName, response); return; } } // 返回404状态码 response.setStatus(HttpServletResponse.SC_NOT_FOUND); }
2. 给VideoRepository新增查询方法
@Repository public interface VideoRepository extends JpaRepository<Video, Long> { Video findByStorageFileName(String fileName); Video findByThumbnailUrl(String thumbnailUrl); // 新增方法 }
3. 修改VideoServiceImpl的writeThumbnailToResponse方法
@Override public void writeThumbnailToResponse(String fileName, HttpServletResponse response) { // 直接构建缩略图文件路径 String thumbnailFullPath = Paths.get("src/main/resources", storagePath, fileName).toString(); File thumbnailFile = new File(thumbnailFullPath); if (!thumbnailFile.exists()) { throw new IllegalArgumentException("Thumbnail file not found with name: " + fileName); } // 根据文件扩展名设置正确的Content-Type String extension = FilenameUtils.getExtension(fileName); String contentType = switch (extension.toLowerCase()) { case "png" -> "image/png"; case "jpg", "jpeg" -> "image/jpeg"; case "gif" -> "image/gif"; default -> "image/octet-stream"; }; response.setContentType(contentType); try { IOUtils.copy(new FileInputStream(thumbnailFile), response.getOutputStream()); response.flushBuffer(); } catch (IOException e) { throw new IllegalArgumentException("Error writing thumbnail to response", e); } }
4. 前端使用正确的缩略图URL
在home页面中,缩略图的src应指向文件接口:
<img src="/files/${video.thumbnailUrl.split('/')[1]}" alt="${video.originalName}">
二、添加评论功能
1. 创建Comment实体
@Builder @AllArgsConstructor @NoArgsConstructor @Entity @Data public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String content; private LocalDateTime createdAt; @ManyToOne @JoinColumn(name = "video_id") private Video video; @ManyToOne @JoinColumn(name = "user_id") private User user; }
2. 创建CommentRepository
@Repository public interface CommentRepository extends JpaRepository<Comment, Long> { List<Comment> findByVideoIdOrderByCreatedAtDesc(Long videoId); }
3. 编写CommentService及实现类
public interface CommentService { Comment addComment(Long videoId, String content, Principal principal); List<Comment> getCommentsByVideoId(Long videoId); }
@Component public class CommentServiceImpl implements CommentService { @Autowired private CommentRepository commentRepository; @Autowired private VideoRepository videoRepository; @Autowired private UserRepository userRepository; @Override public Comment addComment(Long videoId, String content, Principal principal) { Video video = videoRepository.findById(videoId) .orElseThrow(() -> new IllegalArgumentException("Video not found")); User user = userRepository.findByEmail(principal.getName()) .orElseThrow(() -> new IllegalArgumentException("User not found")); Comment comment = Comment.builder() .content(content) .createdAt(LocalDateTime.now()) .video(video) .user(user) .build(); return commentRepository.save(comment); } @Override public List<Comment> getCommentsByVideoId(Long videoId) { return commentRepository.findByVideoIdOrderByCreatedAtDesc(videoId); } }
4. 修改视频详情页接口,传递评论数据
@GetMapping("/video_detail") public String videoDetailPage(@RequestParam("videoId") Long videoId, Model model) { Video video = videoRepository.findById(videoId) .orElseThrow(() -> new IllegalArgumentException("Video not found")); List<Comment> comments = commentService.getCommentsByVideoId(videoId); model.addAttribute("video", video); model.addAttribute("comments", comments); return "video_detail"; }
5. 前端修改video_detail页面,添加评论交互
<main> <div class="video-container"> <video src="/files/${video.storageFileName}" controls></video> </div> <div class="video-actions"> <button class="like-btn"><i class="fas fa-thumbs-up"></i> Like</button> <button class="dislike-btn"><i class="fas fa-thumbs-down"></i> Dislike</button> <button class="subscribe-btn"><i class="fas fa-heart"></i> Subscribe</button> </div> <div class="comment-section"> <h3>评论</h3> <!-- 评论输入框 --> <form id="commentForm"> <textarea name="content" placeholder="写下你的评论..." required></textarea> <button type="submit">发布评论</button> </form> <!-- 评论列表 --> <div class="comments-list"> <#list comments as comment> <div class="comment-item"> <p><strong>${comment.user.username}</strong> ${comment.createdAt}</p> <p>${comment.content}</p> </div> </#list> </div> </div> </main> <script> document.getElementById('commentForm').addEventListener('submit', function(e) { e.preventDefault(); const content = this.querySelector('textarea').value; const videoId = new URLSearchParams(window.location.search).get('videoId'); fetch('/comments/add', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: `videoId=${videoId}&content=${encodeURIComponent(content)}` }).then(response => { if (response.ok) window.location.reload(); }); }); </script>
6. 添加CommentController处理评论请求
@Controller @RequestMapping("/comments") public class CommentController { @Autowired private CommentService commentService; @PostMapping("/add") public String addComment(@RequestParam("videoId") Long videoId, @RequestParam("content") String content, Principal principal) { commentService.addComment(videoId, content, principal); return "redirect:/video_detail?videoId=" + videoId; } }
三、实现点赞/点踩交互
1. 创建LikeDislike实体(记录用户对视频的互动)
@Builder @AllArgsConstructor @NoArgsConstructor @Entity @Data @Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"video_id", "user_id"})}) public class LikeDislike { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private boolean isLike; // true为点赞,false为点踩 @ManyToOne @JoinColumn(name = "video_id") private Video video; @ManyToOne @JoinColumn(name = "user_id") private User user; }
2. 创建LikeDislikeRepository
@Repository public interface LikeDislikeRepository extends JpaRepository<LikeDislike, Long> { Optional<LikeDislike> findByVideoIdAndUserId(Long videoId, Long userId); Long countByVideoIdAndIsLikeTrue(Long videoId); Long countByVideoIdAndIsLikeFalse(Long videoId); }
3. 编写LikeDislikeService及实现类
public interface LikeDislikeService { void toggleLike(Long videoId, Principal principal); void toggleDislike(Long videoId, Principal principal); LikeDislike getUserReaction(Long videoId, Principal principal); Long getLikeCount(Long videoId); Long getDislikeCount(Long videoId); }
@Component public class LikeDislikeServiceImpl implements LikeDislikeService { @Autowired private LikeDislikeRepository likeDislikeRepository; @Autowired private VideoRepository videoRepository; @Autowired private UserRepository userRepository; @Override public void toggleLike(Long videoId, Principal principal) { User user = userRepository.findByEmail(principal.getName()) .orElseThrow(() -> new IllegalArgumentException("User not found")); Video video = videoRepository.findById(videoId) .orElseThrow(() -> new IllegalArgumentException("Video not found")); Optional<LikeDislike> existingReaction = likeDislikeRepository.findByVideoIdAndUserId(videoId, user.getId()); if (existingReaction.isPresent()) { LikeDislike reaction = existingReaction.get(); if (reaction.isLike()) { likeDislikeRepository.delete(reaction); // 取消点赞 } else { reaction.setLike(true); // 从点踩改为点赞 likeDislikeRepository.save(reaction); } } else { // 首次点赞 LikeDislike newReaction = LikeDislike.builder() .isLike(true) .video(video) .user(user) .build(); likeDislikeRepository.save(newReaction); } } @Override public void toggleDislike(Long videoId, Principal principal) { User user = userRepository.findByEmail(principal.getName()) .orElseThrow(() -> new IllegalArgumentException("User not found")); Video video = videoRepository.findById(videoId) .orElseThrow(() -> new IllegalArgumentException("Video not found")); Optional<LikeDislike> existingReaction = likeDislikeRepository.findByVideoIdAndUserId(videoId, user.getId()); if (existingReaction.isPresent()) { LikeDislike reaction = existingReaction.get(); if (!reaction.isLike()) { likeDislikeRepository.delete(reaction); // 取消点踩 } else { reaction.setLike(false); // 从点赞改为点踩 likeDislikeRepository.save(reaction); } } else { // 首次点踩 LikeDislike newReaction = LikeDislike.builder() .isLike(false) .video(video) .user(user) .build(); likeDislikeRepository.save(newReaction); } } @Override public LikeDislike getUserReaction(Long videoId, Principal principal) { if (principal == null) return null; User user = userRepository.findByEmail(principal.getName()).orElse(null); if (user == null) return null; return likeDislikeRepository.findByVideoIdAndUserId(videoId, user.getId()).orElse(null); } @Override public Long getLikeCount(Long videoId) { return likeDislikeRepository.countByVideoIdAndIsLikeTrue(videoId); } @Override public Long getDislikeCount(Long videoId) { return likeDislikeRepository.countByVideoIdAndIsLikeFalse(videoId); } }
4. 修改视频详情页接口,传递点赞点踩数据
@GetMapping("/video_detail") public String videoDetailPage(@RequestParam("videoId") Long videoId, Model model, Principal principal) { Video video = videoRepository.findById(videoId) .orElseThrow(() -> new IllegalArgumentException("Video not found")); List<Comment> comments = commentService.getCommentsByVideoId(videoId); Long likeCount = likeDislikeService.getLikeCount(videoId); Long dislikeCount = likeDislikeService.getDislikeCount(videoId); LikeDislike userReaction = likeDislikeService.getUserReaction(videoId, principal); boolean hasLiked = userReaction != null && userReaction.isLike(); boolean hasDisliked = userReaction != null && !userReaction.isLike(); model.addAttribute("video", video); model.addAttribute("comments", comments); model.addAttribute("likeCount", likeCount); model.addAttribute("dislikeCount", dislikeCount); model.addAttribute("hasLiked", hasLiked); model.addAttribute("hasDisliked", hasDisliked); return "video_detail"; }
5. 前端修改点赞点踩按钮,添加状态展示和交互
<div class="video-actions"> <button class="like-btn ${hasLiked ? 'active' : ''}" data-video-id="${video.id}"> <i class="fas fa-thumbs-up"></i> ${likeCount} </button> <button class="dislike-btn ${hasDisliked ? 'active' : ''}" data-video-id="${video.id}"> <i class="fas fa-thumbs-down"></i> ${dislikeCount} </button> <button class="subscribe-btn"><i class="fas fa-heart"></i> Subscribe</button> </div> <script> // 点赞按钮交互 document.querySelectorAll('.like-btn').forEach(btn => { btn.addEventListener('click', function() { const videoId = this.getAttribute('data-video-id'); fetch(`/likes/toggle?videoId=${videoId}`, {method: 'POST'}) .then(response => { if (response.ok) window.location.reload(); }); }); }); // 点踩按钮交互 document.querySelectorAll('.dislike-btn').forEach(btn => { btn.addEventListener('click', function() { const videoId = this.getAttribute('data-video-id'); fetch(`/dislikes/toggle?videoId=${videoId}`, {method: 'POST'}) .then(response => { if (response.ok) window.location.reload(); }); }); }); </script>
6. 添加LikeDislikeController处理请求
@Controller public class LikeDislikeController { @Autowired private LikeDislikeService likeDislikeService; @PostMapping("/likes/toggle") public String toggleLike(@RequestParam("videoId") Long videoId, Principal principal) { likeDislikeService.toggleLike(videoId, principal); return "redirect:/video_detail?videoId=" + videoId; } @PostMapping("/dislikes/toggle") public String toggleDislike(@RequestParam("videoId") Long videoId, Principal principal) { likeDislikeService.toggleDislike(videoId, principal); return "redirect:/video_detail?videoId=" + videoId; } }
内容的提问来源于stack exchange,提问作者Helouss Mystery
相关产品推荐
相关产品推荐

