feat(web): 新增博客首页文章、分类、标签及博客设置相关接口

- 新增ArticleController提供首页文章分页查询接口
- 实现ArticleService及其实现类,支持分页查询文章及其分类标签关联信息
- 扩展ArticleCategoryRelRepository和ArticleTagRelRepository,支持批量查询文章关联分类和标签
- 新增CategoryController及CategoryService,实现前台分类列表查询接口
- 新增TagController及TagService,实现前台标签列表查询接口
- 新增BlogSettingsController及BlogSettingsService,支持博客配置信息查询
- 添加相应的请求响应VO,包含文章、分类、标签及博客设置数据结构
- 移除无用的User模型代码,简化项目结构
This commit is contained in:
Hanserwei
2025-12-08 19:23:29 +08:00
parent 6fae09f42f
commit 8fc5a3d250
22 changed files with 589 additions and 25 deletions

View File

@@ -0,0 +1,33 @@
package com.hanserwei.web.controller;
import com.hanserwei.common.aspect.ApiOperationLog;
import com.hanserwei.common.utils.PageResponse;
import com.hanserwei.web.model.vo.article.FindIndexArticlePageListReqVO;
import com.hanserwei.web.model.vo.article.FindIndexArticlePageListRspVO;
import com.hanserwei.web.service.ArticleService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* 文章控制器
*
* @author hanserwei
*/
@RestController
public class ArticleController {
@Resource
private ArticleService articleService;
/**
* 获取首页文章分页数据
*/
@PostMapping("/article/list")
@ApiOperationLog(description = "获取首页文章分页数据")
public PageResponse<FindIndexArticlePageListRspVO> findArticlePageList(@RequestBody FindIndexArticlePageListReqVO findIndexArticlePageListReqVO) {
return articleService.findArticlePageList(findIndexArticlePageListReqVO);
}
}

View File

@@ -0,0 +1,31 @@
package com.hanserwei.web.controller;
import com.hanserwei.common.aspect.ApiOperationLog;
import com.hanserwei.common.utils.Response;
import com.hanserwei.web.model.vo.blogsetting.FindBlogSettingsDetailRspVO;
import com.hanserwei.web.service.BlogSettingsService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 博客设置控制器
*/
@RestController
@RequestMapping("/blog/settings")
public class BlogSettingsController {
@Resource
private BlogSettingsService blogSettingsService;
/**
* 获取博客设置详情
*/
@PostMapping("/detail")
@ApiOperationLog(description = "前台获取博客详情")
public Response<FindBlogSettingsDetailRspVO> findDetail() {
return blogSettingsService.findDetail();
}
}

View File

@@ -0,0 +1,35 @@
package com.hanserwei.web.controller;
import com.hanserwei.common.aspect.ApiOperationLog;
import com.hanserwei.common.utils.Response;
import com.hanserwei.web.model.vo.category.FindCategoryListRspVO;
import com.hanserwei.web.service.CategoryService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 分类控制器
*
* @author hanserwei
*/
@RestController
@RequestMapping("/category")
public class CategoryController {
@Resource
private CategoryService categoryService;
/**
* 前台获取分类列表
*/
@PostMapping("/list")
@ApiOperationLog(description = "前台获取分类列表")
public Response<List<FindCategoryListRspVO>> findCategoryList() {
return categoryService.findCategoryList();
}
}

View File

@@ -0,0 +1,35 @@
package com.hanserwei.web.controller;
import com.hanserwei.common.aspect.ApiOperationLog;
import com.hanserwei.common.utils.Response;
import com.hanserwei.web.model.vo.tag.FindTagListRspVO;
import com.hanserwei.web.service.TagService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 标签控制器
*
* @author hanserwei
*/
@RestController
@RequestMapping("/tag")
public class TagController {
@Resource
private TagService tagService;
/**
* 获取标签列表
*/
@PostMapping("/list")
@ApiOperationLog(description = "前台获取标签列表")
public Response<List<FindTagListRspVO>> findTagList() {
return tagService.findTagList();
}
}

View File

