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

@@ -1,6 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ApifoxUploaderProjectSetting"> <component name="ApifoxUploaderProjectSetting">
<option name="apiAccessToken" value="APS-mJ5jVj0KjHRVPvJnChI91r8WFqR0oXhE" /> <option name="apiAccessToken" value="APS-nkhftrUwkg4bhzK4DRYUNguJFix8j1fd" />
<option name="apiProjectIds">
<array>
<option value="&lt;byte-array&gt;rO0ABXNyADZjb20uaXRhbmdjZW50LmlkZWEucGx1Z2luLmFwaS5hY2NvdW50LlByb2plY3RBbmRNb2R1bGUAAAAAAAAAAQIAFVoABmVuYWJsZUwACG1vZHVsZUlkdAASTGphdmEvbGFuZy9TdHJpbmc7TAAGb3RoZXIxcQB+AAFMAAdvdGhlcjEwcQB+AAFMAAdvdGhlcjExcQB+AAFMAAdvdGhlcjEycQB+AAFMAAZvdGhlcjJxAH4AAUwABm90aGVyM3EAfgABTAAGb3RoZXI0cQB+AAFMAAZvdGhlcjVxAH4AAUwABm90aGVyNnEAfgABTAAGb3RoZXI3cQB+AAFMAAZvdGhlcjhxAH4AAUwABm90aGVyOXEAfgABTAAKcGF0aEJlZm9yZXEAfgABTAANcHJvamVjdEZvbGRlcnEAfgABTAAPcHJvamVjdEZvbGRlcklkcQB+AAFMAAlwcm9qZWN0SWRxAH4AAUwAC3Byb2plY3ROYW1lcQB+AAFMAAxzY2hlbWFGb2xkZXJxAH4AAUwACHNjaGVtYUlkcQB+AAF4cAF0ACp3ZWJsb2ctc3ByaW5nYm9vdC53ZWJsb2ctbW9kdWxlLWFkbWluLm1haW50AAc3MjIwMjQ1cHBwdAAHNjY1NzM5NHQAC2JyYW5jaC1tYWludAAM6buY6K6k5qih5Z2XcHBwcHB0AAB0AAnmoLnnm67lvZV0AAEwdAAHNzQ4NDkxM3QABVdCbG9ncQB+AAlxAH4ACg==&lt;/byte-array&gt;" />
</array>
</option>
<option name="treeNodes" value="&lt;byte-array&gt;rO0ABXNyABdqYXZhLnV0aWwuTGlua2VkSGFzaE1hcDTATlwQbMD7AgABWgALYWNjZXNzT3JkZXJ4cgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAMdwgAAAAQAAAAAnQABzM0MjU5MzBzcgAuY29tLml0YW5nY2VudC5pZGVhLnBsdWdpbi5hcGkuYWNjb3VudC5UcmVlTm9kZQAAAAAAAAABAgAQTAAHYWxsUGF0aHQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAFGJyYW5jaEFuZFZlcnNpb25JdGVtdABLW0xjb20vaXRhbmdjZW50L2lkZWEvcGx1Z2luL2RpYWxvZy9jb21wb25lbnQvYWNjb3VudC9BY2NvdW50UmlnaHRQYW5lbEl0ZW07TAAUYnJhbmNoSWRBbmRWZXJzaW9uSWRxAH4ABUwACGNoaWxkcmVudAAPTGphdmEvdXRpbC9NYXA7TAAKZm9sZGVyVHlwZXEAfgAFTAAIZnVsbFBhdGhxAH4ABUwAA2tleXEAfgAFWwAJbW9kZWxJdGVtcQB+AAZMAAhtb2R1bGVJZHEAfgAFTAAEbmFtZXEAfgAFTAAIcGFyZW50SWRxAH4ABUwACXByb2plY3RJZHEAfgAFTAALcHJvamVjdE5hbWVxAH4ABUwABnRlYW1JZHEAfgAFTAAIdGVhbU5hbWVxAH4ABUwABHR5cGV0ADBMY29tL2l0YW5nY2VudC9pZGVhL3BsdWdpbi9hcGkvYWNjb3VudC9Ob2RlVHlwZTt4cHQADOS4quS6uuWboumYn3Bwc3EAfgAAP0AAAAAAAAx3CAAAABAAAAABdAAHNzQ4NDkxM3NxAH4ABHQAEuS4quS6uuWboumYny9XQmxvZ3Bwc3EAfgAAP0AAAAAAAAB3CAAAABAAAAAAeABwcHEAfgAMcHB0AA9XQmxvZyAoNzQ4NDkxMyl0AAczNDI1OTMwcQB+AAx0AAVXQmxvZ3EAfgARcH5yAC5jb20uaXRhbmdjZW50LmlkZWEucGx1Z2luLmFwaS5hY2NvdW50Lk5vZGVUeXBlAAAAAAAAAAASAAB4cgAOamF2YS5sYW5nLkVudW0AAAAAAAAAABIAAHhwdAAHUFJPSkVDVHgAcHBxAH4AA3BwcQB+AApwcHBxAH4AA3EAfgAKfnEAfgATdAAEVEVBTXQABzM5NjY0MDlzcQB+AAR0AAlIYW5zZXJEZXZwcHNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAAXQABzc0NjI0NzJzcQB+AAR0ABlIYW5zZXJEZXYvSU4tQUktaW50ZXJ2aWV3cHBzcQB+AAA/QAAAAAAAAHcIAAAAEAAAAAB4AHBwcQB+AB1wcHQAGUlOLUFJLWludGVydmlldyAoNzQ2MjQ3Mil0AAczOTY2NDA5cQB+AB10AA9JTi1BSS1pbnRlcnZpZXdxAH4AInBxAH4AFXgAcHBxAH4AGXBwcQB+ABtwcHBxAH4AGXEAfgAbcQB+ABd4AA==&lt;/byte-array&gt;" />
<option name="treeNodesJTree" value="&lt;byte-array&gt;rO0ABXNyACFqYXZheC5zd2luZy50cmVlLkRlZmF1bHRUcmVlTW9kZWynvpEmGsXl2QMAA1oAEmFza3NBbGxvd3NDaGlsZHJlbkwADGxpc3RlbmVyTGlzdHQAJUxqYXZheC9zd2luZy9ldmVudC9FdmVudExpc3RlbmVyTGlzdDtMAARyb290dAAbTGphdmF4L3N3aW5nL3RyZWUvVHJlZU5vZGU7eHAAc3IAI2phdmF4LnN3aW5nLmV2ZW50LkV2ZW50TGlzdGVuZXJMaXN0kUjMLXPfDt4DAAB4cHB4c3IAJ2phdmF4LnN3aW5nLnRyZWUuRGVmYXVsdE11dGFibGVUcmVlTm9kZcRYv/zyqHHgAwADWgAOYWxsb3dzQ2hpbGRyZW5MAAhjaGlsZHJlbnQAEkxqYXZhL3V0aWwvVmVjdG9yO0wABnBhcmVudHQAIkxqYXZheC9zd2luZy90cmVlL011dGFibGVUcmVlTm9kZTt4cAFzcgAQamF2YS51dGlsLlZlY3RvctmXfVuAO68BAwADSQARY2FwYWNpdHlJbmNyZW1lbnRJAAxlbGVtZW50Q291bnRbAAtlbGVtZW50RGF0YXQAE1tMamF2YS9sYW5nL09iamVjdDt4cAAAAAAAAAACdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAACnNxAH4ABgFzcQB+AAoAAAAAAAAAAXVxAH4ADQAAAApzcQB+AAYBcHEAfgAPdXEAfgANAAAAAnQACnVzZXJPYmplY3RzcgAuY29tLml0YW5nY2VudC5pZGVhLnBsdWdpbi5hcGkuYWNjb3VudC5UcmVlTm9kZQAAAAAAAAABAgAQTAAHYWxsUGF0aHQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAFGJyYW5jaEFuZFZlcnNpb25JdGVtdABLW0xjb20vaXRhbmdjZW50L2lkZWEvcGx1Z2luL2RpYWxvZy9jb21wb25lbnQvYWNjb3VudC9BY2NvdW50UmlnaHRQYW5lbEl0ZW07TAAUYnJhbmNoSWRBbmRWZXJzaW9uSWRxAH4AFkwACGNoaWxkcmVudAAPTGphdmEvdXRpbC9NYXA7TAAKZm9sZGVyVHlwZXEAfgAWTAAIZnVsbFBhdGhxAH4AFkwAA2tleXEAfgAWWwAJbW9kZWxJdGVtcQB+ABdMAAhtb2R1bGVJZHEAfgAWTAAEbmFtZXEAfgAWTAAIcGFyZW50SWRxAH4AFkwACXByb2plY3RJZHEAfgAWTAALcHJvamVjdE5hbWVxAH4AFkwABnRlYW1JZHEAfgAWTAAIdGVhbU5hbWVxAH4AFkwABHR5cGV0ADBMY29tL2l0YW5nY2VudC9pZGVhL3BsdWdpbi9hcGkvYWNjb3VudC9Ob2RlVHlwZTt4cHQAEuS4quS6uuWboumYny9XQmxvZ3VyAEtbTGNvbS5pdGFuZ2NlbnQuaWRlYS5wbHVnaW4uZGlhbG9nLmNvbXBvbmVudC5hY2NvdW50LkFjY291bnRSaWdodFBhbmVsSXRlbTspvFKeKrgMqQIAAHhwAAAAAXNyAEhjb20uaXRhbmdjZW50LmlkZWEucGx1Z2luLmRpYWxvZy5jb21wb25lbnQuYWNjb3VudC5BY2NvdW50UmlnaHRQYW5lbEl0ZW0AAAAAAAAAAQIABFoAD2lzTWFpbk9yRGVmYXVsdEwACGljb25UeXBlcQB+ABZMAAJpZHEAfgAWTAAEbmFtZXEAfgAWeHABdAAGYnJhbmNodAAHNzIyMDI0NXQABG1haW5wc3IAF2phdmEudXRpbC5MaW5rZWRIYXNoTWFwNMBOXBBswPsCAAFaAAthY2Nlc3NPcmRlcnhyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeABwcHQABzc0ODQ5MTN1cQB+ABwAAAABc3EAfgAeAXQABW1vZGVsdAAHNjY1NzM5NHQADOm7mOiupOaooeWdl3B0AA9XQmxvZyAoNzQ4NDkxMyl0AAczNDI1OTMwdAAHNzQ4NDkxM3QABVdCbG9ndAAHMzQyNTkzMHB+cgAuY29tLml0YW5nY2VudC5pZGVhLnBsdWdpbi5hcGkuYWNjb3VudC5Ob2RlVHlwZQAAAAAAAAAAEgAAeHIADmphdmEubGFuZy5FbnVtAAAAAAAAAAASAAB4cHQAB1BST0pFQ1R4cHBwcHBwcHBweHEAfgAJdXEAfgANAAAAAnEAfgAUc3EAfgAVdAAM5Liq5Lq65Zui6ZifcHBzcQB+ACM/QAAAAAAAAHcIAAAAEAAAAAB4AHBwdAAHMzQyNTkzMHBwdAAM5Liq5Lq65Zui6ZifcHBwdAAHMzQyNTkzMHQADOS4quS6uuWboumYn35xAH4AMXQABFRFQU14c3EAfgAGAXNxAH4ACgAAAAAAAAABdXEAfgANAAAACnNxAH4ABgFwcQB+AD91cQB+AA0AAAACcQB+ABRzcQB+ABV0ABlIYW5zZXJEZXYvSU4tQUktaW50ZXJ2aWV3cHBzcQB+ACM/QAAAAAAAAHcIAAAAEAAAAAB4AHBwdAAHNzQ2MjQ3MnBwdAAZSU4tQUktaW50ZXJ2aWV3ICg3NDYyNDcyKXQABzM5NjY0MDl0AAc3NDYyNDcydAAPSU4tQUktaW50ZXJ2aWV3dAAHMzk2NjQwOXBxAH4AM3hwcHBwcHBwcHB4cQB+AAl1cQB+AA0AAAACcQB+ABRzcQB+ABV0AAlIYW5zZXJEZXZwcHNxAH4AIz9AAAAAAAAAdwgAAAAQAAAAAHgAcHB0AAczOTY2NDA5cHB0AAlIYW5zZXJEZXZwcHB0AAczOTY2NDA5dAAJSGFuc2VyRGV2cQB+AD14cHBwcHBwcHB4cHVxAH4ADQAAAAJxAH4AFHNxAH4AFXQABFJvb3RwcHBwcHQAATBwcHEAfgBXcHBwcHBxAH4APXhzcQB+AAoAAAAAAAAAAnVxAH4ADQAAAAp0AARyb290cQB+AAlwcHBwcHBwcHh4&lt;/byte-array&gt;" />
</component> </component>
</project> </project>

