You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot仿YouTube应用问题:评论点赞失效+缩略图不显示

仿YouTube Spring Boot应用问题解决指南

我正在开发一款仿YouTube的Spring Boot应用,目前已实现视频上传功能,但存在三个问题:

  1. 上传的视频缩略图无法正常显示(已确认缩略图已成功保存)
  2. 无法添加评论
  3. 无法实现点赞/点踩交互

以下是针对每个问题的具体解决方案:


一、缩略图无法显示问题

问题分析

  • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 05:05:54