Compare commits

...

5 Commits

Author SHA1 Message Date
6fae09f42f feat(article): 实现管理端文章模块的增删改查功能
- 新增文章表、内容表、标签关联及分类关联的数据库设计与实现
- 实现文章发布、删除、分页查询、详情查看及更新接口
- 文章发布时支持分类验证和标签新增与绑定
- 删除操作会级联删除文章关联的分类和标签关系
- 查询详情接口返回文章基本信息、正文内容、分类及标签列表
- 支持根据标签ID列表批量查询标签信息
- 管理分类接口新增根据ID查询分类详情功能
- 删除分类、标签时增加文章关联校验,防止误删
- 统一返回结构,异常时抛出业务异常,规范日志输出
- 统一使用JPA进行数据库操作,保障事务一致性
- 优化查询性能,添加必要的索引及外键约束
- 补充对应请求和响应的VO类,支持参数校验与业务传递
2025-12-06 17:30:49 +08:00
7db42c6c30 feat(admin): 新增博客设置及文件上传功能
- 新增博客设置数据库表结构及实体类定义
- 实现博客设置的查询与更新接口及服务层逻辑
- 实现管理端博客设置控制器,支持修改和查询博客设置详情
- 实现文件上传接口和服务,支持通过Rustfs上传文件
- 集成Rustfs客户端配置,支持与Rustfs存储系统交互
- 新增统一响应及异常处理,文件上传异常抛出自定义业务异常
- 更新错误码枚举,添加文件上传失败的错误码定义
- 增加请求参数校验,确保博客设置更新接口数据有效性
- 添加日志记录,跟踪文件上传流程及错误信息
2025-12-05 22:14:47 +08:00
304c458436 refactor(admin): 重构管理模块VO包结构并新增标签管理功能
- 将分类相关VO移至com.hanserwei.admin.model.vo.category包下,用户相关VO移至user包
- 新增标签管理相关VO,包括AddTagReqVO、DeleteTagReqVO、FindTagPageListReqVO、FindTagPageListRspVO、SearchTagReqVO
- 增加AdminTagController,实现标签的增删查和分页查询接口
- 实现AdminTagService及其Impl,完成标签的增删查分页功能
- 新增Tag实体及TagRepository,支持标签数据的持久化及模糊查询
- 优化AdminCategoryServiceImpl分页查询逻辑,提取公共分页查询工具类PageHelper
- 修改CategoryRepository继承JpaSpecificationExecutor,支持动态查询
- 修改TokenAuthenticationFilter,限制JWT认证仅校验/admin路径请求
- 修改Category实体删除注解,调整逻辑删除实现
- 新增数据库脚本,创建t_tag标签表及相关索引和触发器
- 更新ResponseCodeEnum,增加TAG_NOT_EXIST和CATEGORY_NOT_EXIST错误码
- 调整.gitignore,忽略.idea下Apifox相关缓存文件
2025-12-04 23:18:10 +08:00
b7afe9496a feat(jwt): extend token expiration time and improve authentication filter
- Extend JWT token expiration from 1 hour to 30 days
- Improve Authorization header validation in authentication filter
- Add null check for header before calling startsWith method
- Import Strings utility class for better string handling
2025-12-01 20:09:00 +08:00
7380f783ee 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
2025-11-30 22:09:49 +08:00
77 changed files with 3092 additions and 101 deletions

1
.gitignore vendored
View File

@@ -66,3 +66,4 @@ Desktop.ini
application-prod.yml
application-dev.yml
.env
/.idea/.cache/.Apifox_Helper/.toolWindow.db

View File

@@ -1,6 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<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>
</project>

View File

@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourcePerFileMappings">
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/bb8330e4-9a89-4978-ad63-ad6402096c16/console.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>
</project>

1
.idea/sqldialects.xml generated
View File

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

View File

