feat(admin): implement category management functionality

- Added AddCategoryReqVO for category creation with validation
- Created AdminCategoryController with endpoints for add, list, delete and select operations
- Implemented AdminCategoryService interface and its methods
- Added Category entity with JPA annotations and logical delete support
- Created CategoryRepository extending JpaRepository with custom query methods
- Added SQL table creation script for t_category with indexes and constraints
- Implemented PageResponse utility for handling paginated results
- Added FindCategoryPageListReqVO and FindCategoryPageListRspVO for pagination
- Included DeleteCategoryReqVO for category deletion requests
- Updated Jackson configuration to ignore unknown properties
- Added base page query model and user info response VO
- Fixed typo in response code enum for user not exist error
This commit is contained in:
2025-11-30 22:09:26 +08:00
parent 0a126eb520
commit 7380f783ee
27 changed files with 788 additions and 53 deletions

View File

@@ -47,6 +47,8 @@ public class JacksonConfig {
// 设置凡是为 null 的字段,返参中均不返回,请根据项目组约定是否开启
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 忽略未知属性
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper;
}

View File

@@ -0,0 +1,71 @@
package com.hanserwei.common.domain.dataobject;
import jakarta.persistence.*; // 使用 Jakarta Persistence API (JPA 3.0+)
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import org.hibernate.annotations.*;
import lombok.*; // 引入所有 Lombok 注解
import java.io.Serial;
import java.io.Serializable;
import java.time.Instant;
/**
* 文章分类表t_category 对应实体)
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "t_category",
uniqueConstraints = {
@UniqueConstraint(name = "uk_name", columnNames = {"name"})
},
indexes = {
@Index(name = "idx_create_time", columnList = "create_time")
})
@SQLRestriction("is_deleted = false")
@SQLDelete(sql = "UPDATE t_category SET is_deleted = true WHERE id = ?")
public class Category implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 分类ID (BIG SERIAL PRIMARY KEY)
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
/**
* 分类名称 (VARCHAR(60) NOT NULL DEFAULT '')
*/
@Column(name = "name", length = 60, nullable = false)
private String name = ""; // 对应数据库的 DEFAULT ''
/**
* 创建时间 (TIMESTAMP WITHOUT TIME ZONE NOT NULL)
*/
@CreationTimestamp
@Column(name = "create_time", nullable = false, updatable = false)
private Instant createTime;
/**
* 最后一次更新时间 (TIMESTAMP WITHOUT TIME ZONE NOT NULL)
* 配合数据库触发器或 ORM 框架自动更新
*/
@UpdateTimestamp
@Column(name = "update_time", nullable = false)
private Instant updateTime;
/**
* 逻辑删除标志位FALSE未删除 TRUE已删除 (BOOLEAN NOT NULL DEFAULT FALSE)
*/
@Builder.Default
@Column(name = "is_deleted", nullable = false)
private Boolean isDeleted = false;
}

View File

