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

Spring Boot中文章点赞按钮的实现及状态识别问题

解决Spring Boot文章列表批量获取点赞状态的问题

这个问题我之前也遇到过,批量处理点赞状态确实是平衡性能和用户体验的关键,我给你分享几个实用的解决方案,既能避免N+1查询的性能问题,又能优雅地把点赞状态传递到Thymeleaf模板中:

核心思路

先一次性获取所有要展示的文章,再用单条SQL批量查询当前用户对这些文章的点赞记录,最后把点赞状态和文章关联起来,传递到前端渲染。


方案一:使用DTO封装文章+点赞状态

这种方式最直观,专门创建一个DTO类来承载文章信息和对应的点赞状态,代码可读性高。

1. 创建DTO类

public class ArticleLikeDTO {
    // 原文章实体
    private Article article;
    // 当前用户是否点赞
    private boolean isLiked;

    // 构造方法、getter、setter
    public ArticleLikeDTO(Article article, boolean isLiked) {
        this.article = article;
        this.isLiked = isLiked;
    }

    // getter和setter省略
}

2. Service层批量查询并封装DTO

@Service
public class ArticleService {
    @Autowired
    private PeopleWhoLikedRepository likedRepo;
    @Autowired
    private CategoryRepository categoryRepo;

    public List<ArticleLikeDTO> getArticlesWithLikeStatus(Long categoryId, Long currentUserId) {
        // 1. 获取分类下的所有文章
        Category category = categoryRepo.findById(categoryId)
                .orElseThrow(() -> new IllegalArgumentException("分类不存在"));
        List<Article> articles = category.getArticleList();
        
        if (articles.isEmpty()) {
            return Collections.emptyList();
        }

        // 2. 提取所有文章ID,用于批量查询
        List<Long> articleIds = articles.stream()
                .map(Article::getId)
                .collect(Collectors.toList());

        // 3. 单条SQL查询当前用户点赞的所有文章记录
        List<PeopleWhoLiked> likedRecords = likedRepo.findByUserIdAndArticleIdIn(currentUserId, articleIds);

        // 4. 把点赞的文章ID存入Set,方便快速判断(O(1)查询)
        Set<Long> likedArticleIds = likedRecords.stream()
                .map(record -> record.getArticle().getId())
                .collect(Collectors.toSet());

        // 5. 封装成DTO列表
        return articles.stream()
                .map(article -> new ArticleLikeDTO(article, likedArticleIds.contains(article.getId())))
                .collect(Collectors.toList());
    }
}

3. Controller传递数据到Thymeleaf

@GetMapping("/category/{categoryId}")
public String showCategoryArticles(@PathVariable Long categoryId, 
                                   Model model,
                                   @AuthenticationPrincipal User currentUser) {
    // 获取当前用户ID(如果用户未登录,可以传null,后续处理)
    Long userId = currentUser != null ? currentUser.getId() : null;
    
    List<ArticleLikeDTO> articleList = articleService.getArticlesWithLikeStatus(categoryId, userId);
    model.addAttribute("articleList", articleList);
    
    return "articles/list";
}

4. Thymeleaf模板渲染

<div th:each="item : ${articleList}">
    <h4 th:text="${item.article.title}"></h4>
    <p th:text="${item.article.content}"></p>
    
    <!-- 根据isLiked状态渲染按钮 -->
    <button th:if="${item.isLiked}" disabled class="liked-btn">已点赞</button>
    <button th:unless="${item.isLiked}" 
            th:attr="data-article-id=${item.article.id}" 
            class="like-btn">点赞</button>
</div>

方案二:使用Map存储点赞状态(无需额外DTO)

如果不想创建新的DTO类,可以用一个Map<Long, Boolean>来存储文章ID和对应的点赞状态,同样高效。

1. Service层返回点赞状态Map

public Map<Long, Boolean> getUserLikeStatusForArticles(List<Article> articles, Long currentUserId) {
    if (articles.isEmpty() || currentUserId == null) {
        return Collections.emptyMap();
    }

    List<Long> articleIds = articles.stream().map(Article::getId).collect(Collectors.toList());
    List<PeopleWhoLiked> likedRecords = likedRepo.findByUserIdAndArticleIdIn(currentUserId, articleIds);
    
    Set<Long> likedIds = likedRecords.stream().map(r -> r.getArticle().getId()).collect(Collectors.toSet());
    
    // 构建文章ID到点赞状态的Map
    return articles.stream()
            .collect(Collectors.toMap(
                    Article::getId,
                    article -> likedIds.contains(article.getId())
            ));
}

2. Controller传递文章列表和状态Map

@GetMapping("/category/{categoryId}")
public String showCategoryArticles(@PathVariable Long categoryId, 
                                   Model model,
                                   @AuthenticationPrincipal User currentUser) {
    Category category = categoryRepo.findById(categoryId).orElseThrow(() -> new IllegalArgumentException("分类不存在"));
    List<Article> articles = category.getArticleList();
    
    Long userId = currentUser != null ? currentUser.getId() : null;
    Map<Long, Boolean> likeStatusMap = articleService.getUserLikeStatusForArticles(articles, userId);
    
    model.addAttribute("articles", articles);
    model.addAttribute("likeStatusMap", likeStatusMap);
    
    return "articles/list";
}

3. Thymeleaf模板使用Map判断状态

<div th:each="article : ${articles}">
    <h4 th:text="${article.title}"></h4>
    <p th:text="${article.content}"></p>
    
    <button th:if="${likeStatusMap[article.id]}" disabled class="liked-btn">已点赞</button>
    <button th:unless="${likeStatusMap[article.id]}" 
            th:attr="data-article-id=${article.id}" 
            class="like-btn">点赞</button>
</div>

额外注意事项

  1. 未登录用户处理:如果当前用户未登录,所有文章的点赞状态都设为false,或者直接隐藏点赞按钮,避免空指针问题。
  2. 性能优化:用Set存储已点赞的文章ID,因为Set.contains()是O(1)操作,比List.contains()的O(n)高效得多,尤其是文章数量较多时。
  3. JPA查询优化:确保PeopleWhoLikedRepository的findByUserIdAndArticleIdIn方法生成的SQL是高效的,最好给user_id和article_id建立联合索引,进一步提升查询速度。

内容的提问来源于stack exchange,提问作者Mert Doe

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 10:16:46