@@ -40,11 +40,283 @@ CREATE TABLE t_user_role
id BIGSERIAL PRIMARY KEY,
username VARCHAR(60) NOT NULL,
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);
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();
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
-- 1. 创建表结构
CREATE TABLE t_tag
(
-- id: 使用 BIG SERIAL自动创建序列性能优异
id BIGSERIAL PRIMARY KEY,
-- name: 保持 VARCHAR(60),但在 PG 中 TEXT 和 VARCHAR 性能一样,
-- 这里为了保留原表 "60字符限制" 的业务逻辑,继续使用 VARCHAR(60)
name VARCHAR(60) NOT NULL DEFAULT '',
-- create_time: 使用带时区的时间戳,更标准严谨
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- update_time: 同上
update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- is_deleted: 使用原生 BOOLEAN 类型,存储效率高且语义明确
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
-- 约束:显式命名约束,方便后续维护(如报错时能看到具体约束名)
CONSTRAINT uk_tag_name UNIQUE (name)
);
-- 2. 创建普通索引
-- 对应 MySQL 的 KEY `idx_create_time`
CREATE INDEX idx_tag_create_time ON t_tag (create_time);
-- 3. 添加注释 (PostgresSQL 标准方式)
COMMENT ON TABLE t_tag IS '文章标签表';
COMMENT ON COLUMN t_tag.id IS '标签id';
COMMENT ON COLUMN t_tag.name IS '标签名称';
COMMENT ON COLUMN t_tag.create_time IS '创建时间';
COMMENT ON COLUMN t_tag.update_time IS '最后一次更新时间';
COMMENT ON COLUMN t_tag.is_deleted IS '逻辑删除标志位FALSE未删除 TRUE已删除';
-- 4. 应用自动更新时间戳触发器 (体现 PostgresSQL 强大的过程语言优势)
-- 前提:您之前已经执行过 CREATE FUNCTION set_update_time() ...
CREATE TRIGGER set_t_tag_update_time
BEFORE UPDATE
ON t_tag
FOR EACH ROW
EXECUTE FUNCTION set_update_time();
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
CREATE TABLE t_blog_settings
(
-- id: 使用 BIG SERIAL 自动管理序列
id BIGSERIAL PRIMARY KEY,
-- logo: 图片路径可能很长,使用 TEXT 替代 VARCHAR(120),无性能损耗
logo TEXT NOT NULL DEFAULT '',
-- name: 博客名称通常较短,保留 VARCHAR 限制也是一种合理的业务约束
name VARCHAR(60) NOT NULL DEFAULT '',
-- author: 作者名同上
author VARCHAR(20) NOT NULL DEFAULT '',
-- introduction: 介绍语可能会变长,使用 TEXT 更灵活
introduction TEXT NOT NULL DEFAULT '',
-- avatar: 头像路径,使用 TEXT
avatar TEXT NOT NULL DEFAULT '',
-- 下面的主页链接:原 MySQL 定义 varchar(60) 风险很高,
-- 现在的 URL 很容易超过 60 字符PG 使用 TEXT 完美解决
github_homepage TEXT NOT NULL DEFAULT '',
csdn_homepage TEXT NOT NULL DEFAULT '',
gitee_homepage TEXT NOT NULL DEFAULT '',
zhihu_homepage TEXT NOT NULL DEFAULT ''
);
-- 添加注释
COMMENT ON TABLE t_blog_settings IS '博客设置表';
COMMENT ON COLUMN t_blog_settings.id IS 'id';
COMMENT ON COLUMN t_blog_settings.logo IS '博客Logo';
COMMENT ON COLUMN t_blog_settings.name IS '博客名称';
COMMENT ON COLUMN t_blog_settings.author IS '作者名';
COMMENT ON COLUMN t_blog_settings.introduction IS '介绍语';
COMMENT ON COLUMN t_blog_settings.avatar IS '作者头像';
COMMENT ON COLUMN t_blog_settings.github_homepage IS 'GitHub 主页访问地址';
COMMENT ON COLUMN t_blog_settings.csdn_homepage IS 'CSDN 主页访问地址';
COMMENT ON COLUMN t_blog_settings.gitee_homepage IS 'Gitee 主页访问地址';
COMMENT ON COLUMN t_blog_settings.zhihu_homepage IS '知乎主页访问地址';
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
CREATE TABLE t_article
(
id BIGSERIAL PRIMARY KEY,
-- 标题:保持限制,适合作为索引或列表显示
title VARCHAR(120) NOT NULL DEFAULT '',
-- 封面:使用 TEXT不再担心 URL 超长
cover TEXT NOT NULL DEFAULT '',
-- 摘要:使用 TEXT不再受 160 字限制,前端截取即可
summary TEXT DEFAULT '',
-- 阅读量:使用 INTEGER 配合 CHECK 约束模拟 unsigned
read_num INTEGER NOT NULL DEFAULT 1 CHECK (read_num >= 0),
-- 时间与逻辑删除
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
);
-- 索引
CREATE INDEX idx_article_create_time ON t_article (create_time);
-- 自动更新时间戳触发器
CREATE TRIGGER set_t_article_update_time
BEFORE UPDATE
ON t_article
FOR EACH ROW
EXECUTE FUNCTION set_update_time();
-- 注释
COMMENT ON TABLE t_article IS '文章表';
COMMENT ON COLUMN t_article.read_num IS '被阅读次数 (>=0)';
COMMENT ON COLUMN t_article.cover IS '文章封面';
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
CREATE TABLE t_article_content
(
id BIGSERIAL PRIMARY KEY,
-- 外键关联字段
article_id BIGINT NOT NULL,
-- 正文PG 的 TEXT 能够容纳海量文字
content TEXT,
-- 显式外键约束:确保 article_id 必须存在于 t_article 表中
-- ON DELETE CASCADE: 如果物理删除了 t_article对应的内容也会被自动删除
CONSTRAINT fk_article_content_article_id
FOREIGN KEY (article_id)
REFERENCES t_article (id)
ON DELETE CASCADE
);
-- 索引
CREATE INDEX idx_article_content_article_id ON t_article_content (article_id);
-- 注释
COMMENT ON TABLE t_article_content IS '文章内容表';
COMMENT ON COLUMN t_article_content.content IS '教程正文';
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
CREATE TABLE t_article_category_rel
(
id BIGSERIAL PRIMARY KEY,
-- 文章 ID添加外键删除文章时自动删除此关联
article_id BIGINT NOT NULL,
-- 分类 ID添加外键删除分类时... (通常分类不轻易删,或者策略不同,这里设为级联删除)
category_id BIGINT NOT NULL,
-- 约束:保证 article_id 唯一 (对应 MySQL 的 UNIQUE KEY)
CONSTRAINT uni_article_category_rel_article_id UNIQUE (article_id),
-- 外键定义
CONSTRAINT fk_rel_cat_article
FOREIGN KEY (article_id)
REFERENCES t_article (id)
ON DELETE CASCADE,
CONSTRAINT fk_rel_cat_category
FOREIGN KEY (category_id)
REFERENCES t_category (id)
ON DELETE CASCADE
);
-- 索引category_id 需要索引以便反向查询(查询某分类下有哪些文章)
CREATE INDEX idx_rel_cat_category_id ON t_article_category_rel (category_id);
-- 注释
COMMENT ON TABLE t_article_category_rel IS '文章所属分类关联表';
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
-- ====================================================================================================================
CREATE TABLE t_article_tag_rel
(
id BIGSERIAL PRIMARY KEY,
article_id BIGINT NOT NULL,
tag_id BIGINT NOT NULL,
-- 外键定义:级联删除
CONSTRAINT fk_rel_tag_article
FOREIGN KEY (article_id)
REFERENCES t_article (id)
ON DELETE CASCADE,
CONSTRAINT fk_rel_tag_tag
FOREIGN KEY (tag_id)
REFERENCES t_tag (id)
ON DELETE CASCADE,
-- ⚡ 优化:防止同一篇文章被打上重复的标签
CONSTRAINT uk_article_tag_rel_unique_pair UNIQUE (article_id, tag_id)
);
-- 索引
CREATE INDEX idx_rel_tag_article_id ON t_article_tag_rel (article_id);
CREATE INDEX idx_rel_tag_tag_id ON t_article_tag_rel (tag_id);
-- 注释
COMMENT ON TABLE t_article_tag_rel IS '文章对应标签关联表';
-- ====================================================================================================================
-- ====================================================================================================================

View File

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

View File

@@ -0,0 +1,75 @@
package com.hanserwei.admin.controller;
import com.hanserwei.admin.model.vo.article.*;
import com.hanserwei.admin.service.AdminArticleService;
import com.hanserwei.common.aspect.ApiOperationLog;
import com.hanserwei.common.utils.PageResponse;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
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/article")
public class AdminArticleController {
@Resource
private AdminArticleService articleService;
/**
* 发布文章
*/
@PostMapping("/publish")
@ApiOperationLog(description = "文章发布")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public Response<?> publishArticle(@RequestBody @Validated PublishArticleReqVO publishArticleReqVO) {
return articleService.publishArticle(publishArticleReqVO);
}
/**
* 删除文章
*/
@PostMapping("/delete")
@ApiOperationLog(description = "文章删除")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public Response<?> deleteArticle(@RequestBody @Validated DeleteArticleReqVO deleteArticleReqVO) {
return articleService.deleteArticle(deleteArticleReqVO);
}
/**
* 查询文章分页数据
*/
@PostMapping("/list")
@ApiOperationLog(description = "查询文章分页数据")
public PageResponse<FindArticlePageListRspVO> findArticlePageList(@RequestBody @Validated FindArticlePageListReqVO findArticlePageListReqVO) {
return articleService.findArticlePageList(findArticlePageListReqVO);
}
/**
* 查询文章详情
*/
@PostMapping("/detail")
@ApiOperationLog(description = "查询文章详情")
public Response<FindArticleDetailRspVO> findArticleDetail(@RequestBody @Validated FindArticleDetailReqVO findArticlePageListReqVO) {
return articleService.findArticleDetail(findArticlePageListReqVO);
}
/**
* 更新文章
*/
@PostMapping("/update")
@ApiOperationLog(description = "更新文章")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public Response<?> updateArticle(@RequestBody @Validated UpdateArticleReqVO updateArticleReqVO) {
return articleService.updateArticle(updateArticleReqVO);
}
}