@@ -1,25 +0,0 @@
package com.hanserwei.web.model;
import jakarta.validation.constraints.*;
import lombok.Data;
@Data
public class User {
// 用户名
@NotBlank(message = "用户名不能为空") // 注解确保用户名不为空
private String username;
// 性别
@NotNull(message = "性别不能为空") // 注解确保性别不为空
private Integer sex;
// 年龄
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄必须大于或等于 18") // 注解确保年龄大于等于 18
@Max(value = 100, message = "年龄必须小于或等于 100") // 注解确保年龄小于等于 100
private Integer age;
// 邮箱
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确") // 注解确保邮箱格式正确
private String email;
}

View File

@@ -0,0 +1,12 @@
package com.hanserwei.web.model.vo.article;
import com.hanserwei.common.model.BasePageQuery;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
@Builder
public class FindIndexArticlePageListReqVO extends BasePageQuery {
}

View File

@@ -0,0 +1,47 @@
package com.hanserwei.web.model.vo.article;
import com.hanserwei.web.model.vo.category.FindCategoryListRspVO;
import com.hanserwei.web.model.vo.tag.FindTagListRspVO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindIndexArticlePageListRspVO {
/**
* 文章id
*/
private Long id;
/**
* 封面url
*/
private String cover;
/**
* 标题
*/
private String title;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 摘要
*/
private String summary;
/**
* 文章分类
*/
private FindCategoryListRspVO category;
/**
* 文章标签
*/
private List<FindTagListRspVO> tags;
}

View File

@@ -0,0 +1,53 @@
package com.hanserwei.web.model.vo.blogsetting;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
/*
* 博客设置详情响应VO
*/
public class FindBlogSettingsDetailRspVO {
/**
* 博客Logo
*/
private String logo;
/**
* 博客名称
*/
private String name;
/**
* 作者名称
*/
private String author;
/**
* 博客介绍
*/
private String introduction;
/**
* 作者头像
*/
private String avatar;
/**
* GitHub主页
*/
private String githubHomepage;
/**
* CSDN主页
*/
private String csdnHomepage;
/**
* Gitee主页
*/
private String giteeHomepage;
/**
* 知乎主页
*/
private String zhihuHomepage;
}

View File

@@ -0,0 +1,21 @@
package com.hanserwei.web.model.vo.category;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindCategoryListRspVO {
/**
* 分类id
*/
private Long id;
/**
* 分类名称
*/
private String name;
}

View File

@@ -0,0 +1,21 @@
package com.hanserwei.web.model.vo.tag;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindTagListRspVO {
/**
* 标签id
*/
private Long id;
/**
* 标签名称
*/
private String name;
}

View File

@@ -0,0 +1,15 @@
package com.hanserwei.web.service;
import com.hanserwei.common.utils.PageResponse;
import com.hanserwei.web.model.vo.article.FindIndexArticlePageListReqVO;
import com.hanserwei.web.model.vo.article.FindIndexArticlePageListRspVO;
public interface ArticleService {
/**
* 获取首页文章分页数据
*
* @param findIndexArticlePageListReqVO 获取首页文章分页数据请求参数
* @return 响应
*/
PageResponse<FindIndexArticlePageListRspVO> findArticlePageList(FindIndexArticlePageListReqVO findIndexArticlePageListReqVO);
}

View File

@@ -0,0 +1,13 @@
package com.hanserwei.web.service;
import com.hanserwei.common.utils.Response;
import com.hanserwei.web.model.vo.blogsetting.FindBlogSettingsDetailRspVO;
public interface BlogSettingsService {
/**
* 获取博客设置信息
*
* @return 响应
*/
Response<FindBlogSettingsDetailRspVO> findDetail();
}

View File

@@ -0,0 +1,15 @@
package com.hanserwei.web.service;
import com.hanserwei.common.utils.Response;
import com.hanserwei.web.model.vo.category.FindCategoryListRspVO;
import java.util.List;
public interface CategoryService {
/**
* 获取分类列表
*
* @return 分类列表
*/
Response<List<FindCategoryListRspVO>> findCategoryList();
}

View File

@@ -0,0 +1,15 @@
package com.hanserwei.web.service;
import com.hanserwei.common.utils.Response;
import com.hanserwei.web.model.vo.tag.FindTagListRspVO;
import java.util.List;
public interface TagService {
/**
* 获取标签列表
*
* @return 标签列表
*/
Response<List<FindTagListRspVO>> findTagList();
}

View File

