feat(article): 实现管理端文章模块的增删改查功能
- 新增文章表、内容表、标签关联及分类关联的数据库设计与实现 - 实现文章发布、删除、分页查询、详情查看及更新接口 - 文章发布时支持分类验证和标签新增与绑定 - 删除操作会级联删除文章关联的分类和标签关系 - 查询详情接口返回文章基本信息、正文内容、分类及标签列表 - 支持根据标签ID列表批量查询标签信息 - 管理分类接口新增根据ID查询分类详情功能 - 删除分类、标签时增加文章关联校验,防止误删 - 统一返回结构,异常时抛出业务异常,规范日志输出 - 统一使用JPA进行数据库操作,保障事务一致性 - 优化查询性能,添加必要的索引及外键约束 - 补充对应请求和响应的VO类,支持参数校验与业务传递
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
package com.hanserwei.common.domain.dataobject;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.SQLDelete;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Entity
|
||||
@Table(name = "t_article", indexes = {
|
||||
@Index(name = "idx_article_create_time", columnList = "create_time")
|
||||
})
|
||||
@SQLRestriction("is_deleted = false")
|
||||
@SQLDelete(sql = "update t_article set is_deleted = true where id = ?")
|
||||
public class Article implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, length = 120)
|
||||
private String title;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
@Builder.Default
|
||||
private String cover = "";
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
@Builder.Default
|
||||
private String summary = "";
|
||||
|
||||
@Column(name = "read_num", nullable = false)
|
||||
@Builder.Default
|
||||
private Integer readNum = 1;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "create_time", nullable = false, updatable = false)
|
||||
private Instant createTime;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "update_time", nullable = false)
|
||||
private Instant updateTime;
|
||||
|
||||
@Column(name = "is_deleted", nullable = false)
|
||||
@Builder.Default
|
||||
private Boolean isDeleted = false;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.hanserwei.common.domain.dataobject;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Entity
|
||||
@Table(name = "t_article_category_rel",
|
||||
indexes = {
|
||||
@Index(name = "idx_rel_cat_category_id", columnList = "category_id")
|
||||
})
|
||||
public class ArticleCategoryRel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 文章ID (Unique)
|
||||
*/
|
||||
@Column(name = "article_id", nullable = false, unique = true)
|
||||
private Long articleId;
|
||||
|
||||
/**
|
||||
* 分类ID
|
||||
*/
|
||||
@Column(name = "category_id", nullable = false)
|
||||
private Long categoryId;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.hanserwei.common.domain.dataobject;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Entity
|
||||
@Table(name = "t_article_content", indexes = {
|
||||
@Index(name = "idx_article_content_article_id", columnList = "article_id")
|
||||
})
|
||||
public class ArticleContent implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 关联 Article 的 ID
|
||||
* 这里只存储 ID,不建立对象关联,以实现轻量级操作
|
||||
*/
|
||||
@Column(name = "article_id", nullable = false)
|
||||
private Long articleId;
|
||||
|
||||
/**
|
||||
* 正文内容
|
||||
* 映射为 TEXT 类型,支持大文本
|
||||
*/
|
||||
@Column(name = "content", columnDefinition = "TEXT")
|
||||
private String content;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.hanserwei.common.domain.dataobject;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Entity
|
||||
@Table(name = "t_article_tag_rel",
|
||||
indexes = {
|
||||
@Index(name = "idx_rel_tag_article_id", columnList = "article_id"),
|
||||
@Index(name = "idx_rel_tag_tag_id", columnList = "tag_id")
|
||||
},
|
||||
uniqueConstraints = {
|
||||
// 对应数据库中的联合唯一约束
|
||||
@UniqueConstraint(name = "uk_article_tag_rel_unique_pair", columnNames = {"article_id", "tag_id"})
|
||||
})
|
||||
public class ArticleTagRel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 文章ID
|
||||
*/
|
||||
@Column(name = "article_id", nullable = false)
|
||||
private Long articleId;
|
||||
|
||||
/**
|
||||
* 标签ID
|
||||
*/
|
||||
@Column(name = "tag_id", nullable = false)
|
||||
private Long tagId;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.hanserwei.common.domain.repository;
|
||||
|
||||
import com.hanserwei.common.domain.dataobject.ArticleCategoryRel;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ArticleCategoryRelRepository extends JpaRepository<ArticleCategoryRel, Long> {
|
||||
@Modifying(flushAutomatically = true, clearAutomatically = true)
|
||||
@Query("delete from ArticleCategoryRel acr where acr.articleId = :articleId")
|
||||
void deleteByArticleId(@Param("articleId") Long articleId);
|
||||
|
||||
ArticleCategoryRel findByArticleId(Long articleId);
|
||||
|
||||
List<ArticleCategoryRel> findByCategoryId(Long categoryId);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.hanserwei.common.domain.repository;
|
||||
|
||||
import com.hanserwei.common.domain.dataobject.ArticleContent;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ArticleContentRepository extends JpaRepository<ArticleContent, Long> {
|
||||
ArticleContent findByArticleId(Long articleId);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.hanserwei.common.domain.repository;
|
||||
|
||||
import com.hanserwei.common.domain.dataobject.Article;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
public interface ArticleRepository extends JpaRepository<Article, Long>, JpaSpecificationExecutor<Article> {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.hanserwei.common.domain.repository;
|
||||
|
||||
import com.hanserwei.common.domain.dataobject.ArticleTagRel;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ArticleTagRelRepository extends JpaRepository<ArticleTagRel, Long> {
|
||||
@Modifying(flushAutomatically = true, clearAutomatically = true)
|
||||
@Query("delete from ArticleTagRel atr where atr.articleId = :articleId")
|
||||
void deleteByArticleId(@Param("articleId") Long articleId);
|
||||
|
||||
List<ArticleTagRel> findByArticleId(Long articleId);
|
||||
|
||||
List<ArticleTagRel> findByTagId(Long tagId);
|
||||
}
|
||||
@@ -22,4 +22,5 @@ public interface TagRepository extends JpaRepository<Tag, Long>, JpaSpecificatio
|
||||
*/
|
||||
List<Tag> findByNameContaining(String name);
|
||||
|
||||
List<Tag> queryAllByIdIn(Collection<Long> ids);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,12 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
|
||||
CATEGORY_NAME_IS_EXISTED("20005", "该分类已存在,请勿重复添加!"),
|
||||
TAG_NOT_EXIST("20006", "标签不存在!"),
|
||||
CATEGORY_NOT_EXIST("20007", "分类不存在!"),
|
||||
FILE_UPLOAD_FAILED("20008", "上传文件失败!");
|
||||
FILE_UPLOAD_FAILED("20008", "上传文件失败!"),
|
||||
CATEGORY_NOT_EXISTED("20009", "提交的分类不存在!"),
|
||||
TAG_NOT_EXISTED("20010", "标签ID不存在或已删除!"),
|
||||
ARTICLE_NOT_EXIST("20011", "文章不存在!"),
|
||||
TAG_HAS_ARTICLE("20012", "该标签有关联文章,无法删除!"),
|
||||
CATEGORY_HAS_ARTICLE("20013","该分类下有关联文章,无法删除!" );
|
||||
|
||||
// 异常码
|
||||
private final String errorCode;
|
||||
|
||||
@@ -25,20 +25,22 @@ public class PageHelper {
|
||||
/**
|
||||
* 执行带条件的分页查询
|
||||
*
|
||||
* @param repository JPA Repository
|
||||
* @param pageQuery 分页查询参数
|
||||
* @param name 名称(用于模糊查询)
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @param converter DO 到 VO 的转换函数
|
||||
* @param <T> 实体类型
|
||||
* @param <R> 响应VO类型
|
||||
* @param repository JPA Repository
|
||||
* @param pageQuery 分页查询参数
|
||||
* @param searchText 搜索文本(用于模糊查询)
|
||||
* @param searchField 搜索字段名称(如 "name"、"title" 等)
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @param converter DO 到 VO 的转换函数
|
||||
* @param <T> 实体类型
|
||||
* @param <R> 响应VO类型
|
||||
* @return 分页响应
|
||||
*/
|
||||
public static <T, R> PageResponse<R> findPageList(
|
||||
JpaSpecificationExecutor<T> repository,
|
||||
BasePageQuery pageQuery,
|
||||
String name,
|
||||
String searchText,
|
||||
String searchField,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
Function<T, R> converter) {
|
||||
@@ -54,10 +56,10 @@ public class PageHelper {
|
||||
Specification<T> specification = (root, query, criteriaBuilder) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
// 名称模糊查询
|
||||
if (StringUtils.hasText(name)) {
|
||||
// 搜索文本模糊查询
|
||||
if (StringUtils.hasText(searchText) && StringUtils.hasText(searchField)) {
|
||||
predicates.add(
|
||||
criteriaBuilder.like(root.get("name"), "%" + name.trim() + "%")
|
||||
criteriaBuilder.like(root.get(searchField), "%" + searchText.trim() + "%")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,4 +90,27 @@ public class PageHelper {
|
||||
|
||||
return PageResponse.success(page, vos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行带条件的分页查询(使用默认字段名 "name")
|
||||
*
|
||||
* @param repository JPA Repository
|
||||
* @param pageQuery 分页查询参数
|
||||
* @param name 名称(用于模糊查询)
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @param converter DO 到 VO 的转换函数
|
||||
* @param <T> 实体类型
|
||||
* @param <R> 响应VO类型
|
||||
* @return 分页响应
|
||||
*/
|
||||
public static <T, R> PageResponse<R> findPageList(
|
||||
JpaSpecificationExecutor<T> repository,
|
||||
BasePageQuery pageQuery,
|
||||
String name,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
Function<T, R> converter) {
|
||||
return findPageList(repository, pageQuery, name, "name", startDate, endDate, converter);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user