View File

@@ -0,0 +1,43 @@
package com.hanserwei.admin.controller;
import com.hanserwei.admin.model.vo.setting.FindBlogSettingsRspVO;
import com.hanserwei.admin.model.vo.setting.UpdateBlogSettingsReqVO;
import com.hanserwei.admin.service.AdminBlogSettingsService;
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/blog/settings")
public class AdminBlogSettingsController {
@Resource
private AdminBlogSettingsService blogSettingsService;
/**
* 博客基础信息修改
*/
@PostMapping("/update")
@ApiOperationLog(description = "博客基础信息修改")
public Response<?> updateBlogSettings(@RequestBody @Validated UpdateBlogSettingsReqVO updateBlogSettingsReqVO) {
return blogSettingsService.updateBlogSettings(updateBlogSettingsReqVO);
}
/**
* 获取博客设置详情
*/
@PostMapping("/detail")
@ApiOperationLog(description = "获取博客设置详情")
public Response<FindBlogSettingsRspVO> findDetail() {
return blogSettingsService.findDetail();
}
}

View File

@@ -0,0 +1,70 @@
package com.hanserwei.admin.controller;
import com.hanserwei.admin.model.vo.category.*;
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.*;
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();
}
/**
* 根据分类ID查询
*/
@GetMapping("/category/find/{id}")
@ApiOperationLog(description = "根据分类ID查询")
public Response<FindCategoryByIdRspVO> findCategoryById(@PathVariable Long id) {
return adminCategoryService.findCategoryById(id);
}
}

View File

@@ -0,0 +1,33 @@
package com.hanserwei.admin.controller;
import com.hanserwei.admin.model.vo.file.UploadFileRspVO;
import com.hanserwei.admin.service.AdminFileService;
import com.hanserwei.common.aspect.ApiOperationLog;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 管理端文件控制器
*/
@RestController
@RequestMapping("/admin")
public class AdminFileController {
@Resource
private AdminFileService fileService;
/**
* 文件上传
*/
@PostMapping("/file/upload")
@ApiOperationLog(description = "文件上传")
public Response<UploadFileRspVO> uploadFile(@RequestParam("file") MultipartFile file) {
return fileService.uploadFile(file);
}
}

View File

@@ -0,0 +1,73 @@
package com.hanserwei.admin.controller;
import com.hanserwei.admin.model.vo.tag.*;
import com.hanserwei.admin.service.AdminTagService;
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 AdminTagController {
@Resource
private AdminTagService adminTagService;
/**
* 添加标签
*/
@PostMapping("/tag/add")
@ApiOperationLog(description = "添加标签")
public Response<?> addTag(@RequestBody @Validated AddTagReqVO addTagReqVO) {
return adminTagService.addTag(addTagReqVO);
}
/**
* 标签分页数据获取
*/
@PostMapping("/tag/list")
@ApiOperationLog(description = "标签分页数据获取")
public PageResponse<FindTagPageListRspVO> findTagList(@RequestBody @Validated FindTagPageListReqVO findTagPageListReqVO) {
return adminTagService.findTagList(findTagPageListReqVO);
}
/**
* 删除标签
*/
@PostMapping("/tag/delete")
@ApiOperationLog(description = "删除标签")
public Response<?> deleteTag(@RequestBody @Validated DeleteTagReqVO deleteTagReqVO) {
return adminTagService.deleteTag(deleteTagReqVO);
}
/**
* 标签模糊查询
*/
@PostMapping("/tag/search")
@ApiOperationLog(description = "标签模糊查询")
public Response<List<SelectRspVO>> searchTag(@RequestBody @Validated SearchTagReqVO searchTagReqVO) {
return adminTagService.searchTag(searchTagReqVO);
}
/**
* 根据ID列表获取标签
*/
@PostMapping("/tag/list/ids")
@ApiOperationLog(description = "根据ID列表获取标签")
public Response<List<FindTagsByIdsRspVO>> findTagsByIds(@RequestBody @Validated FindTagsByIdsReqVO findTagsByIdsReqVO) {
return adminTagService.findTagsByIds(findTagsByIdsReqVO);
}
}

View File

@@ -0,0 +1,42 @@
package com.hanserwei.admin.controller;
import com.hanserwei.admin.model.vo.user.FindUserInfoRspVO;
import com.hanserwei.admin.model.vo.user.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,20 @@
package com.hanserwei.admin.model.vo.article;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DeleteArticleReqVO {
/**
* 文章ID
*/
@NotNull(message = "文章 ID 不能为空")
private Long id;
}

View File

@@ -0,0 +1,21 @@
package com.hanserwei.admin.model.vo.article;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindArticleDetailReqVO {
/**
* 文章 ID
*/
@NotNull(message = "文章 ID 不能为空")
private Long id;
}

View File

@@ -0,0 +1,51 @@
package com.hanserwei.admin.model.vo.article;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindArticleDetailRspVO {
/**
* 文章 ID
*/
private Long id;
/**
* 文章标题
*/
private String title;
/**
* 文章封面
*/
private String cover;
/**
* 文章内容
*/
private String content;
/**
* 分类 ID
*/
private Long categoryId;
/**
* 标签 ID 集合
*/
private List<Long> tagIds;
/**
* 摘要
*/
private String summary;
}

View File

@@ -0,0 +1,30 @@
package com.hanserwei.admin.model.vo.article;
import com.hanserwei.common.model.BasePageQuery;
import lombok.*;
import java.time.LocalDate;
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindArticlePageListReqVO extends BasePageQuery {
/**
* 文章标题
*/
private String title;
/**
* 发布的起始日期
*/
private LocalDate startDate;
/**
* 发布的结束日期
*/
private LocalDate endDate;
}

View File