@@ -5,6 +5,8 @@ import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import org.hibernate.annotations.UpdateTimestamp;
import java.io.Serial;
@@ -18,7 +20,9 @@ import java.time.Instant; // 推荐用于 TIMESTAMP WITH TIME ZONE
@Setter
@Getter
@Builder
@Table(name = "t_user") // 对应数据库中的表名
@Table(name = "t_user")
@SQLRestriction("is_deleted = false")
@SQLDelete(sql = "UPDATE t_user SET is_deleted = true WHERE id = ?")
public class User implements Serializable {
@Serial

View File

@@ -0,0 +1,13 @@
package com.hanserwei.common.domain.repository;
import com.hanserwei.common.domain.dataobject.Category;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CategoryRepository extends JpaRepository<Category, Long> {
boolean existsCategoryByName(String name);
Page<Category> findAll(Specification<Category> specification, Pageable pageable);
}

View File

@@ -2,7 +2,17 @@ package com.hanserwei.common.domain.repository;
import com.hanserwei.common.domain.dataobject.User;
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 org.springframework.transaction.annotation.Transactional;
public interface UserRepository extends JpaRepository<User, Long> {
User getUsersByUsername(String username);
@Modifying
@Transactional
@Query("UPDATE User u SET u.password = :newPassword WHERE u.username = :username")
int updatePasswordByUsername(@Param("username") String username,
@Param("newPassword") String newPassword);
}

View File

@@ -16,7 +16,9 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
LOGIN_FAIL("20000", "登录失败"),
USERNAME_OR_PWD_ERROR("20001", "用户名或密码错误"),
UNAUTHORIZED("20002", "无访问权限,请先登录!"),
FORBIDDEN("20004", "演示账号仅支持查询操作!")
FORBIDDEN("20004", "演示账号仅支持查询操作!"),
USER_NOT_EXIST("2005", "有户不存在!"),
CATEGORY_NAME_IS_EXISTED("20005", "该分类已存在,请勿重复添加!")
;
// 异常码

View File

@@ -0,0 +1,16 @@
package com.hanserwei.common.model;
import lombok.Data;
@Data
public class BasePageQuery {
/**
* 当前页码, 默认第一页
*/
private Long current = 1L;
/**
* 每页展示的数据数量,默认每页展示 10 条数据
*/
private Long size = 10L;
}

View File

@@ -0,0 +1,22 @@
package com.hanserwei.common.model.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SelectRspVO {
/**
* Select 下拉列表的展示文字
*/
private String label;
/**
* Select 下拉列表的 value 值,如 ID 等
*/
private Object value;
}

View File

@@ -0,0 +1,97 @@
package com.hanserwei.common.utils;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.data.domain.Page;
import java.util.List;
import java.util.Objects;
@EqualsAndHashCode(callSuper = true)
@Data
public class PageResponse<T> extends Response<List<T>> {
/**
* 总记录数
*/
private long total = 0L;
/**
* 每页显示的记录数,默认每页显示 10 条
*/
private long size = 10L;
/**
* 当前页码 (JPA Page 从 0 开始, 这里为方便前端, 统一改为从 1 开始)
*/
private long current;
/**
* 总页数
*/
private long pages;
/**
* 成功响应
*
* @param page Spring Data JPA 提供的分页接口
* @param <T> 响应数据类型
*/
public static <T> PageResponse<T> success(Page<T> page) {
PageResponse<T> response = new PageResponse<>();
response.setSuccess(true);
if (Objects.nonNull(page)) {
// JPA Page 的 getNumber() 是当前页码 (从 0 开始), 我们在返回时通常习惯改为从 1 开始
response.setCurrent(page.getNumber() + 1);
// JPA Page 的 getSize() 是每页大小
response.setSize(page.getSize());
// JPA Page 的 getTotalPages() 是总页数
response.setPages(page.getTotalPages());
// JPA Page 的 getTotalElements() 是总记录数
response.setTotal(page.getTotalElements());
// JPA Page 的 getContent() 是当前页的数据列表
response.setData(page.getContent());
} else {
// 如果传入的 page 为 null设置默认值
response.setCurrent(1L);
response.setSize(10L);
response.setPages(0L);
response.setTotal(0L);
response.setData(null);
}
return response;
}
// 如果您需要处理分页结果DTO例如实体转DTO可以添加一个重载方法
/**
* 成功响应 (适用于将实体 Page<E> 转换为 DTO PageResponse<D>)
*
* @param page Spring Data JPA 提供的实体分页接口
* @param data 经过转换后的 DTO 列表
* @param <E> 实体类型
* @param <D> DTO 类型
*/
public static <E, D> PageResponse<D> success(Page<E> page, List<D> data) {
PageResponse<D> response = new PageResponse<>();
response.setSuccess(true);
if (Objects.nonNull(page)) {
response.setCurrent(page.getNumber() + 1);
response.setSize(page.getSize());
response.setPages(page.getTotalPages());
response.setTotal(page.getTotalElements());
response.setData(data); // 使用传入的 DTO 列表
} else {
response.setCurrent(1L);
response.setSize(10L);
response.setPages(0L);
response.setTotal(0L);
response.setData(data);
}
return response;
}
}