@@ -0,0 +1,130 @@
package com.hanserwei.web.service.impl;
import com.hanserwei.common.domain.dataobject.*;
import com.hanserwei.common.domain.repository.*;
import com.hanserwei.common.utils.PageHelper;
import com.hanserwei.common.utils.PageResponse;
import com.hanserwei.web.model.vo.article.FindIndexArticlePageListReqVO;
import com.hanserwei.web.model.vo.article.FindIndexArticlePageListRspVO;
import com.hanserwei.web.model.vo.category.FindCategoryListRspVO;
import com.hanserwei.web.model.vo.tag.FindTagListRspVO;
import com.hanserwei.web.service.ArticleService;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class ArticleServiceImpl implements ArticleService {
@Resource
private ArticleRepository articleRepository;
@Resource
private CategoryRepository categoryRepository;
@Resource
private ArticleContentRepository articleContentRepository;
@Resource
private ArticleCategoryRelRepository articleCategoryRelRepository;
@Resource
private TagRepository tagRepository;
@Resource
private ArticleTagRelRepository articleTagRelRepository;
@Override
public PageResponse<FindIndexArticlePageListRspVO> findArticlePageList(FindIndexArticlePageListReqVO findIndexArticlePageListReqVO) {
// 查询文章主体数据
PageResponse<Article> articlePageResponse = PageHelper.findPageList(articleRepository,
findIndexArticlePageListReqVO,
null,
null,
null,
null,
e -> e);
List<Article> articleList = articlePageResponse.getData();
List<FindIndexArticlePageListRspVO> vos = null;
if (!CollectionUtils.isEmpty(articleList)) {
vos = articleList.stream().map(e -> {
FindIndexArticlePageListRspVO vo = new FindIndexArticlePageListRspVO();
BeanUtils.copyProperties(e, vo);
// 手动设置 createTime因为 Instant 无法自动转换为 LocalDateTime
if (e.getCreateTime() != null) {
vo.setCreateTime(java.time.LocalDateTime.ofInstant(e.getCreateTime(), java.time.ZoneId.systemDefault()));
}
return vo;
}).toList();
// 拿到所有文章的 ID 集合
List<Long> articleIds = articleList.stream().map(Article::getId).toList();
// 第二步:设置文章所属分类
// 根据文章 ID 批量查询所有关联记录
List<ArticleCategoryRel> articleCategoryRelList = articleCategoryRelRepository.findByArticleIdIn(articleIds);
// 获取所有分类ID
List<Long> categoryIds = articleCategoryRelList.stream()
.map(ArticleCategoryRel::getCategoryId)
.distinct()
.toList();
// 查询所有分类
List<Category> categories = categoryRepository.findAllByIdIn(categoryIds);
// 转 Map, 方便后续根据分类 ID 拿到对应的分类名称
Map<Long, String> categoryIdNameMap = categories.stream().collect(Collectors.toMap(Category::getId, Category::getName));
vos.forEach(vo -> {
Long currentArticleId = vo.getId();
// 过滤出当前文章对应的关联数据
Optional<ArticleCategoryRel> optional = articleCategoryRelList.stream()
.filter(e -> Objects.equals(e.getArticleId(), currentArticleId))
.findAny();
// 如果不为空
if (optional.isPresent()) {
ArticleCategoryRel articleCategoryRel = optional.get();
Long categoryId = articleCategoryRel.getCategoryId();
// 通过分类 ID 从 map 中拿到对应的分类名称
String categoryName = categoryIdNameMap.get(categoryId);
FindCategoryListRspVO findCategoryListRspVO = FindCategoryListRspVO.builder()
.id(categoryId)
.name(categoryName)
.build();
// 设置到当前 vo 类中
vo.setCategory(findCategoryListRspVO);
}
});
// 第三步:设置文章标签
// 查询所有标签
List<Tag> allTagList = tagRepository.findAll();
// 转 Map, 方便后续根据标签 ID 拿到对应的标签名称
Map<Long, String> tagIdNameMap = allTagList.stream().collect(Collectors.toMap(Tag::getId, Tag::getName));
// 拿到所有文章的标签关联记录
List<ArticleTagRel> articleTagRelList = articleTagRelRepository.findByArticleIdIn(articleIds);
vos.forEach(vo -> {
Long currentArticleId = vo.getId();
// 过滤出当前文章对应的关联标签数据
List<ArticleTagRel> currentArticleTagRelList = articleTagRelList.stream()
.filter(e -> Objects.equals(e.getArticleId(), currentArticleId))
.toList();
List<FindTagListRspVO> findTagListRspVOS = new ArrayList<>();
// 将关联记录 DO 转 VO, 并设置对应的标签名称
currentArticleTagRelList.forEach(e -> {
Long tagId = e.getTagId();
String tagName = tagIdNameMap.get(tagId);
FindTagListRspVO findTagListRspVO = FindTagListRspVO.builder()
.id(tagId)
.name(tagName)
.build();
findTagListRspVOS.add(findTagListRspVO);
});
// 设置转换后的标签数据
vo.setTags(findTagListRspVOS);
});
}
PageResponse<FindIndexArticlePageListRspVO> findIndexArticlePageListRspVOPageResponse = new PageResponse<>();
BeanUtils.copyProperties(articlePageResponse, findIndexArticlePageListRspVOPageResponse);
findIndexArticlePageListRspVOPageResponse.setData(vos);
return findIndexArticlePageListRspVOPageResponse;
}
public ArticleContentRepository getArticleContentRepository() {
return articleContentRepository;
}
}