@@ -0,0 +1,36 @@
package com.hanserwei.admin.model.vo.article;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindArticlePageListRspVO {
/**
* 文章 ID
*/
private Long id;
/**
* 文章标题
*/
private String title;
/**
* 文章封面
*/
private String cover;
/**
* 发布时间
*/
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,59 @@
package com.hanserwei.admin.model.vo.article;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
/*
* 发布文章请求参数
*/
public class PublishArticleReqVO {
/**
* 文章标题
*/
@NotBlank(message = "文章标题不能为空")
@Length(min = 1, max = 40, message = "文章标题字数需大于 1 小于 40")
private String title;
/**
* 文章内容
*/
@NotBlank(message = "文章内容不能为空")
private String content;
/**
* 文章封面图片URL
*/
@NotBlank(message = "文章封面不能为空")
private String cover;
/**
* 文章摘要
*/
private String summary;
/**
* 文章分类ID
*/
@NotNull(message = "文章分类不能为空")
private Long categoryId;
/**
* 文章标签列表
*/
@NotEmpty(message = "文章标签不能为空")
private List<String> tags;
}

View File

@@ -0,0 +1,65 @@
package com.hanserwei.admin.model.vo.article;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import java.util.List;
/**
* 更新文章请求参数
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UpdateArticleReqVO {
/**
* 文章ID
*/
@NotNull(message = "文章 ID 不能为空")
private Long id;
/**
* 文章标题
*/
@NotBlank(message = "文章标题不能为空")
@Length(min = 1, max = 40, message = "文章标题字数需大于 1 小于 40")
private String title;
/**
* 文章内容
*/
@NotBlank(message = "文章内容不能为空")
private String content;
/**
* 文章封面图片URL
*/
@NotBlank(message = "文章封面不能为空")
private String cover;
/**
* 文章摘要
*/
private String summary;
/**
* 文章分类ID
*/
@NotNull(message = "文章分类不能为空")
private Long categoryId;
/**
* 文章标签列表
*/
@NotEmpty(message = "文章标签不能为空")
private List<String> tags;
}

View File

@@ -0,0 +1,23 @@
package com.hanserwei.admin.model.vo.category;
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.category;
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,21 @@
package com.hanserwei.admin.model.vo.category;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindCategoryByIdRspVO {
/**
* 分类 ID
*/
private Long id;
/**
* 分类名称
*/
private String name;
}

View File

@@ -0,0 +1,30 @@
package com.hanserwei.admin.model.vo.category;
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.category;
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,19 @@
package com.hanserwei.admin.model.vo.file;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UploadFileRspVO {
/**
* 文件的访问链接
*/
private String url;
}

View File

@@ -0,0 +1,62 @@
package com.hanserwei.admin.model.vo.setting;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 查询博客设置响应 VO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindBlogSettingsRspVO {
/**
* 博客 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,68 @@
package com.hanserwei.admin.model.vo.setting;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 更新博客设置请求 VO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UpdateBlogSettingsReqVO {
/**
* 博客 LOGO
*/
@NotBlank(message = "博客 LOGO 不能为空")
private String logo;
/**
* 博客名称
*/
@NotBlank(message = "博客名称不能为空")
private String name;
/**
* 博客作者
*/
@NotBlank(message = "博客作者不能为空")
private String author;
/**
* 博客介绍语
*/
@NotBlank(message = "博客介绍语不能为空")
private String introduction;
/**
* 博客头像
*/
@NotBlank(message = "博客头像不能为空")
private String avatar;
/**
* GitHub 主页
*/
private String githubHomepage;
/**
* CSDN 主页
*/
private String csdnHomepage;
/**
* Gitee 主页
*/
private String giteeHomepage;
/**
* 知乎主页
*/
private String zhihuHomepage;
}

View File

@@ -0,0 +1,22 @@
package com.hanserwei.admin.model.vo.tag;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AddTagReqVO {
/**
* 标签集合
*/
@NotEmpty(message = "标签集合 不能为空")
private List<String> tags;
}

View File

@@ -0,0 +1,20 @@
package com.hanserwei.admin.model.vo.tag;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DeleteTagReqVO {
/**
* 标签 ID
*/
@NotNull(message = "标签 ID 不能为空")
private Long id;
}

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
package com.hanserwei.admin.model.vo.tag;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FindTagsByIdsReqVO {
/**
* 标签 ID 集合
*/
@NotEmpty(message = "标签 ID 集合不能为空")
private List<Long> tagIds;
}

View File

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

View File

@@ -0,0 +1,19 @@
package com.hanserwei.admin.model.vo.tag;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SearchTagReqVO {
@NotEmpty(message = "标签查询关键词不能为空!")
private String key;
}

View File

@@ -0,0 +1,18 @@
package com.hanserwei.admin.model.vo.user;
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.user;
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,47 @@
package com.hanserwei.admin.service;
import com.hanserwei.admin.model.vo.article.*;
import com.hanserwei.common.utils.PageResponse;
import com.hanserwei.common.utils.Response;
public interface AdminArticleService {
/**
* 发布文章
*
* @param publishArticleReqVO 文章发布参数
* @return 响应
*/
Response<?> publishArticle(PublishArticleReqVO publishArticleReqVO);
/**
* 删除文章
*
* @param deleteArticleReqVO 删除文章参数
* @return 响应
*/
Response<?> deleteArticle(DeleteArticleReqVO deleteArticleReqVO);
/**
* 查询文章分页数据
*
* @param findArticlePageListReqVO 查询文章分页请求参数
* @return 响应
*/
PageResponse<FindArticlePageListRspVO> findArticlePageList(FindArticlePageListReqVO findArticlePageListReqVO);
/**
* 查询文章详情
*
* @param findArticleDetailReqVO 查询文章详情参数
* @return 响应
*/
Response<FindArticleDetailRspVO> findArticleDetail(FindArticleDetailReqVO findArticleDetailReqVO);
/**
* 更新文章
*
* @param updateArticleReqVO 更新文章参数
* @return 响应
*/
Response<?> updateArticle(UpdateArticleReqVO updateArticleReqVO);
}

View File

@@ -0,0 +1,22 @@
package com.hanserwei.admin.service;
import com.hanserwei.admin.model.vo.setting.FindBlogSettingsRspVO;
import com.hanserwei.admin.model.vo.setting.UpdateBlogSettingsReqVO;
import com.hanserwei.common.utils.Response;
public interface AdminBlogSettingsService {
/**
* 更新博客设置信息
*
* @param updateBlogSettingsReqVO 博客设置信息
* @return 响应
*/
Response<?> updateBlogSettings(UpdateBlogSettingsReqVO updateBlogSettingsReqVO);
/**
* 获取博客设置详情
*
* @return 博客设置详情
*/
Response<FindBlogSettingsRspVO> findDetail();
}

View File

@@ -0,0 +1,49 @@
package com.hanserwei.admin.service;
import com.hanserwei.admin.model.vo.category.*;
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();
/**
* 根据分类 ID 查询分类
*
* @param id 分类 ID
* @return 查询结果
*/
Response<FindCategoryByIdRspVO> findCategoryById(Long id);
}

View File

@@ -0,0 +1,15 @@
package com.hanserwei.admin.service;
import com.hanserwei.admin.model.vo.file.UploadFileRspVO;
import com.hanserwei.common.utils.Response;
import org.springframework.web.multipart.MultipartFile;
public interface AdminFileService {
/**
* 上传文件
*
* @param file 文件
* @return 访问地址
*/
Response<UploadFileRspVO> uploadFile(MultipartFile file);
}