View File

@@ -2,5 +2,6 @@
<project version="4"> <project version="4">
<component name="DataSourcePerFileMappings"> <component name="DataSourcePerFileMappings">
<file url="file://$PROJECT_DIR$/sql/createTable.sql" value="bb8330e4-9a89-4978-ad63-ad6402096c16" /> <file url="file://$PROJECT_DIR$/sql/createTable.sql" value="bb8330e4-9a89-4978-ad63-ad6402096c16" />
<file url="file://$PROJECT_DIR$/weblog-module-common/src/main/java/com/hanserwei/common/domain/dataobject/Category.java" value="bb8330e4-9a89-4978-ad63-ad6402096c16" />
</component> </component>
</project> </project>

1
.idea/sqldialects.xml generated
View File

@@ -2,6 +2,5 @@
<project version="4"> <project version="4">
<component name="SqlDialectMappings"> <component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/sql/createTable.sql" dialect="PostgreSQL" /> <file url="file://$PROJECT_DIR$/sql/createTable.sql" dialect="PostgreSQL" />
<file url="PROJECT" dialect="PostgreSQL" />
</component> </component>
</project> </project>

View File

@@ -40,11 +40,59 @@ CREATE TABLE t_user_role
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
username VARCHAR(60) NOT NULL, username VARCHAR(60) NOT NULL,
role_name VARCHAR(60) NOT NULL, -- 重命名为 role_name 避免关键字冲突 role_name VARCHAR(60) NOT NULL, -- 重命名为 role_name 避免关键字冲突
create_time TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
); );
CREATE INDEX idx_username ON t_user_role (username); CREATE INDEX idx_username ON t_user_role (username);
COMMENT ON COLUMN t_user_role.role_name IS '角色名称'; COMMENT ON COLUMN t_user_role.role_name IS '角色名称';
-- 为 t_user_role 表创建触发器
CREATE TRIGGER set_t_user_role_update_time
BEFORE UPDATE
ON t_user_role
FOR EACH ROW
EXECUTE FUNCTION set_update_time();
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
CREATE TABLE t_category
(
-- id对应 MySQL 的 bigint(20) unsigned NOT NULL AUTO_INCREMENT
id BIGSERIAL PRIMARY KEY,
-- 分类名称VARCHAR(60) NOT NULL DEFAULT '',同时是 UNIQUE 约束
"name" VARCHAR(60) NOT NULL DEFAULT '',
-- 创建时间
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
-- 最后一次更新时间
update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
-- 逻辑删除标志位tinyint(2) NOT NULL DEFAULT '0',改为 BOOLEAN
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
-- UNIQUE KEY uk_name (`name`)
CONSTRAINT uk_name UNIQUE ("name")
);
-- 添加非唯一索引(对应 MySQL 的 KEY `idx_create_time`
CREATE INDEX idx_create_time ON t_category (create_time);
-- 可选:添加注释
COMMENT ON TABLE t_category IS '文章分类表';
COMMENT ON COLUMN t_category.id IS '分类id';
COMMENT ON COLUMN t_category.name IS '分类名称';
COMMENT ON COLUMN t_category.create_time IS '创建时间';
COMMENT ON COLUMN t_category.update_time IS '最后一次更新时间';
COMMENT ON COLUMN t_category.is_deleted IS '逻辑删除标志位FALSE未删除 TRUE已删除';
-- 为 t_category 表创建触发器
CREATE TRIGGER set_t_category_update_time
BEFORE UPDATE
ON t_category
FOR EACH ROW
EXECUTE FUNCTION set_update_time();
-- ==================================================================================================================== -- ====================================================================================================================
-- ==================================================================================================================== -- ====================================================================================================================

View File

@@ -8,6 +8,7 @@ dependencies {
implementation(project(":weblog-module-common")) implementation(project(":weblog-module-common"))
api(project(":weblog-module-jwt")) api(project(":weblog-module-jwt"))
implementation("org.springframework.boot:spring-boot-starter-validation")
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test") testImplementation("org.springframework.security:spring-security-test")

View File

@@ -0,0 +1,67 @@
package com.hanserwei.admin.controller;
import com.hanserwei.admin.model.vo.AddCategoryReqVO;
import com.hanserwei.admin.model.vo.DeleteCategoryReqVO;
import com.hanserwei.admin.model.vo.FindCategoryPageListReqVO;
import com.hanserwei.admin.model.vo.FindCategoryPageListRspVO;
import com.hanserwei.admin.service.AdminCategoryService;
import com.hanserwei.common.aspect.ApiOperationLog;
import com.hanserwei.common.model.vo.SelectRspVO;
import com.hanserwei.common.utils.PageResponse;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 管理端分类控制器
*/
@RestController
@RequestMapping("/admin")
public class AdminCategoryController {
@Resource
private AdminCategoryService adminCategoryService;
/**
* 添加分类
*/
@PostMapping("/category/add")
@ApiOperationLog(description = "添加分类")
public Response<?> addCategory(@RequestBody @Validated AddCategoryReqVO addCategoryReqVO) {
return adminCategoryService.addCategory(addCategoryReqVO);
}
/**
* 分类分页数据获取
*/
@PostMapping("/category/list")
@ApiOperationLog(description = "分类分页数据获取")
public PageResponse<FindCategoryPageListRspVO> findCategoryList(@RequestBody @Validated FindCategoryPageListReqVO findCategoryPageListReqVO) {
return adminCategoryService.findCategoryList(findCategoryPageListReqVO);
}
/**
* 删除分类
*/
@PostMapping("/category/delete")
@ApiOperationLog(description = "删除分类")
public Response<?> deleteCategory(@RequestBody @Validated DeleteCategoryReqVO deleteCategoryReqVO) {
return adminCategoryService.deleteCategory(deleteCategoryReqVO);
}
/**
* 分类下拉列表
*/
@PostMapping("/category/select/list")
@ApiOperationLog(description = "分类 Select 下拉列表数据获取")
public Response<List<SelectRspVO>> findCategorySelectList() {
return adminCategoryService.findCategorySelectList();
}
}

View File

@@ -0,0 +1,42 @@
package com.hanserwei.admin.controller;
import com.hanserwei.admin.model.vo.FindUserInfoRspVO;
import com.hanserwei.admin.model.vo.UpdateAdminUserPasswordReqVO;
import com.hanserwei.admin.service.AdminUserService;
import com.hanserwei.common.aspect.ApiOperationLog;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 管理端用户控制器
*/
@RestController
@RequestMapping("/admin")
public class AdminUserController {
@Resource
private AdminUserService adminUserService;
/**
* 修改用户密码
*/
@PostMapping("/password/update")
@ApiOperationLog(description = "修改用户密码")
public Response<?> updatePassword(@RequestBody @Validated UpdateAdminUserPasswordReqVO updateAdminUserPasswordReqVO) {
return adminUserService.updatePassword(updateAdminUserPasswordReqVO);
}
/**
* 获取用户信息
*/
@PostMapping("/user/info")
@ApiOperationLog(description = "获取用户信息")
public Response<FindUserInfoRspVO> findUserInfo() {
return adminUserService.findUserInfo();
}
}

View File

@@ -0,0 +1,23 @@
package com.hanserwei.admin.model.vo;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AddCategoryReqVO {
/**
* 分类名称
*/
@NotBlank(message = "分类名称不能为空")
@Length(min = 1, max = 10, message = "分类名称字数限制 1 ~ 10 之间")
private String name;
}

View File

@@ -0,0 +1,18 @@
package com.hanserwei.admin.model.vo;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DeleteCategoryReqVO {
@NotNull(message = "分类 ID 不能为空")
private Long id;
}

View File

@@ -0,0 +1,30 @@
package com.hanserwei.admin.model.vo;
import com.hanserwei.common.model.BasePageQuery;
import lombok.*;
import java.time.LocalDate;
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindCategoryPageListReqVO extends BasePageQuery {
/**
* 分类名称
*/
private String name;
/**
* 创建的起始日期
*/
private LocalDate startDate;
/**
* 创建的结束日期
*/
private LocalDate endDate;
}

View File

@@ -0,0 +1,30 @@
package com.hanserwei.admin.model.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindCategoryPageListRspVO {
/**
* 分类 ID
*/
private Long id;
/**
* 分类名称
*/
private String name;
/**
* 创建时间
*/
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,18 @@
package com.hanserwei.admin.model.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindUserInfoRspVO {
/**
* 用户名
*/
private String username;
}

View File

@@ -0,0 +1,26 @@
package com.hanserwei.admin.model.vo;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UpdateAdminUserPasswordReqVO {
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
private String username;
/**
* 新密码
*/
@NotBlank(message = "密码不能为空")
private String password;
}

View File

@@ -0,0 +1,45 @@
package com.hanserwei.admin.service;
import com.hanserwei.admin.model.vo.AddCategoryReqVO;
import com.hanserwei.admin.model.vo.DeleteCategoryReqVO;
import com.hanserwei.admin.model.vo.FindCategoryPageListReqVO;
import com.hanserwei.admin.model.vo.FindCategoryPageListRspVO;
import com.hanserwei.common.model.vo.SelectRspVO;
import com.hanserwei.common.utils.PageResponse;
import com.hanserwei.common.utils.Response;
import java.util.List;
public interface AdminCategoryService {
/**
* 添加分类
*
* @param addCategoryReqVO 添加分类请求参数
* @return 添加结果
*/
Response<?> addCategory(AddCategoryReqVO addCategoryReqVO);
/**
* 分类分页数据查询
*
* @param findCategoryPageListReqVO 分页查询分类参数
* @return 查询结果
*/
PageResponse<FindCategoryPageListRspVO> findCategoryList(FindCategoryPageListReqVO findCategoryPageListReqVO);
/**
* 删除分类
*
* @param deleteCategoryReqVO 删除分类参数
* @return 删除结果
*/
Response<?> deleteCategory(DeleteCategoryReqVO deleteCategoryReqVO);
/**
* 获取文章分类的 Select 列表数据
*
* @return Select 列表数据
*/
Response<List<SelectRspVO>> findCategorySelectList();
}

View File

@@ -0,0 +1,20 @@
package com.hanserwei.admin.service;
import com.hanserwei.admin.model.vo.FindUserInfoRspVO;
import com.hanserwei.admin.model.vo.UpdateAdminUserPasswordReqVO;
import com.hanserwei.common.utils.Response;
public interface AdminUserService {
/**
* 修改密码
* @param updateAdminUserPasswordReqVO 修改密码参数
* @return 修改密码结果
*/
Response<?> updatePassword(UpdateAdminUserPasswordReqVO updateAdminUserPasswordReqVO);
/**
* 获取当前登录用户信息
* @return 当前登录用户信息
*/
Response<FindUserInfoRspVO> findUserInfo();
}

View File

@@ -0,0 +1,120 @@
package com.hanserwei.admin.service.impl;
import com.hanserwei.admin.model.vo.AddCategoryReqVO;
import com.hanserwei.admin.model.vo.DeleteCategoryReqVO;
import com.hanserwei.admin.model.vo.FindCategoryPageListReqVO;
import com.hanserwei.admin.model.vo.FindCategoryPageListRspVO;
import com.hanserwei.admin.service.AdminCategoryService;
import com.hanserwei.common.domain.dataobject.Category;
import com.hanserwei.common.domain.repository.CategoryRepository;
import com.hanserwei.common.enums.ResponseCodeEnum;
import com.hanserwei.common.exception.BizException;
import com.hanserwei.common.model.vo.SelectRspVO;
import com.hanserwei.common.utils.PageResponse;
import com.hanserwei.common.utils.Response;
import io.jsonwebtoken.lang.Strings;
import jakarta.annotation.Resource;
import jakarta.persistence.criteria.Predicate;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
public class AdminCategoryServiceImpl implements AdminCategoryService {
@Resource
private CategoryRepository categoryRepository;
@Override
public Response<?> addCategory(AddCategoryReqVO addCategoryReqVO) {
String categoryName = addCategoryReqVO.getName();
// 先判断是否存在
if (categoryRepository.existsCategoryByName(categoryName)) {
throw new BizException(ResponseCodeEnum.CATEGORY_NAME_IS_EXISTED);
}
// 构造Category对象
Category category = Category.builder()
.name(categoryName)
.build();
categoryRepository.save(category);
return Response.success();
}
@Override
public PageResponse<FindCategoryPageListRspVO> findCategoryList(FindCategoryPageListReqVO findCategoryPageListReqVO) {
Long current = findCategoryPageListReqVO.getCurrent();
Long size = findCategoryPageListReqVO.getSize();
Pageable pageable = PageRequest.of(current.intValue() - 1,
size.intValue(),
Sort.by(Sort.Direction.DESC, "createTime"));
// 构建查询条件
Specification<Category> specification = (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
String name = findCategoryPageListReqVO.getName();
if (Strings.hasText(name)) {
predicates.add(
criteriaBuilder.like(root.get("name"), "%" + name.trim() + "%")
);
}
if (Objects.nonNull(findCategoryPageListReqVO.getStartDate())){
predicates.add(
criteriaBuilder.greaterThanOrEqualTo(root.get("createTime"), findCategoryPageListReqVO.getStartDate())
);
}
if (Objects.nonNull(findCategoryPageListReqVO.getEndDate())) {
predicates.add(
criteriaBuilder.lessThan(root.get("createTime"), findCategoryPageListReqVO.getEndDate().plusDays(1).atStartOfDay())
);
}
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
Page<Category> categoryDOPage = categoryRepository.findAll(specification, pageable);
List<FindCategoryPageListRspVO> vos = categoryDOPage.getContent().stream()
.map(category -> FindCategoryPageListRspVO.builder()
.id(category.getId())
.name(category.getName())
.createTime(LocalDateTime.ofInstant(category.getCreateTime(), ZoneId.systemDefault()))
.build())
.collect(Collectors.toList());
return PageResponse.success(categoryDOPage, vos);
}
@Override
public Response<?> deleteCategory(DeleteCategoryReqVO deleteCategoryReqVO) {
Long id = deleteCategoryReqVO.getId();
categoryRepository.deleteById(id);
return Response.success();
}
@Override
public Response<List<SelectRspVO>> findCategorySelectList() {
List<Category> categoryList = categoryRepository.findAll();
// DO 转 VO
List<SelectRspVO> selectRspVOS = null;
if (!CollectionUtils.isEmpty(categoryList)){
selectRspVOS = categoryList.stream()
.map(category -> SelectRspVO.builder()
.label(category.getName())
.value(category.getId())
.build())
.toList();
}
return Response.success(selectRspVOS);
}
}

View File

@@ -0,0 +1,51 @@
package com.hanserwei.admin.service.impl;
import com.hanserwei.admin.model.vo.FindUserInfoRspVO;
import com.hanserwei.admin.model.vo.UpdateAdminUserPasswordReqVO;
import com.hanserwei.admin.service.AdminUserService;
import com.hanserwei.common.domain.repository.UserRepository;
import com.hanserwei.common.enums.ResponseCodeEnum;
import com.hanserwei.common.exception.BizException;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class AdminUserServiceImpl implements AdminUserService {
@Resource
private UserRepository userRepository;
@Resource
private PasswordEncoder passwordEncoder;
@Override
public Response<?> updatePassword(UpdateAdminUserPasswordReqVO updateAdminUserPasswordReqVO) {
// 拿到用户名密码
String username = updateAdminUserPasswordReqVO.getUsername();
String password = updateAdminUserPasswordReqVO.getPassword();
// 加密密码
String encodePassword = passwordEncoder.encode(password);
int updatedRows = userRepository.updatePasswordByUsername(username, encodePassword);
if (updatedRows == 0) {
// 如果更新行数为 0说明用户名不存在
throw new BizException(ResponseCodeEnum.USER_NOT_EXIST);
}
return Response.success();
}
@Override
public Response<FindUserInfoRspVO> findUserInfo() {
// 获取存储在 ThreadLocal 中的用户信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 拿到用户名
String username = authentication.getName();
return Response.success(new FindUserInfoRspVO(username));
}
}

View File

@@ -47,6 +47,8 @@ public class JacksonConfig {
// 设置凡是为 null 的字段,返参中均不返回,请根据项目组约定是否开启 // 设置凡是为 null 的字段,返参中均不返回,请根据项目组约定是否开启
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 忽略未知属性
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper; 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.Getter;
import lombok.Setter; import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import org.hibernate.annotations.UpdateTimestamp; import org.hibernate.annotations.UpdateTimestamp;
import java.io.Serial; import java.io.Serial;
@@ -18,7 +20,9 @@ import java.time.Instant; // 推荐用于 TIMESTAMP WITH TIME ZONE
@Setter @Setter
@Getter @Getter
@Builder @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 { public class User implements Serializable {
@Serial @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 com.hanserwei.common.domain.dataobject.User;
import org.springframework.data.jpa.repository.JpaRepository; 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> { public interface UserRepository extends JpaRepository<User, Long> {
User getUsersByUsername(String username); 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", "登录失败"), LOGIN_FAIL("20000", "登录失败"),
USERNAME_OR_PWD_ERROR("20001", "用户名或密码错误"), USERNAME_OR_PWD_ERROR("20001", "用户名或密码错误"),
UNAUTHORIZED("20002", "无访问权限,请先登录!"), 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;
}
}

View File

@@ -1,48 +0,0 @@
package com.hanserwei.web.controller;
import com.hanserwei.common.aspect.ApiOperationLog;
import com.hanserwei.common.utils.Response;
import com.hanserwei.web.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.stream.Collectors;
@RestController
@Slf4j
public class TestController {
@PostMapping("/admin/test")
@ApiOperationLog(description = "测试接口")
public ResponseEntity<String>test(@RequestBody @Validated User user, BindingResult bindingResult) {
// 是否存在校验错误
if (bindingResult.hasErrors()) {
// 获取校验不通过字段的提示信息
String errorMsg = bindingResult.getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(errorMsg);
}
// 返参
return ResponseEntity.ok("参数没有任何问题");
}
@PostMapping("/admin/update")
@ApiOperationLog(description = "测试更新接口")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public Response<?> testUpdate() {
log.info("更新成功...");
return Response.success();
}
}