View File

@@ -0,0 +1,36 @@
package com.hanserwei.web.service.impl;
import com.hanserwei.common.domain.repository.BlogSettingsRepository;
import com.hanserwei.common.utils.Response;
import com.hanserwei.web.model.vo.blogsetting.FindBlogSettingsDetailRspVO;
import com.hanserwei.web.service.BlogSettingsService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@Service
public class BlogSettingsServiceImpl implements BlogSettingsService {
@Resource
private BlogSettingsRepository blogSettingsRepository;
@Override
public Response<FindBlogSettingsDetailRspVO> findDetail() {
return blogSettingsRepository.findById(1L)
.map(e -> {
FindBlogSettingsDetailRspVO vo = FindBlogSettingsDetailRspVO.builder()
.author(e.getAuthor())
.logo(e.getLogo())
.name(e.getName())
.introduction(e.getIntroduction())
.avatar(e.getAvatar())
.githubHomepage(e.getGithubHomepage())
.csdnHomepage(e.getCsdnHomepage())
.giteeHomepage(e.getGiteeHomepage())
.zhihuHomepage(e.getZhihuHomepage())
.build();
return Response.success(vo);
})
.orElse(Response.success(null));
}
}

View File

@@ -0,0 +1,34 @@
package com.hanserwei.web.service.impl;
import com.hanserwei.common.domain.dataobject.Category;
import com.hanserwei.common.domain.repository.CategoryRepository;
import com.hanserwei.common.utils.Response;
import com.hanserwei.web.model.vo.category.FindCategoryListRspVO;
import com.hanserwei.web.service.CategoryService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
@Service
public class CategoryServiceImpl implements CategoryService {
@Resource
private CategoryRepository categoryRepository;
@Override
public Response<List<FindCategoryListRspVO>> findCategoryList() {
List<Category> categoryList = categoryRepository.findAll();
List<FindCategoryListRspVO> vos = null;
if (!CollectionUtils.isEmpty(categoryList)) {
vos = categoryList.stream().map(category -> {
FindCategoryListRspVO vo = new FindCategoryListRspVO();
vo.setId(category.getId());
vo.setName(category.getName());
return vo;
}).toList();
}
return Response.success(vos);
}
}

View File

@@ -0,0 +1,32 @@
package com.hanserwei.web.service.impl;
import com.hanserwei.common.domain.dataobject.Tag;
import com.hanserwei.common.domain.repository.TagRepository;
import com.hanserwei.common.utils.Response;
import com.hanserwei.web.model.vo.tag.FindTagListRspVO;
import com.hanserwei.web.service.TagService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
@Service
public class TagServiceImpl implements TagService {
@Resource
private TagRepository tagRepository;
@Override
public Response<List<FindTagListRspVO>> findTagList() {
List<Tag> tagList = tagRepository.findAll();
List<FindTagListRspVO> tagListVO = null;
if (!CollectionUtils.isEmpty(tagList)) {
tagListVO = tagList.stream().map(tag -> FindTagListRspVO.builder()
.id(tag.getId())
.name(tag.getName())
.build()).toList();
}
return Response.success(tagListVO);
}
}