View File

@@ -0,0 +1,50 @@
package com.hanserwei.admin.service;
import com.hanserwei.admin.model.vo.tag.*;
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 AdminTagService {
/**
* 添加标签
*
* @param addTagReqVO 添加标签请求参数
* @return 响应结果
*/
Response<?> addTag(AddTagReqVO addTagReqVO);
/**
* 标签分页数据获取
*
* @param findTagPageListReqVO 标签分页数据获取请求参数
* @return 响应结果
*/
PageResponse<FindTagPageListRspVO> findTagList(FindTagPageListReqVO findTagPageListReqVO);
/**
* 删除标签
*
* @param deleteTagReqVO 删除标签请求参数
* @return 响应结果
*/
Response<?> deleteTag(DeleteTagReqVO deleteTagReqVO);
/**
* 标签下拉列表数据获取
*
* @param searchTagReqVO 搜索标签请求参数
* @return 响应结果
*/
Response<List<SelectRspVO>> searchTag(SearchTagReqVO searchTagReqVO);
/**
* 根据 ID 列表获取标签
*
* @param findTagsByIdsReqVO 根据 ID 列表获取标签请求参数
* @return 响应结果
*/
Response<List<FindTagsByIdsRspVO>> findTagsByIds(FindTagsByIdsReqVO findTagsByIdsReqVO);
}

View File

@@ -0,0 +1,20 @@
package com.hanserwei.admin.service;
import com.hanserwei.admin.model.vo.user.FindUserInfoRspVO;
import com.hanserwei.admin.model.vo.user.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,202 @@
package com.hanserwei.admin.service.impl;
import com.hanserwei.admin.model.vo.article.*;
import com.hanserwei.admin.service.AdminArticleService;
import com.hanserwei.common.domain.dataobject.*;
import com.hanserwei.common.domain.repository.*;
import com.hanserwei.common.enums.ResponseCodeEnum;
import com.hanserwei.common.exception.BizException;
import com.hanserwei.common.utils.PageHelper;
import com.hanserwei.common.utils.PageResponse;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.Optional;
@Slf4j
@Service
public class AdminArticleServiceImpl implements AdminArticleService {
@Resource
private ArticleRepository articleRepository;
@Resource
private ArticleTagRelRepository articleTagRelRepository;
@Resource
private ArticleCategoryRelRepository articleCategoryRelRepository;
@Resource
private ArticleContentRepository articleContentRepository;
@Resource
private CategoryRepository categoryRepository;
@Resource
private TagRepository tagRepository;
@Override
@Transactional(rollbackFor = Exception.class)
public Response<?> publishArticle(PublishArticleReqVO publishArticleReqVO) {
Article article = new Article();
BeanUtils.copyProperties(publishArticleReqVO, article);
Article saved = articleRepository.save(article);
// 拿到保存后的文章id
Long articleId = saved.getId();
ArticleContent articleContent = ArticleContent.builder()
.articleId(articleId)
.content(publishArticleReqVO.getContent())
.build();
articleContentRepository.save(articleContent);
// 处理分类
Long categoryId = publishArticleReqVO.getCategoryId();
categoryRepository.findById(categoryId)
.ifPresentOrElse(p -> {
ArticleCategoryRel articleCategoryRel = ArticleCategoryRel.builder()
.articleId(articleId)
.categoryId(categoryId)
.build();
articleCategoryRelRepository.save(articleCategoryRel);
}, () -> {
log.warn("==>文章分类不存在: {}", categoryId);
throw new BizException(ResponseCodeEnum.CATEGORY_NOT_EXISTED);
});
// 保存标签
List<String> tags = publishArticleReqVO.getTags();
insertTags(articleId, tags);
return Response.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Response<?> deleteArticle(DeleteArticleReqVO deleteArticleReqVO) {
Long id = deleteArticleReqVO.getId();
articleRepository.deleteById(id);
articleCategoryRelRepository.deleteByArticleId(id);
articleTagRelRepository.deleteByArticleId(id);
return Response.success();
}
@Override
public PageResponse<FindArticlePageListRspVO> findArticlePageList(FindArticlePageListReqVO findArticlePageListReqVO) {
return PageHelper.findPageList(
articleRepository,
findArticlePageListReqVO,
findArticlePageListReqVO.getTitle(),
"title",
findArticlePageListReqVO.getStartDate(),
findArticlePageListReqVO.getEndDate(),
article -> FindArticlePageListRspVO.builder()
.id(article.getId())
.title(article.getTitle())
.cover(article.getCover())
.createTime(LocalDateTime.ofInstant(article.getCreateTime(), ZoneId.systemDefault()))
.build()
);
}
@Override
public Response<FindArticleDetailRspVO> findArticleDetail(FindArticleDetailReqVO findArticleDetailReqVO) {
Long id = findArticleDetailReqVO.getId();
Article article = articleRepository.findById(id)
.orElseThrow(() -> {
log.warn("==>文章不存在: {}", id);
return new BizException(ResponseCodeEnum.ARTICLE_NOT_EXIST);
});
// 文章正文详情
ArticleContent articleContent = articleContentRepository.findByArticleId(id);
// 所属分类
ArticleCategoryRel articleCategoryRel = articleCategoryRelRepository.findByArticleId(id);
// 对应标签集合
List<ArticleTagRel> articleTagRelList = articleTagRelRepository.findByArticleId(id);
List<Long> tags = articleTagRelList.stream().map(ArticleTagRel::getTagId).toList();
// 封装响应结果
FindArticleDetailRspVO findArticleDetailRspVO = new FindArticleDetailRspVO();
BeanUtils.copyProperties(article, findArticleDetailRspVO);
findArticleDetailRspVO.setTagIds(tags);
findArticleDetailRspVO.setCategoryId(articleCategoryRel.getCategoryId());
findArticleDetailRspVO.setContent(articleContent.getContent());
return Response.success(findArticleDetailRspVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Response<?> updateArticle(UpdateArticleReqVO updateArticleReqVO) {
Long id = updateArticleReqVO.getId();
Article article = articleRepository.findById(id)
.orElseThrow(() -> {
log.warn("==>欲更新的文章不存在: {}", id);
return new BizException(ResponseCodeEnum.ARTICLE_NOT_EXIST);
});
BeanUtils.copyProperties(updateArticleReqVO, article);
// 更新文章正文
ArticleContent articleContent = articleContentRepository.findByArticleId(id);
articleContent.setContent(updateArticleReqVO.getContent());
articleContentRepository.save(articleContent);
// 更新分类
Long categoryId = updateArticleReqVO.getCategoryId();
// 验证该ID的分类是否存在
categoryRepository.findById(categoryId).orElseThrow(() -> {
log.warn("==>欲更新的文章其分类不存在: {}", categoryId);
return new BizException(ResponseCodeEnum.CATEGORY_NOT_EXISTED);
});
// 先删除关联的分类记录,再插入新的关联记录
articleCategoryRelRepository.deleteByArticleId(id);
ArticleCategoryRel articleCategoryRel = ArticleCategoryRel.builder()
.articleId(id)
.categoryId(categoryId)
.build();
articleCategoryRelRepository.save(articleCategoryRel);
// 删除该文章的标签关联
articleTagRelRepository.deleteByArticleId(id);
insertTags(id, updateArticleReqVO.getTags());
return Response.success();
}
/**
* 插入标签
*
* @param articleId 文章ID
* @param tags 标签列表可能是ID字符串或标签名称
*/
private void insertTags(Long articleId, List<String> tags) {
if (CollectionUtils.isEmpty(tags)) {
return;
}
for (String tagStr : tags) {
Long tagId;
// 判断是否为数字ID
try {
tagId = Long.parseLong(tagStr);
// 验证该ID的标签是否存在
Optional<Tag> tagOptional = tagRepository.findById(tagId);
if (tagOptional.isEmpty()) {
log.warn("==>标签ID不存在: {}", tagId);
throw new BizException(ResponseCodeEnum.TAG_NOT_EXISTED);
}
} catch (NumberFormatException e) {
// 不是数字,说明是新标签名称,需要先保存标签
Tag newTag = Tag.builder()
.name(tagStr)
.build();
Tag savedTag = tagRepository.save(newTag);
tagId = savedTag.getId();
log.info("==>新增标签: {}, ID: {}", tagStr, tagId);
}
// 保存文章-标签关联关系
ArticleTagRel articleTagRel = ArticleTagRel.builder()
.articleId(articleId)
.tagId(tagId)
.build();
articleTagRelRepository.save(articleTagRel);
}
}
}

View File

@@ -0,0 +1,50 @@
package com.hanserwei.admin.service.impl;
import com.hanserwei.admin.model.vo.setting.FindBlogSettingsRspVO;
import com.hanserwei.admin.model.vo.setting.UpdateBlogSettingsReqVO;
import com.hanserwei.admin.service.AdminBlogSettingsService;
import com.hanserwei.common.domain.dataobject.BlogSettings;
import com.hanserwei.common.domain.repository.BlogSettingsRepository;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
@Service
public class AdminBlogSettingsServiceImpl implements AdminBlogSettingsService {
@Resource
private BlogSettingsRepository blogSettingsRepository;
@Override
public Response<?> updateBlogSettings(UpdateBlogSettingsReqVO updateBlogSettingsReqVO) {
// 保存或更新博客设置
blogSettingsRepository.findById(1L)
.ifPresentOrElse(existingSettings -> {
// 如果存在,则更新现有记录
BeanUtils.copyProperties(updateBlogSettingsReqVO, existingSettings);
blogSettingsRepository.saveAndFlush(existingSettings);
}, () -> {
// 如果不存在,则创建新记录
BlogSettings blogSettings = new BlogSettings();
BeanUtils.copyProperties(updateBlogSettingsReqVO, blogSettings);
blogSettings.setId(1L);
blogSettingsRepository.saveAndFlush(blogSettings);
});
return Response.success();
}
@Override
public Response<FindBlogSettingsRspVO> findDetail() {
return blogSettingsRepository.findById(1L)
.map(e -> {
FindBlogSettingsRspVO findBlogSettingsRspVO = new FindBlogSettingsRspVO();
BeanUtils.copyProperties(e, findBlogSettingsRspVO);
return Response.success(findBlogSettingsRspVO);
})
.orElse(Response.success(null));
}
}

View File

@@ -0,0 +1,102 @@
package com.hanserwei.admin.service.impl;
import com.hanserwei.admin.model.vo.category.*;
import com.hanserwei.admin.service.AdminCategoryService;
import com.hanserwei.common.domain.dataobject.ArticleCategoryRel;
import com.hanserwei.common.domain.dataobject.Category;
import com.hanserwei.common.domain.repository.ArticleCategoryRelRepository;
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.PageHelper;
import com.hanserwei.common.utils.PageResponse;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
@Service
public class AdminCategoryServiceImpl implements AdminCategoryService {
@Resource
private CategoryRepository categoryRepository;
@Resource
private ArticleCategoryRelRepository articleCategoryRelRepository;
@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) {
return PageHelper.findPageList(
categoryRepository,
findCategoryPageListReqVO,
findCategoryPageListReqVO.getName(),
findCategoryPageListReqVO.getStartDate(),
findCategoryPageListReqVO.getEndDate(),
category -> FindCategoryPageListRspVO.builder()
.id(category.getId())
.name(category.getName())
.createTime(LocalDateTime.ofInstant(category.getCreateTime(), ZoneId.systemDefault()))
.build()
);
}
@Override
public Response<?> deleteCategory(DeleteCategoryReqVO deleteCategoryReqVO) {
List<ArticleCategoryRel> articleTagRelList = articleCategoryRelRepository.findByCategoryId((deleteCategoryReqVO.getId()));
if (!CollectionUtils.isEmpty(articleTagRelList)) {
throw new BizException(ResponseCodeEnum.CATEGORY_HAS_ARTICLE);
}
return categoryRepository.findById(deleteCategoryReqVO.getId())
.map(tag -> {
tag.setIsDeleted(true);
categoryRepository.save(tag);
return Response.success();
})
.orElse(Response.fail(ResponseCodeEnum.CATEGORY_NOT_EXIST));
}
@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);
}
@Override
public Response<FindCategoryByIdRspVO> findCategoryById(Long id) {
Category category = categoryRepository.findById(id)
.orElseThrow(() -> new BizException(ResponseCodeEnum.CATEGORY_NOT_EXIST));
return Response.success(FindCategoryByIdRspVO.builder()
.id(category.getId())
.name(category.getName())
.build());
}
}

View File

@@ -0,0 +1,32 @@
package com.hanserwei.admin.service.impl;
import com.hanserwei.admin.model.vo.file.UploadFileRspVO;
import com.hanserwei.admin.service.AdminFileService;
import com.hanserwei.admin.utils.RustfsUtils;
import com.hanserwei.common.enums.ResponseCodeEnum;
import com.hanserwei.common.exception.BizException;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Service
public class AdminFileServiceImpl implements AdminFileService {
@Resource
private RustfsUtils rustfsUtils;
@Override
public Response<UploadFileRspVO> uploadFile(MultipartFile file) {
try {
// 上传文件
String url = rustfsUtils.uploadFile(file);
return Response.success(UploadFileRspVO.builder().url(url).build());
} catch (Exception e) {
log.error("==> 上传文件异常:{} ...", e.getMessage());
throw new BizException(ResponseCodeEnum.FILE_UPLOAD_FAILED);
}
}
}

View File

@@ -0,0 +1,128 @@
package com.hanserwei.admin.service.impl;
import com.hanserwei.admin.model.vo.tag.*;
import com.hanserwei.admin.service.AdminTagService;
import com.hanserwei.common.domain.dataobject.ArticleTagRel;
import com.hanserwei.common.domain.dataobject.Tag;
import com.hanserwei.common.domain.repository.ArticleTagRelRepository;
import com.hanserwei.common.domain.repository.TagRepository;
import com.hanserwei.common.enums.ResponseCodeEnum;
import com.hanserwei.common.exception.BizException;
import com.hanserwei.common.model.vo.SelectRspVO;
import com.hanserwei.common.utils.PageHelper;
import com.hanserwei.common.utils.PageResponse;
import com.hanserwei.common.utils.Response;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
@Service
public class AdminTagServiceImpl implements AdminTagService {
@Resource
private TagRepository tagRepository;
@Resource
private ArticleTagRelRepository articleTagRelRepository;
/**
* 添加标签
*
* @param addTagReqVO 添加标签请求对象
* @return 响应结果
*/
@Override
public Response<?> addTag(AddTagReqVO addTagReqVO) {
// 获取标签列表
List<String> tagList = addTagReqVO.getTags();
// 对标签进行清洗:去除空格、过滤空字符串、去重
List<String> names = tagList.stream()
.map(String::trim) // 去除首尾空格
.filter(s -> !s.isEmpty()) // 过滤空字符串
.distinct() // 去重
.toList();
// 查询数据库中已存在的标签名称
List<String> exists = tagRepository.findByNameIn(names).stream()
.map(Tag::getName)
.toList();
// 筛选出需要新建的标签,并构建标签对象
List<Tag> toCreate = names.stream()
.filter(n -> !exists.contains(n)) // 过滤掉已存在的标签
.map(n -> Tag.builder().name(n).build()) // 构建标签对象
.toList();
// 批量保存新标签到数据库
if (!toCreate.isEmpty()) {
tagRepository.saveAllAndFlush(toCreate);
}
return Response.success();
}
@Override
public PageResponse<FindTagPageListRspVO> findTagList(FindTagPageListReqVO findTagPageListReqVO) {
return PageHelper.findPageList(
tagRepository,
findTagPageListReqVO,
findTagPageListReqVO.getName(),
findTagPageListReqVO.getStartDate(),
findTagPageListReqVO.getEndDate(),
tag -> FindTagPageListRspVO.builder()
.id(tag.getId())
.name(tag.getName())
.createTime(LocalDateTime.ofInstant(tag.getCreateTime(), ZoneId.systemDefault()))
.build()
);
}
@Override
public Response<?> deleteTag(DeleteTagReqVO deleteTagReqVO) {
// 检查是否有关联的文章
List<ArticleTagRel> articleTagRelsList = articleTagRelRepository.findByTagId(deleteTagReqVO.getId());
if (!articleTagRelsList.isEmpty()) {
throw new BizException(ResponseCodeEnum.TAG_HAS_ARTICLE);
}
return tagRepository.findById(deleteTagReqVO.getId())
.map(tag -> {
tag.setIsDeleted(true);
tagRepository.save(tag);
return Response.success();
})
.orElse(Response.fail(ResponseCodeEnum.TAG_NOT_EXIST));
}
@Override
public Response<List<SelectRspVO>> searchTag(SearchTagReqVO searchTagReqVO) {
// 使用模糊查询获取标签列表
List<Tag> tags = tagRepository.findByNameContaining(searchTagReqVO.getKey());
// 将标签转换为下拉列表格式
List<SelectRspVO> vos = tags.stream()
.map(tag -> SelectRspVO.builder()
.label(tag.getName())
.value(tag.getId())
.build())
.toList();
return Response.success(vos);
}
@Override
public Response<List<FindTagsByIdsRspVO>> findTagsByIds(FindTagsByIdsReqVO findTagsByIdsReqVO) {
List<Tag> tags = tagRepository.queryAllByIdIn(findTagsByIdsReqVO.getTagIds());
List<FindTagsByIdsRspVO> vos = tags.stream()
.map(tag -> FindTagsByIdsRspVO.builder()
.id(tag.getId())
.name(tag.getName())
.build())
.toList();
return Response.success(vos);
}
}

View File

@@ -0,0 +1,51 @@
package com.hanserwei.admin.service.impl;
import com.hanserwei.admin.model.vo.user.FindUserInfoRspVO;
import com.hanserwei.admin.model.vo.user.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

@@ -0,0 +1,72 @@
package com.hanserwei.admin.utils;
import com.hanserwei.common.config.RustfsProperties;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.util.UUID;
@Component
@Slf4j
public class RustfsUtils {
@Resource
private RustfsProperties rustfsProperties;
@Resource
private S3Client s3Client;
/**
* 上传文件
*
* @param file 文件
* @return 访问地址
* @throws Exception 异常
*/
public String uploadFile(MultipartFile file) throws Exception {
// 判断文件是否为空
if (file == null || file.getSize() == 0) {
log.error("==> 上传文件异常:文件大小为空 ...");
throw new RuntimeException("文件大小不能为空");
}
// 文件的原始名称
String originalFileName = file.getOriginalFilename();
// 文件的 Content-Type
String contentType = file.getContentType();
// 生成存储对象的名称(将 UUID 字符串中的 - 替换成空字符串)
String key = UUID.randomUUID().toString().replace("-", "");
// 获取文件的后缀,如 .jpg
String suffix = null;
if (originalFileName != null) {
suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
}
// 拼接上文件后缀,即为要存储的文件名
String objectName = String.format("%s%s", key, suffix);
log.info("==> 开始上传文件至 Rustfs, ObjectName: {}", objectName);
// 上传文件至 Rustfs
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.key(objectName)
.bucket(rustfsProperties.getBucketName())
.contentType(contentType)
.contentLength(file.getSize())
.build();
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
// 返回文件的访问链接
String url = String.format("%s/%s/%s", rustfsProperties.getEndpoint(), rustfsProperties.getBucketName(), objectName);
log.info("==> 上传文件至 Rustfs 成功,访问路径: {}", url);
return url;
}
}

View File

@@ -13,6 +13,7 @@ dependencies {
api("org.springframework.boot:spring-boot-starter-web")
api("org.springframework.boot:spring-boot-starter-security")
api("org.springframework.boot:spring-boot-starter-aop")
api("software.amazon.awssdk:s3:2.40.1")
runtimeOnly("org.postgresql:postgresql")

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,32 @@
package com.hanserwei.common.config;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import java.net.URI;
@Component
public class RustfsClientConfig {
@Resource
private RustfsProperties rustfsProperties;
@Bean
public S3Client s3Client() {
return S3Client.builder()
.endpointOverride(URI.create(rustfsProperties.getEndpoint()))
.region(Region.US_EAST_1)
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(rustfsProperties.getAccessKey(), rustfsProperties.getSecretKey())
)
)
.forcePathStyle(true)
.build();
}
}

View File

@@ -0,0 +1,15 @@
package com.hanserwei.common.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Data
@ConfigurationProperties(prefix = "rustfs")
public class RustfsProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucketName;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,94 @@
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_blog_settings")
public class BlogSettings implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 博客Logo
* 数据库类型为 TEXTJava中映射为 String 即可
*/
@Column(name = "logo", nullable = false, columnDefinition = "TEXT")
@Builder.Default
private String logo = "";
/**
* 博客名称
*/
@Column(name = "name", length = 60, nullable = false)
@Builder.Default
private String name = "";
/**
* 作者名
*/
@Column(name = "author", length = 20, nullable = false)
@Builder.Default
private String author = "";
/**
* 介绍语
*/
@Column(name = "introduction", nullable = false, columnDefinition = "TEXT")
@Builder.Default
private String introduction = "";
/**
* 作者头像
*/
@Column(name = "avatar", nullable = false, columnDefinition = "TEXT")
@Builder.Default
private String avatar = "";
/**
* GitHub 主页访问地址
*/
@Column(name = "github_homepage", nullable = false, columnDefinition = "TEXT")
@Builder.Default
private String githubHomepage = "";
/**
* CSDN 主页访问地址
*/
@Column(name = "csdn_homepage", nullable = false, columnDefinition = "TEXT")
@Builder.Default
private String csdnHomepage = "";
/**
* Gitee 主页访问地址
*/
@Column(name = "gitee_homepage", nullable = false, columnDefinition = "TEXT")
@Builder.Default
private String giteeHomepage = "";
/**
* 知乎主页访问地址
*/
@Column(name = "zhihu_homepage", nullable = false, columnDefinition = "TEXT")
@Builder.Default
private String zhihuHomepage = "";
}

View File

@@ -0,0 +1,70 @@
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")
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

@@ -0,0 +1,41 @@
package com.hanserwei.common.domain.dataobject;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.SQLRestriction;
import org.hibernate.annotations.UpdateTimestamp;
import java.io.Serializable;
import java.time.Instant;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "t_tag", indexes = {
@Index(name = "idx_tag_create_time", columnList = "create_time")
})
@SQLRestriction("is_deleted = false")
public class Tag implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 60, unique = true)
private String name;
@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;
}

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,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);
}

View File

@@ -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);
}

View File

@@ -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> {
}

View File

@@ -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);
}

View File

@@ -0,0 +1,8 @@
package com.hanserwei.common.domain.repository;
import com.hanserwei.common.domain.dataobject.BlogSettings;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BlogSettingsRepository extends JpaRepository<BlogSettings, Long> {
}

View File

@@ -0,0 +1,14 @@
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;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface CategoryRepository extends JpaRepository<Category, Long>, JpaSpecificationExecutor<Category> {
boolean existsCategoryByName(String name);
Page<Category> findAll(Specification<Category> specification, Pageable pageable);
}

View File

@@ -0,0 +1,26 @@
package com.hanserwei.common.domain.repository;
import com.hanserwei.common.domain.dataobject.Tag;
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;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import java.util.Collection;
import java.util.List;
public interface TagRepository extends JpaRepository<Tag, Long>, JpaSpecificationExecutor<Tag> {
Collection<Tag> findByNameIn(List<String> names);
Page<Tag> findAll(Specification<Tag> specification, Pageable pageable);
/**
* 根据标签名称模糊查询
* @param name 标签名称关键词
* @return 标签列表
*/
List<Tag> findByNameContaining(String name);
List<Tag> queryAllByIdIn(Collection<Long> ids);
}

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,8 +16,17 @@ 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", "该分类已存在,请勿重复添加!"),
TAG_NOT_EXIST("20006", "标签不存在!"),
CATEGORY_NOT_EXIST("20007", "分类不存在!"),
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;

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,116 @@
package com.hanserwei.common.utils;
import com.hanserwei.common.model.BasePageQuery;
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.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
/**
* 分页查询工具类
* 用于抽取通用的分页查询逻辑
*/
public class PageHelper {
/**
* 执行带条件的分页查询
*
* @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 searchText,
String searchField,
LocalDate startDate,
LocalDate endDate,
Function<T, R> converter) {
// 构建分页参数
Pageable pageable = PageRequest.of(
pageQuery.getCurrent().intValue() - 1,
pageQuery.getSize().intValue(),
Sort.by(Sort.Direction.DESC, "createTime")
);
// 构建查询条件
Specification<T> specification = (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
// 搜索文本模糊查询
if (StringUtils.hasText(searchText) && StringUtils.hasText(searchField)) {
predicates.add(
criteriaBuilder.like(root.get(searchField), "%" + searchText.trim() + "%")
);
}
// 开始日期查询
if (Objects.nonNull(startDate)) {
predicates.add(
criteriaBuilder.greaterThanOrEqualTo(root.get("createTime"), startDate)
);
}
// 结束日期查询
if (Objects.nonNull(endDate)) {
predicates.add(
criteriaBuilder.lessThan(root.get("createTime"), endDate.plusDays(1).atStartOfDay())
);
}
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
// 执行查询
Page<T> page = repository.findAll(specification, pageable);
// DO 转 VO
List<R> vos = page.getContent().stream()
.map(converter)
.toList();
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);
}
}

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

@@ -44,8 +44,11 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
// 1. 从请求头中获取 Authorization
String header = request.getHeader("Authorization");
String requestURI = request.getRequestURI();
log.info("Request URI: {}", requestURI);
if (requestURI.startsWith("/admin")) {
// 2. 校验头格式 (必须以 Bearer 开头)
if (StringUtils.startsWith(header, "Bearer ")) {
if (header != null && header.startsWith("Bearer ")) {
String token = StringUtils.substring(header, 7);
log.info("JWT Token: {}", token);
if (StringUtils.isNotBlank(token)) {
@@ -97,5 +100,8 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
// 8. 放行请求 (如果没有 Token 或者 Token 校验通过,继续执行下一个过滤器)
filterChain.doFilter(request, response);
} else {
filterChain.doFilter(request, response);
}
}
}

View File

@@ -80,7 +80,7 @@ public class JwtTokenHelper implements InitializingBean {
*/
public String generateToken(String username) {
Instant now = Instant.now();
Instant expireTime = now.plus(1, ChronoUnit.HOURS);
Instant expireTime = now.plus(30, ChronoUnit.DAYS);
return Jwts.builder()
.header().add("type", "JWT").and() // 推荐添加 header

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();
}
}